diff --git a/forge-ai/pom.xml b/forge-ai/pom.xml index 3f176653a65..667324e060d 100644 --- a/forge-ai/pom.xml +++ b/forge-ai/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.36-SNAPSHOT forge-ai diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index 51798704399..b3dc45e4999 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -24,6 +24,7 @@ import forge.ai.ability.AnimateAi; import forge.card.CardTypeView; import forge.game.GameEntity; import forge.game.ability.AbilityFactory; +import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.ability.effects.ProtectEffect; import forge.game.card.*; @@ -464,7 +465,7 @@ public class AiAttackController { final CardCollectionView beastions = ai.getCardsIn(ZoneType.Battlefield, "Beastmaster Ascension"); int minCreatures = 7; for (final Card beastion : beastions) { - final int counters = beastion.getCounters(CounterType.QUEST); + final int counters = beastion.getCounters(CounterEnumType.QUEST); minCreatures = Math.min(minCreatures, 7 - counters); } if (this.attackers.size() >= minCreatures) { @@ -1065,7 +1066,7 @@ public class AiAttackController { } } // if enough damage: switch to next planeswalker or player - if (damage >= pw.getCounters(CounterType.LOYALTY)) { + if (damage >= pw.getCounters(CounterEnumType.LOYALTY)) { List pwDefending = combat.getDefendingPlaneswalkers(); boolean found = false; // look for next planeswalker @@ -1135,7 +1136,6 @@ public class AiAttackController { // TODO Somehow subtract expected damage of other attacking creatures from enemy life total (how? other attackers not yet declared? Can the AI guesstimate which of their creatures will not get blocked?) if (attacker.getCurrentPower() * Integer.parseInt(attacker.getSVar("NonCombatPriority")) < ai.getOpponentsSmallestLifeTotal()) { // Check if the card actually has an ability the AI can and wants to play, if not, attacking is fine! - boolean wantability = false; for (SpellAbility sa : attacker.getSpellAbilities()) { // Do not attack if we can afford using the ability. if (sa.isAbility()) { @@ -1192,7 +1192,7 @@ public class AiAttackController { if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) { numberOfPossibleBlockers += 1; if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false) - && !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterType.P1P1) == 0)) { + && !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterEnumType.P1P1) == 0)) { canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature // see if the defending creature is of higher or lower // value. We don't want to attack only to lose value @@ -1365,21 +1365,12 @@ public class AiAttackController { if (c.hasSVar("AIExertCondition")) { if (!c.getSVar("AIExertCondition").isEmpty()) { final String needsToExert = c.getSVar("AIExertCondition"); - int x = 0; - int y = 0; String sVar = needsToExert.split(" ")[0]; String comparator = needsToExert.split(" ")[1]; String compareTo = comparator.substring(2); - try { - x = Integer.parseInt(sVar); - } catch (final NumberFormatException e) { - x = CardFactoryUtil.xCount(c, c.getSVar(sVar)); - } - try { - y = Integer.parseInt(compareTo); - } catch (final NumberFormatException e) { - y = CardFactoryUtil.xCount(c, c.getSVar(compareTo)); - } + + int x = AbilityUtils.calculateAmount(c, sVar, null); + int y = AbilityUtils.calculateAmount(c, compareTo, null); if (Expressions.compare(x, comparator, y)) { shouldExert = true; } diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index bef6eaa8961..7f1104d95fd 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -228,9 +228,9 @@ public class AiBlockController { // 3.Blockers that can destroy the attacker and have an upside when dying killingBlockers = getKillingBlockers(combat, attacker, blockers); for (Card b : killingBlockers) { - if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterType.P1P1) == 0) || b.hasSVar("SacMe") - || (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1) - || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0) + if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterEnumType.P1P1) == 0) || b.hasSVar("SacMe") + || (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterEnumType.TIME) == 1) + || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterEnumType.FADE) == 0) || b.hasSVar("EndOfTurnLeavePlay")) { blocker = b; break; @@ -299,8 +299,8 @@ public class AiBlockController { final List blockers = getPossibleBlockers(combat, attacker, blockersLeft, true); for (Card b : blockers) { - if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1) - || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0) + if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterEnumType.TIME) == 1) + || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterEnumType.FADE) == 0) || b.hasSVar("EndOfTurnLeavePlay")) { blocker = b; if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) { @@ -851,7 +851,7 @@ public class AiBlockController { damageToPW += ComputerUtilCombat.predictDamageTo((Card) def, pwatkr.getNetCombatDamage(), pwatkr, true); } } - if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= def.getCounters(CounterType.LOYALTY)) { + if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= def.getCounters(CounterEnumType.LOYALTY)) { threatenedPWs.add((Card) def); } } @@ -909,7 +909,7 @@ public class AiBlockController { damageToPW += ComputerUtilCombat.predictDamageTo(pw, pwAtk.getNetCombatDamage(), pwAtk, true); } } - if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterType.LOYALTY)) { + if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterEnumType.LOYALTY)) { for (Card chump : pwDefenders) { if (chosenChumpBlockers.contains(chump)) { combat.removeFromCombat(chump); diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index b4b3293d108..8f941ed6886 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -177,7 +177,7 @@ public class AiController { && CardFactoryUtil.isCounterable(host)) { return true; } else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host) - && host.getCMC() == c.getCounters(CounterType.CHARGE)) { + && host.getCMC() == c.getCounters(CounterEnumType.CHARGE)) { return true; } else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)) { String hostName = host.getName(); @@ -769,7 +769,7 @@ public class AiController { return AiPlayDecision.CantPlayAi; } } - else if (sa.getPayCosts() != null){ + else { Cost payCosts = sa.getPayCosts(); ManaCost mana = payCosts.getTotalMana(); if (mana != null) { @@ -858,7 +858,7 @@ public class AiController { int neededMana = 0; boolean dangerousRecurringCost = false; - Cost costWithBuyback = sa.getPayCosts() != null ? sa.getPayCosts().copy() : Cost.Zero; + Cost costWithBuyback = sa.getPayCosts().copy(); for (OptionalCostValue opt : GameActionUtil.getOptionalCostValues(sa)) { if (opt.getType() == OptionalCost.Buyback) { costWithBuyback.add(opt.getCost()); @@ -907,8 +907,8 @@ public class AiController { public int compare(final SpellAbility a, final SpellAbility b) { // sort from highest cost to lowest // we want the highest costs first - int a1 = a.getPayCosts() == null ? 0 : a.getPayCosts().getTotalMana().getCMC(); - int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC(); + int a1 = a.getPayCosts().getTotalMana().getCMC(); + int b1 = b.getPayCosts().getTotalMana().getCMC(); // deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) { @@ -927,12 +927,12 @@ public class AiController { // deprioritize pump spells with pure energy cost (can be activated last, // since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler) int a2 = 0, b2 = 0; - if (a.getApi() == ApiType.Pump && a.getPayCosts() != null && a.getPayCosts().getCostEnergy() != null) { + if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) { if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { a2 = a.getPayCosts().getCostEnergy().convertAmount(); } } - if (b.getApi() == ApiType.Pump && b.getPayCosts() != null && b.getPayCosts().getCostEnergy() != null) { + if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) { if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { b2 = b.getPayCosts().getCostEnergy().convertAmount(); } @@ -956,8 +956,7 @@ public class AiController { return 1; } - if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi() - && a.getPayCosts() != null && b.getPayCosts() != null) { + if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) { // Cheaper Spectacle costs should be preferred // FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not? // (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs) @@ -1479,7 +1478,7 @@ public class AiController { } for (SpellAbility sa : card.getSpellAbilities()) { - if (sa.getPayCosts() != null && sa.isAbility() + if (sa.isAbility() && sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().getCMC() > 0 && (!sa.getPayCosts().hasTapCost() || !isTapLand) @@ -1802,7 +1801,7 @@ public class AiController { throw new UnsupportedOperationException("AI is not supposed to reach this code at the moment"); } - public CardCollection chooseCardsForEffect(CardCollectionView pool, SpellAbility sa, int min, int max, boolean isOptional) { + public CardCollection chooseCardsForEffect(CardCollectionView pool, SpellAbility sa, int min, int max, boolean isOptional, Map params) { if (sa == null || sa.getApi() == null) { throw new UnsupportedOperationException(); } @@ -1835,7 +1834,7 @@ public class AiController { default: CardCollection editablePool = new CardCollection(pool); for (int i = 0; i < max; i++) { - Card c = player.getController().chooseSingleEntityForEffect(editablePool, sa, null, isOptional); + Card c = player.getController().chooseSingleEntityForEffect(editablePool, sa, null, isOptional, params); if (c != null) { result.add(c); editablePool.remove(c); @@ -1986,6 +1985,35 @@ public class AiController { return MyRandom.getRandom().nextBoolean(); } + public boolean chooseEvenOdd(SpellAbility sa) { + String aiLogic = sa.getParamOrDefault("AILogic", ""); + + if (aiLogic.equals("AlwaysEven")) { + return false; // false is Even + } else if (aiLogic.equals("AlwaysOdd")) { + return true; // true is Odd + } else if (aiLogic.equals("Random")) { + return MyRandom.getRandom().nextBoolean(); + } else if (aiLogic.equals("CMCInHand")) { + CardCollectionView hand = sa.getActivatingPlayer().getCardsIn(ZoneType.Hand); + int numEven = CardLists.filter(hand, CardPredicates.evenCMC()).size(); + int numOdd = CardLists.filter(hand, CardPredicates.oddCMC()).size(); + return numOdd > numEven; + } else if (aiLogic.equals("CMCOppControls")) { + CardCollectionView hand = sa.getActivatingPlayer().getOpponents().getCardsIn(ZoneType.Battlefield); + int numEven = CardLists.filter(hand, CardPredicates.evenCMC()).size(); + int numOdd = CardLists.filter(hand, CardPredicates.oddCMC()).size(); + return numOdd > numEven; + } else if (aiLogic.equals("CMCOppControlsByPower")) { + // TODO: improve this to check for how dangerous those creatures actually are relative to host card + CardCollectionView hand = sa.getActivatingPlayer().getOpponents().getCardsIn(ZoneType.Battlefield); + int powerEven = Aggregates.sum(CardLists.filter(hand, CardPredicates.evenCMC()), Accessors.fnGetNetPower); + int powerOdd = Aggregates.sum(CardLists.filter(hand, CardPredicates.oddCMC()), Accessors.fnGetNetPower); + return powerOdd > powerEven; + } + return MyRandom.getRandom().nextBoolean(); // outside of any specific logic, choose randomly + } + public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List origin, SpellAbility sa, CardCollection fetchList, Player player2, Player decider) { if (useSimulation) { diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java index aa6f83699ca..52abcc6f94b 100644 --- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; import com.google.common.base.Predicate; +import com.google.common.base.Predicates; import com.google.common.collect.Lists; import forge.card.CardType; @@ -16,12 +17,14 @@ import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.card.CardPredicates.Presets; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.cost.*; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityStackInstance; import forge.game.zone.ZoneType; +import forge.util.Aggregates; import forge.util.TextUtil; import forge.util.collect.FCollectionView; @@ -104,6 +107,24 @@ public class AiCostDecision extends CostDecisionMakerBase { } return PaymentDecision.card(randomSubset); } + else if (type.equals("DifferentNames")) { + CardCollection differentNames = new CardCollection(); + CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe")); + while (c > 0) { + Card chosen; + if (!discardMe.isEmpty()) { + chosen = Aggregates.random(discardMe); + discardMe = CardLists.filter(discardMe, Predicates.not(CardPredicates.sharesNameWith(chosen))); + } else { + final Card worst = ComputerUtilCard.getWorstAI(hand); + chosen = worst != null ? worst : Aggregates.random(hand); + } + differentNames.add(chosen); + hand = CardLists.filter(hand, Predicates.not(CardPredicates.sharesNameWith(chosen))); + c--; + } + return PaymentDecision.card(differentNames); + } else { final AiController aic = ((PlayerControllerAi)player.getController()).getAi(); @@ -329,7 +350,7 @@ public class AiCostDecision extends CostDecisionMakerBase { } CardCollectionView topLib = player.getCardsIn(ZoneType.Library, c); - return topLib.size() < c ? null : PaymentDecision.card(topLib); + return topLib.size() < c ? null : PaymentDecision.number(c); } @Override @@ -494,7 +515,7 @@ public class AiCostDecision extends CostDecisionMakerBase { @Override public boolean apply(Card card) { for (final SpellAbility sa : card.getSpellAbilities()) { - if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.isManaAbility() && sa.getPayCosts().hasTapCost()) { return true; } } @@ -627,41 +648,41 @@ public class AiCostDecision extends CostDecisionMakerBase { // the first things are benefit from removing counters // try to remove +1/+1 counter from undying creature - List prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.P1P1, c), + List prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.P1P1, c), CardPredicates.hasKeyword("Undying")); if (!prefs.isEmpty()) { - Collections.sort(prefs, CardPredicates.compareByCounterType(CounterType.P1P1)); + Collections.sort(prefs, CardPredicates.compareByCounterType(CounterEnumType.P1P1)); PaymentDecision result = PaymentDecision.card(prefs); - result.ct = CounterType.P1P1; + result.ct = CounterType.get(CounterEnumType.P1P1); return result; } // try to remove -1/-1 counter from persist creature - prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.M1M1, c), + prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.M1M1, c), CardPredicates.hasKeyword("Persist")); if (!prefs.isEmpty()) { - Collections.sort(prefs, CardPredicates.compareByCounterType(CounterType.M1M1)); + Collections.sort(prefs, CardPredicates.compareByCounterType(CounterEnumType.M1M1)); PaymentDecision result = PaymentDecision.card(prefs); - result.ct = CounterType.M1M1; + result.ct = CounterType.get(CounterEnumType.M1M1); return result; } // try to remove Time counter from Chronozoa, it will generate more - prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.TIME, c), + prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.TIME, c), CardPredicates.nameEquals("Chronozoa")); if (!prefs.isEmpty()) { - Collections.sort(prefs, CardPredicates.compareByCounterType(CounterType.TIME)); + Collections.sort(prefs, CardPredicates.compareByCounterType(CounterEnumType.TIME)); PaymentDecision result = PaymentDecision.card(prefs); - result.ct = CounterType.TIME; + result.ct = CounterType.get(CounterEnumType.TIME); return result; } // try to remove Quest counter on something with enough counters for the // effect to continue - prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.QUEST, c)); + prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.QUEST, c)); if (!prefs.isEmpty()) { prefs = CardLists.filter(prefs, new Predicate() { @@ -673,12 +694,12 @@ public class AiCostDecision extends CostDecisionMakerBase { if (crd.hasSVar("MaxQuestEffect")) { e = Integer.parseInt(crd.getSVar("MaxQuestEffect")); } - return crd.getCounters(CounterType.QUEST) >= e + c; + return crd.getCounters(CounterEnumType.QUEST) >= e + c; } }); - Collections.sort(prefs, Collections.reverseOrder(CardPredicates.compareByCounterType(CounterType.QUEST))); + Collections.sort(prefs, Collections.reverseOrder(CardPredicates.compareByCounterType(CounterEnumType.QUEST))); PaymentDecision result = PaymentDecision.card(prefs); - result.ct = CounterType.QUEST; + result.ct = CounterType.get(CounterEnumType.QUEST); return result; } @@ -775,7 +796,7 @@ public class AiCostDecision extends CostDecisionMakerBase { @Override public boolean apply(final Card crd) { for (Map.Entry e : crd.getCounters().entrySet()) { - if (e.getValue() >= c && (ctr.equals("ANY") || e.getKey() == CounterType.valueOf(ctr))) { + if (e.getValue() >= c && (ctr.equals("ANY") || e.getKey().equals(CounterType.getType(ctr)))) { return true; } } @@ -787,7 +808,7 @@ public class AiCostDecision extends CostDecisionMakerBase { PaymentDecision result = PaymentDecision.card(card); for (Map.Entry e : card.getCounters().entrySet()) { - if (e.getValue() >= c && (ctr.equals("ANY") || e.getKey() == CounterType.valueOf(ctr))) { + if (e.getValue() >= c && (ctr.equals("ANY") || e.getKey().equals(CounterType.getType(ctr)))) { result.ct = e.getKey(); break; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index ff73d875785..5cd886aaf7f 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -65,7 +65,7 @@ import java.util.*; *

* ComputerUtil class. *

- * + * * @author Forge * @version $Id$ */ @@ -91,9 +91,6 @@ public class ComputerUtil { } } - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -216,12 +213,9 @@ public class ComputerUtil { sa.setActivatingPlayer(ai); if (!ComputerUtilCost.canPayCost(sa, ai)) return false; - + final Card source = sa.getHostCard(); if (sa.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -246,9 +240,6 @@ public class ComputerUtil { final Card source = sa.getHostCard(); if (sa.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -267,9 +258,6 @@ public class ComputerUtil { final Card source = newSA.getHostCard(); if (newSA.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(newSA); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); newSA.setHostCard(game.getAction().moveToStack(source, sa)); if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) { @@ -290,9 +278,6 @@ public class ComputerUtil { if (ComputerUtilCost.canPayCost(sa, ai)) { final Card source = sa.getHostCard(); if (sa.isSpell() && !source.isCopiedSpell()) { - source.setCastSA(sa); - sa.setLastStateBattlefield(game.getLastStateBattlefield()); - sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -364,8 +349,8 @@ public class ComputerUtil { for (int ip = 0; ip < 6; ip++) { final int priority = 6 - ip; if (priority == 2 && ai.isCardInPlay("Crucible of Worlds")) { - CardCollection landsInPlay = CardLists.getType(typeList, "Land"); - if (!landsInPlay.isEmpty()) { + CardCollection landsInPlay = CardLists.getType(typeList, "Land"); + if (!landsInPlay.isEmpty()) { // Don't need more land. return ComputerUtilCard.getWorstLand(landsInPlay); } @@ -394,16 +379,16 @@ public class ComputerUtil { return ComputerUtilCard.getWorstLand(landsInPlay); } } - + // try everything when about to die - if (game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS) - && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) { - final CardCollection nonCreatures = CardLists.getNotType(typeList, "Creature"); - if (!nonCreatures.isEmpty()) { - return ComputerUtilCard.getWorstAI(nonCreatures); - } else if (!typeList.isEmpty()) { - return ComputerUtilCard.getWorstAI(typeList); - } + if (game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS) + && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) { + final CardCollection nonCreatures = CardLists.getNotType(typeList, "Creature"); + if (!nonCreatures.isEmpty()) { + return ComputerUtilCard.getWorstAI(nonCreatures); + } else if (!typeList.isEmpty()) { + return ComputerUtilCard.getWorstAI(typeList); + } } } else if (pref.contains("DiscardCost")) { // search for permanents with DiscardMe @@ -465,14 +450,14 @@ public class ComputerUtil { return ComputerUtilCard.getWorstLand(landsInHand); } } - + // try everything when about to die if (activate != null && "Reality Smasher".equals(activate.getName()) || - game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS) - && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) { - if (!typeList.isEmpty()) { - return ComputerUtilCard.getWorstAI(typeList); - } + game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS) + && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) { + if (!typeList.isEmpty()) { + return ComputerUtilCard.getWorstAI(typeList); + } } } else if (pref.contains("DonateMe")) { // search for permanents with DonateMe. priority 1 is the lowest, priority 5 the highest @@ -555,7 +540,7 @@ public class ComputerUtil { public static CardCollection chooseExileFrom(final Player ai, final ZoneType zone, final String type, final Card activate, final Card target, final int amount) { CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(zone), type.split(";"), activate.getController(), activate, null); - + if ((target != null) && target.getController() == ai) { typeList.remove(target); // don't exile the card we're pumping } @@ -576,7 +561,7 @@ public class ComputerUtil { public static CardCollection choosePutToLibraryFrom(final Player ai, final ZoneType zone, final String type, final Card activate, final Card target, final int amount) { CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(zone), type.split(";"), activate.getController(), activate, null); - + if ((target != null) && target.getController() == ai) { typeList.remove(target); // don't move the card we're pumping } @@ -587,11 +572,11 @@ public class ComputerUtil { CardLists.sortByPowerAsc(typeList); final CardCollection list = new CardCollection(); - + if (zone != ZoneType.Hand) { Collections.reverse(typeList); } - + for (int i = 0; i < amount; i++) { list.add(typeList.get(i)); } @@ -651,7 +636,7 @@ public class ComputerUtil { } ComputerUtilCard.sortByEvaluateCreature(typeList); Collections.reverse(typeList); - + final CardCollection tapList = new CardCollection(); // Accumulate from "worst" creature @@ -724,7 +709,7 @@ public class ComputerUtil { return returnList; } - public static CardCollection choosePermanentsToSacrifice(final Player ai, final CardCollectionView cardlist, final int amount, final SpellAbility source, + public static CardCollection choosePermanentsToSacrifice(final Player ai, final CardCollectionView cardlist, final int amount, final SpellAbility source, final boolean destroy, final boolean isOptional) { CardCollection remaining = new CardCollection(cardlist); final CardCollection sacrificed = new CardCollection(); @@ -733,9 +718,9 @@ public class ComputerUtil { final int considerSacThreshold = getAIPreferenceParameter(host, "CreatureEvalThreshold"); if ("OpponentOnly".equals(source.getParam("AILogic"))) { - if(!source.getActivatingPlayer().isOpponentOf(ai)) { - return sacrificed; // sacrifice none - } + if(!source.getActivatingPlayer().isOpponentOf(ai)) { + return sacrificed; // sacrifice none + } } else if ("DesecrationDemon".equals(source.getParam("AILogic"))) { if (!SpecialCardAi.DesecrationDemon.considerSacrificingCreature(ai, source)) { return sacrificed; // don't sacrifice unless in special conditions specified by DesecrationDemon AI @@ -753,27 +738,27 @@ public class ComputerUtil { boolean removedSelf = false; if (isOptional && source.hasParam("Devour") || source.hasParam("Exploit") || considerSacLogic) { - if (source.hasParam("Exploit")) { - for (Trigger t : host.getTriggers()) { - if (t.getMode() == TriggerType.Exploited) { - final String execute = t.getParam("Execute"); - if (execute == null) { - continue; - } - final SpellAbility exSA = AbilityFactory.getAbility(host.getSVar(execute), host); + if (source.hasParam("Exploit")) { + for (Trigger t : host.getTriggers()) { + if (t.getMode() == TriggerType.Exploited) { + final String execute = t.getParam("Execute"); + if (execute == null) { + continue; + } + final SpellAbility exSA = AbilityFactory.getAbility(host.getSVar(execute), host); - exSA.setActivatingPlayer(ai); - exSA.setTrigger(true); + exSA.setActivatingPlayer(ai); + exSA.setTrigger(true); - // Run non-mandatory trigger. - // These checks only work if the Executing SpellAbility is an Ability_Sub. - if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA.getApi()).doTriggerAI(ai, exSA, false)) { - // AI would not run this trigger if given the chance - return sacrificed; - } - } - } - } + // Run non-mandatory trigger. + // These checks only work if the Executing SpellAbility is an Ability_Sub. + if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA.getApi()).doTriggerAI(ai, exSA, false)) { + // AI would not run this trigger if given the chance + return sacrificed; + } + } + } + } remaining = CardLists.filter(remaining, new Predicate() { @Override public boolean apply(final Card c) { @@ -834,7 +819,7 @@ public class ComputerUtil { if (ai.isOpponentOf(c.getController())) return c; } - + if (destroy) { final CardCollection indestructibles = CardLists.getKeyword(remaining, Keyword.INDESTRUCTIBLE); if (!indestructibles.isEmpty()) { @@ -924,7 +909,7 @@ public class ComputerUtil { } catch (final Exception ex) { throw new RuntimeException(TextUtil.concatNoSpace("There is an error in the card code for ", c.getName(), ":", ex.getMessage()), ex); - } + } } } @@ -973,16 +958,16 @@ public class ComputerUtil { final Card card = sa.getHostCard(); if (card.hasSVar("PlayMain1")) { - if (card.getSVar("PlayMain1").equals("ALWAYS") || sa.getPayCosts().hasNoManaCost()) { - return true; - } else if (card.getSVar("PlayMain1").equals("OPPONENTCREATURES")) { - //Only play these main1 when the opponent has creatures (stealing and giving them haste) - if (!ai.getOpponents().getCreaturesInPlay().isEmpty()) { - return true; - } - } else if (!card.getController().getCreaturesInPlay().isEmpty()) { - return true; - } + if (card.getSVar("PlayMain1").equals("ALWAYS") || sa.getPayCosts().hasNoManaCost()) { + return true; + } else if (card.getSVar("PlayMain1").equals("OPPONENTCREATURES")) { + //Only play these main1 when the opponent has creatures (stealing and giving them haste) + if (!ai.getOpponents().getCreaturesInPlay().isEmpty()) { + return true; + } + } else if (!card.getController().getCreaturesInPlay().isEmpty()) { + return true; + } } // try not to cast Raid creatures in main 1 if an attack is likely @@ -995,7 +980,7 @@ public class ComputerUtil { } if (card.getManaCost().isZero()) { - return true; + return true; } if (card.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) { @@ -1023,9 +1008,9 @@ public class ComputerUtil { && (card.hasKeyword(Keyword.HASTE) || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) { return true; } - + if (card.hasKeyword(Keyword.EXALTED)) { - return true; + return true; } //cast equipments in Main1 when there are creatures to equip and no other unequipped equipment @@ -1155,7 +1140,7 @@ public class ComputerUtil { if (discard.hasSVar("DiscardMe")) { return true; } - + final Game game = ai.getGame(); final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS); final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS); @@ -1255,11 +1240,11 @@ public class ComputerUtil { } } } // AntiBuffedBy - - if (sub != null) { + + if (sub != null) { return castSpellInMain1(ai, sub); } - + return false; } @@ -1268,7 +1253,7 @@ public class ComputerUtil { int activations = sa.getActivationsThisTurn(); if (!sa.isIntrinsic()) { - return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory + return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory } if (activations < 10) { //10 activations per turn should still be acceptable @@ -1285,27 +1270,27 @@ public class ComputerUtil { return false; } if (abCost.hasTapCost() && source.hasSVar("AITapDown")) { - return true; + return true; } else if (sa.hasParam("Planeswalker") && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) { - for (final CostPart part : abCost.getCostParts()) { - if (part instanceof CostPutCounter) { - return true; - } - } + for (final CostPart part : abCost.getCostParts()) { + if (part instanceof CostPutCounter) { + return true; + } + } } for (final CostPart part : abCost.getCostParts()) { if (part instanceof CostSacrifice) { final CostSacrifice sac = (CostSacrifice) part; - + final String type = sac.getType(); - + if (type.equals("CARDNAME")) { if (source.getSVar("SacMe").equals("6")) { return true; } continue; } - + final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(","), source.getController(), source, sa); for (Card c : typeList) { @@ -1340,14 +1325,14 @@ public class ComputerUtil { Map params = stAb.getMapParams(); if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword") && params.get("AddKeyword").contains("Haste")) { - + if (c.isEquipment() && c.getEquipping() == null) { return true; } final String affected = params.get("Affected"); if (affected.contains("Creature.YouCtrl") - || affected.contains("Other+YouCtrl")) { + || affected.contains("Other+YouCtrl")) { return true; } else if (affected.contains("Creature.PairedWith") && !c.isPaired()) { return true; @@ -1356,10 +1341,10 @@ public class ComputerUtil { } for (Trigger t : c.getTriggers()) { - Map params = t.getMapParams(); + Map params = t.getMapParams(); if (!"ChangesZone".equals(params.get("Mode")) - || !"Battlefield".equals(params.get("Destination")) - || !params.containsKey("ValidCard")) { + || !"Battlefield".equals(params.get("Destination")) + || !params.containsKey("ValidCard")) { continue; } @@ -1367,7 +1352,7 @@ public class ComputerUtil { if (valid.contains("Creature.YouCtrl") || valid.contains("Other+YouCtrl") ) { - final SpellAbility sa = t.getTriggeredSA(); + final SpellAbility sa = t.getOverridingAbility(); if (sa != null && sa.getApi() == ApiType.Pump && sa.hasParam("KW") && sa.getParam("KW").contains("Haste")) { return true; @@ -1375,10 +1360,10 @@ public class ComputerUtil { } } } - + all.addAll(ai.getCardsActivableInExternalZones(true)); all.addAll(ai.getCardsIn(ZoneType.Hand)); - + for (final Card c : all) { for (final SpellAbility sa : c.getSpellAbilities()) { if (sa.getApi() == ApiType.Pump && sa.hasParam("KW") && sa.getParam("KW").contains("Haste")) { @@ -1413,10 +1398,10 @@ public class ComputerUtil { public static boolean hasAFogEffect(final Player ai) { final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); - + all.addAll(ai.getCardsActivableInExternalZones(true)); all.addAll(ai.getCardsIn(ZoneType.Hand)); - + for (final Card c : all) { for (final SpellAbility sa : c.getSpellAbilities()) { if (sa.getApi() != ApiType.Fog) { @@ -1446,7 +1431,7 @@ public class ComputerUtil { final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); all.addAll(ai.getCardsActivableInExternalZones(true)); all.addAll(CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(Presets.PERMANENTS))); - + for (final Card c : all) { for (final SpellAbility sa : c.getSpellAbilities()) { if (sa.getApi() != ApiType.DealDamage) { @@ -1505,7 +1490,7 @@ public class ComputerUtil { /** * Returns list of objects threatened by effects on the stack - * + * * @param ai * calling player * @param sa @@ -1520,7 +1505,7 @@ public class ComputerUtil { if (game.getStack().isEmpty()) { return objects; } - + // check stack for something that will kill this for (SpellAbilityStackInstance si : game.getStack()) { // iterate from top of stack to find SpellAbility, including sub-abilities, @@ -1538,8 +1523,8 @@ public class ComputerUtil { if (top) { break; // only evaluate top-stack } - } - + } + return objects; } @@ -1551,14 +1536,14 @@ public class ComputerUtil { int toughness = 0; boolean grantIndestructible = false; boolean grantShroud = false; - + if (topStack == null) { return objects; } - + final Card source = topStack.getHostCard(); final ApiType threatApi = topStack.getApi(); - + // Can only Predict things from AFs if (threatApi == null) { return threatened; @@ -1572,7 +1557,7 @@ public class ComputerUtil { CardCollectionView battleField = aiPlayer.getCardsIn(ZoneType.Battlefield); objects = CardLists.getValidCards(battleField, topStack.getParam("ValidCards").split(","), source.getController(), source, topStack); } else { - return threatened; + return threatened; } } else { objects = topStack.getTargets().getTargets(); @@ -1586,7 +1571,7 @@ public class ComputerUtil { } } if (canBeTargeted.isEmpty()) { - return threatened; + return threatened; } objects = canBeTargeted; } @@ -1655,7 +1640,7 @@ public class ComputerUtil { } // don't use it on creatures that can't be regenerated - if ((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll) && + if ((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll) && (!c.canBeShielded() || noRegen)) { continue; } @@ -1667,14 +1652,14 @@ public class ComputerUtil { continue; } } - + if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) { boolean canSave = ComputerUtilCombat.predictDamageTo(c, dmg - toughness, source, false) < ComputerUtilCombat.getDamageToKill(c); if (!canSave) { continue; } } - + // cannot protect against source if (saviourApi == ApiType.Protection && (ProtectAi.toProtectFrom(source, saviour) == null)) { continue; @@ -1685,7 +1670,7 @@ public class ComputerUtil { if (saviourApi == ApiType.ChangeZone && (c.getOwner().isOpponentOf(aiPlayer) || c.isToken())) { continue; } - + if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c)) { threatened.add(c); } @@ -1704,7 +1689,7 @@ public class ComputerUtil { } // -Toughness Curse else if ((threatApi == ApiType.Pump || threatApi == ApiType.PumpAll && topStack.isCurse()) - && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll + && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll || saviourApi == ApiType.Protection || saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll || saviourApi == null)) { final int dmg = -AbilityUtils.calculateAmount(topStack.getHostCard(), @@ -1717,7 +1702,7 @@ public class ComputerUtil { if (!canRemove) { continue; } - + if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) { final boolean cantSave = c.getNetToughness() + toughness <= dmg || (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && !grantIndestructible @@ -1726,14 +1711,14 @@ public class ComputerUtil { continue; } } - + if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) { boolean canSave = c.getNetToughness() + toughness > dmg; if (!canSave) { continue; } } - + if (saviourApi == ApiType.Protection) { if (tgt == null || (ProtectAi.toProtectFrom(source, saviour) == null)) { continue; @@ -1827,9 +1812,9 @@ public class ComputerUtil { } } //GainControl - else if ((threatApi == ApiType.GainControl - || (threatApi == ApiType.Attach && topStack.hasParam("AILogic") && topStack.getParam("AILogic").equals("GainControl") )) - && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll + else if ((threatApi == ApiType.GainControl + || (threatApi == ApiType.Attach && topStack.hasParam("AILogic") && topStack.getParam("AILogic").equals("GainControl") )) + && (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll || saviourApi == ApiType.Protection || saviourApi == null)) { for (final Object o : objects) { if (o instanceof Card) { @@ -1946,7 +1931,7 @@ public class ComputerUtil { public static int scoreHand(CardCollectionView handList, Player ai, int cardsToReturn) { // TODO Improve hand scoring in relation to cards to return. // If final hand size is 5, score a hand based on what that 5 would be. - // Or if this is really really fast, determine what the 5 would be based on scoring + // Or if this is really really fast, determine what the 5 would be based on scoring // All of the possibilities final AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); @@ -2029,16 +2014,16 @@ public class ComputerUtil { final CardCollectionView handList = ai.getCardsIn(ZoneType.Hand); return scoreHand(handList, ai, cardsToReturn) <= 0; } - + public static CardCollection getPartialParisCandidates(Player ai) { // Commander no longer uses partial paris. final CardCollection candidates = new CardCollection(); final CardCollectionView handList = ai.getCardsIn(ZoneType.Hand); - + final CardCollection lands = CardLists.getValidCards(handList, "Card.Land", ai, null); final CardCollection nonLands = CardLists.getValidCards(handList, "Card.nonLand", ai, null); CardLists.sortByCmcDesc(nonLands); - + if (lands.size() >= 3 && lands.size() <= 4) { return candidates; } @@ -2046,7 +2031,7 @@ public class ComputerUtil { //Not enough lands! int tgtCandidates = Math.max(Math.abs(lands.size()-nonLands.size()), 3); System.out.println("Partial Paris: " + ai.getName() + " lacks lands, aiming to exile " + tgtCandidates + " cards."); - + for (int i=0;i manaArts = Arrays.asList("Mox Pearl", "Mox Sapphire", "Mox Jet", "Mox Ruby", "Mox Emerald"); - + // evaluate creatures available in deck CardCollectionView allCreatures = CardLists.filter(allCards, Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isOwner(player))); int numCards = allCreatures.size(); @@ -2200,7 +2185,7 @@ public class ComputerUtil { } Collections.sort(goodChoices, CardLists.TextLenComparator); - + CardLists.sortByCmcDesc(goodChoices); dChoices.add(goodChoices.get(0)); @@ -2211,15 +2196,18 @@ public class ComputerUtil { if (p == aiChooser) { // ask that ai player what he would like to discard final AiController aic = ((PlayerControllerAi)p.getController()).getAi(); return aic.getCardsToDiscard(min, max, validCards, sa); - } + } // no special options for human or remote friends return getCardsToDiscardFromOpponent(aiChooser, p, sa, validCards, min, max); } - public static String chooseSomeType(Player ai, String kindOfType, String logic, List invalidTypes) { + public static String chooseSomeType(Player ai, String kindOfType, String logic, Collection validTypes, List invalidTypes) { if (invalidTypes == null) { invalidTypes = ImmutableList.of(); } + if (validTypes == null) { + validTypes = ImmutableList.of(); + } final Game game = ai.getGame(); String chosen = ""; @@ -2243,7 +2231,7 @@ public class ComputerUtil { } } if (StringUtils.isEmpty(chosen)) { - chosen = "Creature"; + chosen = validTypes.isEmpty() ? "Creature" : Aggregates.random(validTypes); } } else if (kindOfType.equals("Creature")) { if (logic != null) { @@ -2257,7 +2245,7 @@ public class ComputerUtil { chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Battlefield), valid); } else if (logic.equals("MostProminentOppControls")) { - CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); + CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); chosen = ComputerUtilCard.getMostProminentType(list, valid); if (!CardType.isACreatureType(chosen) || invalidTypes.contains(chosen)) { list = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents()); @@ -2284,11 +2272,11 @@ public class ComputerUtil { chosen = ComputerUtilCard.getMostProminentType(list, valid); } else if (logic.equals("MostNeededType")) { - // Choose a type that is in the deck, but not in hand or on the battlefield + // Choose a type that is in the deck, but not in hand or on the battlefield final List basics = new ArrayList<>(CardType.Constant.BASIC_TYPES); CardCollectionView presentCards = CardCollection.combine(ai.getCardsIn(ZoneType.Battlefield), ai.getCardsIn(ZoneType.Hand)); CardCollectionView possibleCards = ai.getAllCards(); - + for (String b : basics) { if (!Iterables.any(presentCards, CardPredicates.isType(b)) && Iterables.any(possibleCards, CardPredicates.isType(b))) { chosen = b; @@ -2338,13 +2326,15 @@ public class ComputerUtil { return chosen; } - public static Object vote(Player ai, List options, SpellAbility sa, Multimap votes) { + public static Object vote(Player ai, List options, SpellAbility sa, Multimap votes, Player forPlayer) { final Card source = sa.getHostCard(); final Player controller = source.getController(); final Game game = controller.getGame(); boolean opponent = controller.isOpponentOf(ai); + final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1); + if (!sa.hasParam("AILogic")) { return Aggregates.random(options); } @@ -2398,7 +2388,7 @@ public class ComputerUtil { } } // is it can't receive counters, choose +1/+1 ones - if (!source.canReceiveCounters(CounterType.P1P1)) { + if (!source.canReceiveCounters(p1p1Type)) { return opponent ? "Feather" : "Quill"; } // if source is not on the battlefield anymore, choose +1/+1 @@ -2430,7 +2420,7 @@ public class ComputerUtil { Card token = TokenAi.spawnToken(controller, saToken); // is it can't receive counters, choose +1/+1 ones - if (!source.canReceiveCounters(CounterType.P1P1)) { + if (!source.canReceiveCounters(p1p1Type)) { return opponent ? "Strength" : "Numbers"; } @@ -2440,7 +2430,7 @@ public class ComputerUtil { } // token would not survive - if (token == null) { + if (token == null || !token.isCreature() || token.getNetToughness() < 1) { return opponent ? "Numbers" : "Strength"; } @@ -2453,11 +2443,11 @@ public class ComputerUtil { Card sourceNumbers = CardUtil.getLKICopy(source); Card sourceStrength = CardUtil.getLKICopy(source); - sourceNumbers.setCounters(CounterType.P1P1, sourceNumbers.getCounters(CounterType.P1P1) + numStrength); + sourceNumbers.setCounters(p1p1Type, sourceNumbers.getCounters(p1p1Type) + numStrength); sourceNumbers.setZone(source.getZone()); - sourceStrength.setCounters(CounterType.P1P1, - sourceStrength.getCounters(CounterType.P1P1) + numStrength + 1); + sourceStrength.setCounters(p1p1Type, + sourceStrength.getCounters(p1p1Type) + numStrength + 1); sourceStrength.setZone(source.getZone()); int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers; @@ -2479,7 +2469,7 @@ public class ComputerUtil { } // is it can't receive counters, choose +1/+1 ones - if (!source.canReceiveCounters(CounterType.P1P1)) { + if (!source.canReceiveCounters(p1p1Type)) { return opponent ? "Sprout" : "Harvest"; } @@ -2556,11 +2546,11 @@ public class ComputerUtil { }); return ComputerUtilCard.getBestCreatureAI(killables); } - + public static int predictDamageFromSpell(final SpellAbility sa, final Player targetPlayer) { int damage = -1; // returns -1 if the spell does not deal damage final Card card = sa.getHostCard(); - + SpellAbility ab = sa; while (ab != null) { if (ab.getApi() == ApiType.DealDamage) { @@ -2579,12 +2569,12 @@ public class ComputerUtil { } ab = ab.getSubAbility(); } - + return damage; } - + public static int getDamageForPlaying(final Player player, final SpellAbility sa) { - + // check for bad spell cast triggers int damage = 0; final Game game = player.getGame(); @@ -2614,7 +2604,7 @@ public class ComputerUtil { continue; } } - + if (trigParams.containsKey("ValidActivatingPlayer")) { if (!player.isValid(trigParams.get("ValidActivatingPlayer"), source.getController(), source, sa)) { continue; @@ -2674,7 +2664,7 @@ public class ComputerUtil { } } } - + return damage; } @@ -2697,7 +2687,7 @@ public class ComputerUtil { if (!trigger.requirementsCheck(game)) { continue; } - if (trigParams.containsKey("CheckOnTriggeredCard") + if (trigParams.containsKey("CheckOnTriggeredCard") && AbilityUtils.getDefinedCards(permanent, source.getSVar(trigParams.get("CheckOnTriggeredCard").split(" ")[0]), null).isEmpty()) { continue; } @@ -2771,27 +2761,27 @@ public class ComputerUtil { } public static boolean isNegativeCounter(CounterType type, Card c) { - return type == CounterType.AGE || type == CounterType.BRIBERY || type == CounterType.DOOM - || type == CounterType.M1M1 || type == CounterType.M0M2 || type == CounterType.M0M1 - || type == CounterType.M1M0 || type == CounterType.M2M1 || type == CounterType.M2M2 + return type.is(CounterEnumType.AGE) || type.is(CounterEnumType.BRIBERY) || type.is(CounterEnumType.DOOM) + || type.is(CounterEnumType.M1M1) || type.is(CounterEnumType.M0M2) || type.is(CounterEnumType.M0M1) + || type.is(CounterEnumType.M1M0) || type.is(CounterEnumType.M2M1) || type.is(CounterEnumType.M2M2) // Blaze only hurts Lands - || (type == CounterType.BLAZE && c.isLand()) + || (type.is(CounterEnumType.BLAZE) && c.isLand()) // Iceberg does use Ice as Storage - || (type == CounterType.ICE && !"Iceberg".equals(c.getName())) + || (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName())) // some lands does use Depletion as Storage Counter - || (type == CounterType.DEPLETION && c.hasKeyword("CARDNAME doesn't untap during your untap step.")) + || (type.is(CounterEnumType.DEPLETION) && c.hasKeyword("CARDNAME doesn't untap during your untap step.")) // treat Time Counters on suspended Cards as Bad, // and also on Chronozoa - || (type == CounterType.TIME && (!c.isInPlay() || "Chronozoa".equals(c.getName()))) - || type == CounterType.GOLD || type == CounterType.MUSIC || type == CounterType.PUPA - || type == CounterType.PARALYZATION || type == CounterType.SHELL || type == CounterType.SLEEP - || type == CounterType.SLUMBER || type == CounterType.SLEIGHT || type == CounterType.WAGE; + || (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName()))) + || type.is(CounterEnumType.GOLD) || type.is(CounterEnumType.MUSIC) || type.is(CounterEnumType.PUPA) + || type.is(CounterEnumType.PARALYZATION) || type.is(CounterEnumType.SHELL) || type.is(CounterEnumType.SLEEP) + || type.is(CounterEnumType.SLUMBER) || type.is(CounterEnumType.SLEIGHT) || type.is(CounterEnumType.WAGE); } // this countertypes has no effect public static boolean isUselessCounter(CounterType type) { - return type == CounterType.AWAKENING || type == CounterType.MANIFESTATION || type == CounterType.PETRIFICATION - || type == CounterType.TRAINING; + return type.is(CounterEnumType.AWAKENING) || type.is(CounterEnumType.MANIFESTATION) || type.is(CounterEnumType.PETRIFICATION) + || type.is(CounterEnumType.TRAINING); } public static Player evaluateBoardPosition(final List listToEvaluate) { @@ -2903,7 +2893,7 @@ public class ComputerUtil { return false; } - + public static boolean targetPlayableSpellCard(final Player ai, CardCollection options, final SpellAbility sa, final boolean withoutPayingManaCost) { // determine and target a card with a SA that the AI can afford and will play AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index 2550137674d..e844a474749 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -113,9 +113,7 @@ public class ComputerUtilAbility { List priorityAltSa = Lists.newArrayList(); List otherAltSa = Lists.newArrayList(); for (SpellAbility altSa : saAltCosts) { - if (altSa.getPayCosts() == null || sa.getPayCosts() == null) { - otherAltSa.add(altSa); - } else if (sa.getPayCosts().isOnlyManaCost() + if (sa.getPayCosts().isOnlyManaCost() && altSa.getPayCosts().isOnlyManaCost() && sa.getPayCosts().getTotalMana().compareTo(altSa.getPayCosts().getTotalMana()) == 1) { // the alternative cost is strictly cheaper, so why not? (e.g. Omniscience etc.) priorityAltSa.add(altSa); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index b27476cae09..9d66fb23171 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -5,7 +5,6 @@ import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; - import forge.card.CardType; import forge.card.ColorSet; import forge.card.MagicColor; @@ -172,30 +171,30 @@ public class ComputerUtilCard { // if no non-basic lands, target the least represented basic land type String sminBL = ""; - int iminBL = 20000; // hopefully no one will ever have more than 20000 - // lands of one type.... + int iminBL = Integer.MAX_VALUE; int n = 0; for (String name : MagicColor.Constant.BASIC_LANDS) { n = CardLists.getType(land, name).size(); - if ((n < iminBL) && (n > 0)) { - // if two or more are tied, only the - // first - // one checked will be used + if (n < iminBL && n > 0) { iminBL = n; sminBL = name; } } - if (iminBL == 20000) { - return null; // no basic land was a minimum + if (iminBL == Integer.MAX_VALUE) { + // All basic lands have no basic land type. Just return something + Iterator untapped = Iterables.filter(land, CardPredicates.Presets.UNTAPPED).iterator(); + if (untapped.hasNext()) { + return untapped.next(); + } + return land.get(0); } final List bLand = CardLists.getType(land, sminBL); - + for (Card ut : Iterables.filter(bLand, CardPredicates.Presets.UNTAPPED)) { return ut; } - return Aggregates.random(bLand); // random tapped land of least represented type } @@ -1423,8 +1422,8 @@ public class ComputerUtilCard { if (combat.isAttacking(c) && opp.getLife() > 0) { int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true); int pumpedDmg = ComputerUtilCombat.damageIfUnblocked(pumped, opp, pumpedCombat, true); - int poisonOrig = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(c, ai) : 0; - int poisonPumped = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0; + int poisonOrig = opp.canReceiveCounters(CounterEnumType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(c, ai) : 0; + int poisonPumped = opp.canReceiveCounters(CounterEnumType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0; // predict Infect if (pumpedDmg == 0 && c.hasKeyword(Keyword.INFECT)) { @@ -1447,7 +1446,7 @@ public class ComputerUtilCard { } if (pumpedDmg > dmg) { if ((!c.hasKeyword(Keyword.INFECT) && pumpedDmg >= opp.getLife()) - || (c.hasKeyword(Keyword.INFECT) && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters()) + || (c.hasKeyword(Keyword.INFECT) && opp.canReceiveCounters(CounterEnumType.POISON) && pumpedDmg >= opp.getPoisonCounters()) || ("PumpForTrample".equals(sa.getParam("AILogic")))) { return true; } @@ -1475,7 +1474,7 @@ public class ComputerUtilCard { if (totalPowerUnblocked >= opp.getLife()) { return true; } else if (totalPowerUnblocked > dmg && sa.getHostCard() != null && sa.getHostCard().isInPlay()) { - if (sa.getPayCosts() != null && sa.getPayCosts().hasNoManaCost()) { + if (sa.getPayCosts().hasNoManaCost()) { return true; // always activate abilities which cost no mana and which can increase unblocked damage } } @@ -1766,10 +1765,10 @@ public class ComputerUtilCard { } public static boolean hasActiveUndyingOrPersist(final Card c) { - if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterType.P1P1) == 0) { + if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterEnumType.P1P1) == 0) { return true; } - if (c.hasKeyword(Keyword.PERSIST) && c.getCounters(CounterType.M1M1) == 0) { + if (c.hasKeyword(Keyword.PERSIST) && c.getCounters(CounterEnumType.M1M1) == 0) { return true; } return false; @@ -1786,10 +1785,6 @@ public class ComputerUtilCard { for (Card c : otb) { for (SpellAbility sa : c.getSpellAbilities()) { - if (sa.getPayCosts() == null) { - continue; - } - CostPayEnergy energyCost = sa.getPayCosts().getCostEnergy(); if (energyCost != null) { int amount = energyCost.convertAmount(); @@ -1913,21 +1908,12 @@ public class ComputerUtilCard { } if (card.getSVar(needsToPlayVarName).length() > 0) { final String needsToPlay = card.getSVar(needsToPlayVarName); - int x = 0; - int y = 0; String sVar = needsToPlay.split(" ")[0]; String comparator = needsToPlay.split(" ")[1]; String compareTo = comparator.substring(2); - try { - x = Integer.parseInt(sVar); - } catch (final NumberFormatException e) { - x = CardFactoryUtil.xCount(card, card.getSVar(sVar)); - } - try { - y = Integer.parseInt(compareTo); - } catch (final NumberFormatException e) { - y = CardFactoryUtil.xCount(card, card.getSVar(compareTo)); - } + int x = AbilityUtils.calculateAmount(card, sVar, sa); + int y = AbilityUtils.calculateAmount(card, compareTo, sa); + if (!Expressions.compare(x, comparator, y)) { return AiPlayDecision.NeedsToPlayCriteriaNotMet; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index da25cab609d..8e66d4a60d0 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -328,7 +328,7 @@ public class ComputerUtilCombat { public static int resultingPoison(final Player ai, final Combat combat) { // ai can't get poision counters, so the value can't change - if (!ai.canReceiveCounters(CounterType.POISON)) { + if (!ai.canReceiveCounters(CounterEnumType.POISON)) { return ai.getPoisonCounters(); } @@ -931,7 +931,7 @@ public class ComputerUtilCombat { if (dealsFirstStrikeDamage(attacker, withoutAbilities, null) && (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)) && !dealsFirstStrikeDamage(blocker, withoutAbilities, null) - && !blocker.canReceiveCounters(CounterType.M1M1)) { + && !blocker.canReceiveCounters(CounterEnumType.M1M1)) { power -= attacker.getNetCombatDamage(); } @@ -973,62 +973,45 @@ public class ComputerUtilCombat { } theTriggers.addAll(attacker.getTriggers()); for (final Trigger trigger : theTriggers) { - final Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) { continue; } - Map abilityParams = null; - if (trigger.getOverridingAbility() != null) { - abilityParams = trigger.getOverridingAbility().getMapParams(); - } else if (trigParams.containsKey("Execute")) { - final String ability = source.getSVar(trigParams.get("Execute")); - abilityParams = AbilityFactory.getMapParams(ability); - } else { + SpellAbility sa = trigger.ensureAbility(); + if (sa == null) { continue; } - if (abilityParams.containsKey("AB") && !abilityParams.get("AB").equals("Pump")) { + if (!ApiType.Pump.equals(sa.getApi())) { continue; } - if (abilityParams.containsKey("DB") && !abilityParams.get("DB").equals("Pump")) { + + if (sa.usesTargeting()) { continue; } - if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) { - continue; // targeted pumping not supported + + if (!sa.hasParam("NumAtt")) { + continue; } - final List list = AbilityUtils.getDefinedCards(source, abilityParams.get("Defined"), null); - if (abilityParams.containsKey("Defined") && abilityParams.get("Defined").equals("TriggeredBlocker")) { + + String defined = sa.getParam("Defined"); + final List list = AbilityUtils.getDefinedCards(source, defined, sa); + if ("TriggeredBlocker".equals(defined)) { list.add(blocker); } - if (list.isEmpty()) { - continue; - } if (!list.contains(blocker)) { continue; } - if (!abilityParams.containsKey("NumAtt")) { - continue; - } - String att = abilityParams.get("NumAtt"); - if (att.startsWith("+")) { - att = att.substring(1); - } - try { - power += Integer.parseInt(att); - } catch (final NumberFormatException nfe) { - // can't parse the number (X for example) - power += 0; - } + power += AbilityUtils.calculateAmount(source, sa.getParam("NumAtt"), sa, true); } if (withoutAbilities) { return power; } for (SpellAbility ability : blocker.getAllSpellAbilities()) { - if (!(ability instanceof AbilityActivated) || ability.getPayCosts() == null) { + if (!(ability instanceof AbilityActivated)) { continue; } if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) { @@ -1058,7 +1041,7 @@ public class ComputerUtilCombat { continue; } - if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) { + if (ability.hasParam("Adapt") && blocker.getCounters(CounterEnumType.P1P1) > 0) { continue; } @@ -1108,102 +1091,61 @@ public class ComputerUtilCombat { } theTriggers.addAll(attacker.getTriggers()); for (final Trigger trigger : theTriggers) { - final Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) { continue; } - Map abilityParams = null; - if (trigger.getOverridingAbility() != null) { - abilityParams = trigger.getOverridingAbility().getMapParams(); - } else if (trigParams.containsKey("Execute")) { - final String ability = source.getSVar(trigParams.get("Execute")); - abilityParams = AbilityFactory.getMapParams(ability); - } else { + SpellAbility sa = trigger.ensureAbility(); + if (sa == null) { continue; } - String abType = ""; - if (abilityParams.containsKey("AB")) { - abType = abilityParams.get("AB"); - } else if (abilityParams.containsKey("DB")) { - abType = abilityParams.get("DB"); - } - // DealDamage triggers - if (abType.equals("DealDamage")) { - if (!abilityParams.containsKey("Defined") || !abilityParams.get("Defined").equals("TriggeredBlocker")) { - continue; - } - int damage = 0; - try { - damage = Integer.parseInt(abilityParams.get("NumDmg")); - } catch (final NumberFormatException nfe) { - // can't parse the number (X for example) + if (ApiType.DealDamage.equals(sa.getApi())) { + if (!"TriggeredBlocker".equals(sa.getParam("Defined"))) { continue; } + int damage = AbilityUtils.calculateAmount(source, sa.getParam("NumDmg"), sa); toughness -= predictDamageTo(blocker, damage, 0, source, false); - continue; - } + } else // -1/-1 PutCounter triggers - if (abType.equals("PutCounter")) { - if (!abilityParams.containsKey("Defined") || !abilityParams.get("Defined").equals("TriggeredBlocker")) { + if (ApiType.PutCounter.equals(sa.getApi())) { + if (!"TriggeredBlocker".equals(sa.getParam("Defined"))) { continue; } - if (!abilityParams.containsKey("CounterType") || !abilityParams.get("CounterType").equals("M1M1")) { + if (!"M1M1".equals(sa.getParam("CounterType"))) { continue; } - int num = 0; - try { - num = Integer.parseInt(abilityParams.get("CounterNum")); - } catch (final NumberFormatException nfe) { - // can't parse the number (X for example) - continue; - } - toughness -= num; - continue; - } + toughness -= AbilityUtils.calculateAmount(source, sa.getParam("CounterNum"), sa); + } else // Pump triggers - if (!abType.equals("Pump")) { - continue; - } - if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) { - continue; // targeted pumping not supported - } - final List list = AbilityUtils.getDefinedCards(source, abilityParams.get("Defined"), null); - if (abilityParams.containsKey("Defined") && abilityParams.get("Defined").equals("TriggeredBlocker")) { - list.add(blocker); - } - if (list.isEmpty()) { - continue; - } - if (!list.contains(blocker)) { - continue; - } - if (!abilityParams.containsKey("NumDef")) { - continue; - } - - String def = abilityParams.get("NumDef"); - if (def.startsWith("+")) { - def = def.substring(1); - } - try { - toughness += Integer.parseInt(def); - } catch (final NumberFormatException nfe) { - // can't parse the number (X for example) + if (ApiType.Pump.equals(sa.getApi())) { + if (sa.usesTargeting()) { + continue; // targeted pumping not supported + } + final List list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), null); + if ("TriggeredBlocker".equals(sa.getParam("Defined"))) { + list.add(blocker); + } + if (list.isEmpty() || !list.contains(blocker)) { + continue; + } + if (!sa.hasParam("NumDef")) { + continue; + } + toughness += AbilityUtils.calculateAmount(source, sa.getParam("NumDef"), sa, true); } } if (withoutAbilities) { return toughness; } for (SpellAbility ability : blocker.getAllSpellAbilities()) { - if (!(ability instanceof AbilityActivated) || ability.getPayCosts() == null) { + if (!(ability instanceof AbilityActivated)) { continue; } @@ -1234,7 +1176,7 @@ public class ComputerUtilCombat { continue; } - if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) { + if (ability.hasParam("Adapt") && blocker.getCounters(CounterEnumType.P1P1) > 0) { continue; } @@ -1296,7 +1238,7 @@ public class ComputerUtilCombat { if (ComputerUtilCombat.dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)) && !ComputerUtilCombat.dealsFirstStrikeDamage(attacker, withoutAbilities, combat) - && !attacker.canReceiveCounters(CounterType.M1M1)) { + && !attacker.canReceiveCounters(CounterEnumType.M1M1)) { power -= blocker.getNetCombatDamage(); } theTriggers.addAll(blocker.getTriggers()); @@ -1426,7 +1368,7 @@ public class ComputerUtilCombat { return power; } for (SpellAbility ability : attacker.getAllSpellAbilities()) { - if (!(ability instanceof AbilityActivated) || ability.getPayCosts() == null) { + if (!(ability instanceof AbilityActivated)) { continue; } if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) { @@ -1456,7 +1398,7 @@ public class ComputerUtilCombat { continue; } - if (ability.hasParam("Adapt") && attacker.getCounters(CounterType.P1P1) > 0) { + if (ability.hasParam("Adapt") && attacker.getCounters(CounterEnumType.P1P1) > 0) { continue; } @@ -1521,148 +1463,135 @@ public class ComputerUtilCombat { final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield); for (final Card card : cardList) { for (final StaticAbility stAb : card.getStaticAbilities()) { - final Map params = stAb.getMapParams(); - if (!params.get("Mode").equals("Continuous")) { + if (!"Continuous".equals(stAb.getParam("Mode"))) { continue; } - if (params.containsKey("Affected") && params.get("Affected").contains("attacking")) { - final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature"); + if (!stAb.hasParam("Affected")) { + continue; + } + if (!stAb.hasParam("AddToughness")) { + continue; + } + String affected = stAb.getParam("Affected"); + String addT = stAb.getParam("AddToughness"); + if (affected.contains("attacking")) { + final String valid = TextUtil.fastReplace(affected, "attacking", "Creature"); if (!attacker.isValid(valid, card.getController(), card, null)) { continue; } - if (params.containsKey("AddToughness")) { - if (params.get("AddToughness").equals("X")) { - toughness += CardFactoryUtil.xCount(card, card.getSVar("X")); - } else if (params.get("AddToughness").equals("Y")) { - toughness += CardFactoryUtil.xCount(card, card.getSVar("Y")); - } else { - toughness += Integer.valueOf(params.get("AddToughness")); - } - } - } else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) { - final String valid = TextUtil.fastReplace(params.get("Affected"), "untapped", "Creature"); + toughness += AbilityUtils.calculateAmount(card, addT, stAb, true); + } else if (affected.contains("untapped")) { + final String valid = TextUtil.fastReplace(affected, "untapped", "Creature"); if (!attacker.isValid(valid, card.getController(), card, null) || attacker.hasKeyword(Keyword.VIGILANCE)) { continue; } // remove the bonus, because it will no longer be granted - if (params.containsKey("AddToughness")) { - toughness -= Integer.valueOf(params.get("AddToughness")); - } + toughness -= AbilityUtils.calculateAmount(card, addT, stAb, true); } } } } for (final Trigger trigger : theTriggers) { - final Map trigParams = trigger.getMapParams(); final Card source = trigger.getHostCard(); if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) { continue; } - Map abilityParams = null; - if (trigger.getOverridingAbility() != null) { - abilityParams = trigger.getOverridingAbility().getMapParams(); - } else if (trigParams.containsKey("Execute")) { - final String ability = source.getSVar(trigParams.get("Execute")); - abilityParams = AbilityFactory.getMapParams(ability); - } else { + SpellAbility sa = trigger.ensureAbility(); + if (sa == null) { continue; } + sa.setActivatingPlayer(source.getController()); - if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) { + if (sa.usesTargeting()) { continue; // targeted pumping not supported } // DealDamage triggers - if ((abilityParams.containsKey("AB") && abilityParams.get("AB").equals("DealDamage")) - || (abilityParams.containsKey("DB") && abilityParams.get("DB").equals("DealDamage"))) { - if (!abilityParams.containsKey("Defined") || !abilityParams.get("Defined").equals("TriggeredAttacker")) { - continue; - } - int damage = 0; - try { - damage = Integer.parseInt(abilityParams.get("NumDmg")); - } catch (final NumberFormatException nfe) { - // can't parse the number (X for example) + if (ApiType.DealDamage.equals(sa.getApi())) { + if ("TriggeredAttacker".equals(sa.getParam("Defined"))) { continue; } + int damage = AbilityUtils.calculateAmount(source, sa.getParam("NumDmg"), sa); + toughness -= predictDamageTo(attacker, damage, 0, source, false); continue; - } + } else if (ApiType.Pump.equals(sa.getApi())) { - // Pump triggers - if (abilityParams.containsKey("AB") && !abilityParams.get("AB").equals("Pump") - && !abilityParams.get("AB").equals("PumpAll")) { - continue; - } - if (abilityParams.containsKey("DB") && !abilityParams.get("DB").equals("Pump") - && !abilityParams.get("DB").equals("PumpAll")) { - continue; - } - - if (abilityParams.containsKey("Cost")) { - SpellAbility sa = null; - if (trigger.getOverridingAbility() != null) { - sa = trigger.getOverridingAbility(); - } else { - final String ability = source.getSVar(trigParams.get("Execute")); - sa = AbilityFactory.getAbility(ability, source); + if (sa.hasParam("Cost")) { + if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + continue; + } } - - sa.setActivatingPlayer(source.getController()); - if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + if (!sa.hasParam("NumDef")) { continue; } - } - - List list = Lists.newArrayList(); - if (!abilityParams.containsKey("ValidCards")) { - list = AbilityUtils.getDefinedCards(source, abilityParams.get("Defined"), null); - } - if (abilityParams.containsKey("Defined") && abilityParams.get("Defined").equals("TriggeredAttacker")) { - list.add(attacker); - } - if (abilityParams.containsKey("ValidCards")) { - if (attacker.isValid(abilityParams.get("ValidCards").split(","), source.getController(), source, null) - || attacker.isValid(abilityParams.get("ValidCards").replace("attacking+", "").split(","), - source.getController(), source, null)) { + CardCollection list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); + if ("TriggeredAttacker".equals(sa.getParam("Defined"))) { list.add(attacker); } - } - if (list.isEmpty()) { - continue; - } - if (!list.contains(attacker)) { - continue; - } - if (!abilityParams.containsKey("NumDef")) { - continue; - } - - String def = abilityParams.get("NumDef"); - if (def.startsWith("+")) { - def = def.substring(1); - } - if (def.matches("[0-9][0-9]?") || def.matches("-" + "[0-9][0-9]?")) { - toughness += Integer.parseInt(def); - } else { - String bonus = source.getSVar(def); - if (bonus.contains("TriggerCount$NumBlockers")) { - bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1"); - } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee - bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1"); + if (!list.contains(attacker)) { + continue; + } + + String def = sa.getParam("NumDef"); + if (def.startsWith("+")) { + def = def.substring(1); + } + if (def.matches("[0-9][0-9]?") || def.matches("-" + "[0-9][0-9]?")) { + toughness += Integer.parseInt(def); + } else { + String bonus = AbilityUtils.getSVar(sa, def); + if (bonus.contains("TriggerCount$NumBlockers")) { + bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1"); + } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee + bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1"); + } + toughness += CardFactoryUtil.xCount(source, bonus); + } + } else if (ApiType.PumpAll.equals(sa.getApi())) { + + if (sa.hasParam("Cost")) { + if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + continue; + } + } + + if (!sa.hasParam("ValidCards")) { + continue; + } + if (!sa.hasParam("NumDef")) { + continue; + } + if (!attacker.isValid(sa.getParam("ValidCards").replace("attacking+", "").split(","), source.getController(), source, sa)) { + continue; + } + + String def = sa.getParam("NumDef"); + if (def.startsWith("+")) { + def = def.substring(1); + } + if (def.matches("[0-9][0-9]?") || def.matches("-" + "[0-9][0-9]?")) { + toughness += Integer.parseInt(def); + } else { + String bonus = AbilityUtils.getSVar(sa, def); + if (bonus.contains("TriggerCount$NumBlockers")) { + bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1"); + } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee + bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1"); + } + toughness += CardFactoryUtil.xCount(source, bonus); } - toughness += CardFactoryUtil.xCount(source, bonus); } } if (withoutAbilities) { return toughness; } for (SpellAbility ability : attacker.getAllSpellAbilities()) { - if (!(ability instanceof AbilityActivated) || ability.getPayCosts() == null) { + if (!(ability instanceof AbilityActivated)) { continue; } @@ -1672,18 +1601,19 @@ public class ComputerUtilCombat { if (ability.usesTargeting() && !ability.canTarget(attacker)) { continue; } + if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) { + continue; + } + if (!ComputerUtilCost.canPayCost(ability, attacker.getController())) { + continue; + } if (ability.getApi() == ApiType.Pump) { if (!ability.hasParam("NumDef")) { continue; } - if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) { - int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability); - if (tBonus > 0) { - toughness += tBonus; - } - } + toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true); } else if (ability.getApi() == ApiType.PutCounter) { if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) { continue; @@ -1693,15 +1623,13 @@ public class ComputerUtilCombat { continue; } - if (ability.hasParam("Adapt") && attacker.getCounters(CounterType.P1P1) > 0) { + if (ability.hasParam("Adapt") && attacker.getCounters(CounterEnumType.P1P1) > 0) { continue; } - if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) { - int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability); - if (tBonus > 0) { - toughness += tBonus; - } + int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability); + if (tBonus > 0) { + toughness += tBonus; } } } @@ -1848,10 +1776,10 @@ public class ComputerUtilCombat { if (((attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker) && !withoutAbilities)) && !(blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT))) - || (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterType.M1M1) && (attacker - .getCounters(CounterType.M1M1) == 0)) - || (attacker.hasKeyword(Keyword.UNDYING) && !attacker.canReceiveCounters(CounterType.P1P1) && (attacker - .getCounters(CounterType.P1P1) == 0))) { + || (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterEnumType.M1M1) && (attacker + .getCounters(CounterEnumType.M1M1) == 0)) + || (attacker.hasKeyword(Keyword.UNDYING) && !attacker.canReceiveCounters(CounterEnumType.P1P1) && (attacker + .getCounters(CounterEnumType.P1P1) == 0))) { return false; } @@ -2080,10 +2008,10 @@ public class ComputerUtilCombat { if (((blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker .hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))) - || (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterType.M1M1) && (blocker - .getCounters(CounterType.M1M1) == 0)) - || (blocker.hasKeyword(Keyword.UNDYING) && !blocker.canReceiveCounters(CounterType.P1P1) && (blocker - .getCounters(CounterType.P1P1) == 0))) { + || (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterEnumType.M1M1) && (blocker + .getCounters(CounterEnumType.M1M1) == 0)) + || (blocker.hasKeyword(Keyword.UNDYING) && !blocker.canReceiveCounters(CounterEnumType.P1P1) && (blocker + .getCounters(CounterEnumType.P1P1) == 0))) { return false; } @@ -2517,7 +2445,7 @@ public class ComputerUtilCombat { final Player controller = combatant.getController(); for (Card c : controller.getCardsIn(ZoneType.Battlefield)) { for (SpellAbility ability : c.getAllSpellAbilities()) { - if (!(ability instanceof AbilityActivated) || ability.getPayCosts() == null) { + if (!(ability instanceof AbilityActivated)) { continue; } if (ability.getApi() != ApiType.Pump) { diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java index c94a5e7e16d..26fa41c1dd5 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java @@ -45,7 +45,7 @@ public class ComputerUtilCost { final CostPutCounter addCounter = (CostPutCounter) part; final CounterType type = addCounter.getCounter(); - if (type.equals(CounterType.M1M1)) { + if (type.equals(CounterEnumType.M1M1)) { return false; } } @@ -75,7 +75,7 @@ public class ComputerUtilCost { final CounterType type = remCounter.counter; if (!part.payCostFromSource()) { - if (CounterType.P1P1.equals(type)) { + if (CounterEnumType.P1P1.equals(type)) { return false; } continue; @@ -97,7 +97,7 @@ public class ComputerUtilCost { // check the sa what the PaymentDecision is. // ignore Loyality abilities with Zero as Cost - if (sa != null && !CounterType.LOYALTY.equals(type)) { + if (sa != null && !CounterEnumType.LOYALTY.equals(type)) { final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa); PaymentDecision pay = decision.visit(remCounter); if (pay == null || pay.c <= 0) { @@ -106,7 +106,7 @@ public class ComputerUtilCost { } //don't kill the creature - if (CounterType.P1P1.equals(type) && source.getLethalDamage() <= 1 + if (CounterEnumType.P1P1.equals(type) && source.getLethalDamage() <= 1 && !source.hasKeyword(Keyword.UNDYING)) { return false; } @@ -467,9 +467,9 @@ public class ComputerUtilCost { if(!meetsRestriction) continue; - try { + if (StringUtils.isNumeric(parts[0])) { extraManaNeeded += Integer.parseInt(parts[0]); - } catch (final NumberFormatException e) { + } else { System.out.println("wrong SpellsNeedExtraMana SVar format on " + c); } } @@ -480,9 +480,9 @@ public class ComputerUtilCost { } final String snem = c.getSVar("SpellsNeedExtraManaEffect"); if (!StringUtils.isBlank(snem)) { - try { + if (StringUtils.isNumeric(snem)) { extraManaNeeded += Integer.parseInt(snem); - } catch (final NumberFormatException e) { + } else { System.out.println("wrong SpellsNeedExtraManaEffect SVar format on " + c); } } @@ -529,7 +529,7 @@ public class ComputerUtilCost { public boolean apply(Card card) { boolean hasManaSa = false; for (final SpellAbility sa : card.getSpellAbilities()) { - if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.isManaAbility() && sa.getPayCosts().hasTapCost()) { hasManaSa = true; break; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 4102df2fefb..f90a554dbc6 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -371,7 +371,7 @@ public class ComputerUtilMana { adjustManaCostToAvoidNegEffects(cost, sa.getHostCard(), ai); List manaSpentToPay = test ? new ArrayList<>() : sa.getPayingMana(); boolean purePhyrexian = cost.containsOnlyPhyrexianMana(); - int testEnergyPool = ai.getCounters(CounterType.ENERGY); + int testEnergyPool = ai.getCounters(CounterEnumType.ENERGY); List paymentList = Lists.newArrayList(); @@ -507,16 +507,10 @@ public class ComputerUtilMana { } } else { - if (saPayment.getPayCosts() != null) { - final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment); - if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment))) { - saList.remove(saPayment); - continue; - } - } - else { - System.err.println("Ability " + saPayment + " from " + saPayment.getHostCard() + " had NULL as payCost"); - saPayment.getHostCard().tap(); + final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment); + if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment))) { + saList.remove(saPayment); + continue; } ai.getGame().getStack().addAndUnfreeze(saPayment); @@ -741,7 +735,7 @@ public class ComputerUtilMana { continue; } - if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsManaRestrictions(saBeingPaidFor)) { + if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsSpellAndShardRestrictions(saBeingPaidFor, shard, thisMana.getColor())) { continue; } @@ -837,10 +831,9 @@ public class ComputerUtilMana { if (checkCosts) { // Check if AI can still play this mana ability ma.setActivatingPlayer(ai); - if (ma.getPayCosts() != null) { // if the AI can't pay the additional costs skip the mana ability - if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) { - return false; - } + // if the AI can't pay the additional costs skip the mana ability + if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) { + return false; } else if (sourceCard.isTapped()) { return false; @@ -1144,7 +1137,7 @@ public class ComputerUtilMana { ManaCostBeingPaid cost = new ManaCostBeingPaid(mana, restriction); // Tack xMana Payments into mana here if X is a set value - if (sa.getPayCosts() != null && (cost.getXcounter() > 0 || extraMana > 0)) { + if (cost.getXcounter() > 0 || extraMana > 0) { int manaToAdd = 0; if (test && extraMana > 0) { final int multiplicator = Math.max(cost.getXcounter(), 1); @@ -1169,7 +1162,7 @@ public class ComputerUtilMana { cost.increaseShard(shardToGrow, manaToAdd); if (!test) { - card.setXManaCostPaid(manaToAdd / cost.getXcounter()); + sa.setXManaCostPaid(manaToAdd / cost.getXcounter()); } } @@ -1218,7 +1211,7 @@ public class ComputerUtilMana { for (SpellAbility ma : src.getManaAbilities()) { ma.setActivatingPlayer(p); if (!checkPlayable || ma.canPlay()) { - int costsToActivate = ma.getPayCosts() != null && ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0; + int costsToActivate = ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0; int producedMana = ma.getParamOrDefault("Produced", "").split(" ").length; int producedAmount = AbilityUtils.calculateAmount(src, ma.getParamOrDefault("Amount", "1"), ma); @@ -1594,7 +1587,7 @@ public class ComputerUtilMana { } public static int determineMaxAffordableX(Player ai, SpellAbility sa) { - if (sa.getPayCosts() == null || sa.getPayCosts().getCostMana() == null) { + if (sa.getPayCosts().getCostMana() == null) { return -1; } diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java index 3b7a9c7890a..917012b236d 100644 --- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java @@ -5,7 +5,7 @@ import com.google.common.base.Function; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.Card; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.cost.CostPayEnergy; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; @@ -242,11 +242,11 @@ public class CreatureEvaluator implements Function { && "+X".equals(sa.getParam("NumDef")) && !sa.usesTargeting() && (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) { - if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { + if (sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { // Electrostatic Pummeler, can be expanded for similar cards int initPower = getEffectivePower(sa.getHostCard()); int pumpedPower = initPower; - int energy = sa.getHostCard().getController().getCounters(CounterType.ENERGY); + int energy = sa.getHostCard().getController().getCounters(CounterEnumType.ENERGY); if (energy > 0) { int numActivations = energy / 3; for (int i = 0; i < numActivations; i++) { diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index 9d71da36c6a..600bbe41d7d 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -592,6 +592,7 @@ public abstract class GameState { cardToEnchantPlayerId.clear(); cardToRememberedId.clear(); cardToExiledWithId.clear(); + cardToImprintedId.clear(); markedDamage.clear(); cardToChosenClrs.clear(); cardToChosenCards.clear(); @@ -981,14 +982,27 @@ public abstract class GameState { spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim(); } - PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef); + Card c = null; - if (pc == null) { - System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!"); - return; + if (StringUtils.isNumeric(spellDef)) { + // Precast from a specific host + c = idToCard.get(Integer.parseInt(spellDef)); + if (c == null) { + System.err.println("ERROR: Could not find a card with ID " + spellDef + " to precast!"); + return; + } + } else { + // Precast from a card by name + PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef); + + if (pc == null) { + System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!"); + return; + } + + c = Card.fromPaperCard(pc, activator); } - Card c = Card.fromPaperCard(pc, activator); SpellAbility sa = null; if (!scriptID.isEmpty()) { @@ -1077,11 +1091,11 @@ public abstract class GameState { } private void applyCountersToGameEntity(GameEntity entity, String counterString) { - entity.setCounters(Maps.newEnumMap(CounterType.class)); + entity.setCounters(Maps.newHashMap()); String[] allCounterStrings = counterString.split(","); for (final String counterPair : allCounterStrings) { String[] pair = counterPair.split("=", 2); - entity.addCounter(CounterType.valueOf(pair[0]), Integer.parseInt(pair[1]), null, false, false, null); + entity.addCounter(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, false, false, null); } } @@ -1123,7 +1137,7 @@ public abstract class GameState { Map counters = c.getCounters(); // Note: Not clearCounters() since we want to keep the counters // var as-is. - c.setCounters(Maps.newEnumMap(CounterType.class)); + c.setCounters(Maps.newHashMap()); if (c.isAura()) { // dummy "enchanting" to indicate that the card will be force-attached elsewhere // (will be overridden later, so the actual value shouldn't matter) diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 04a9f303832..1f4e66b3198 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -145,12 +145,12 @@ public class PlayerControllerAi extends PlayerController { } @Override - public CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional) { - return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional); + public CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional, Map params) { + return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional, params); } @Override - public T chooseSingleEntityForEffect(FCollectionView optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player targetedPlayer) { + public T chooseSingleEntityForEffect(FCollectionView optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player targetedPlayer, Map params) { if (delayedReveal != null) { reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix()); } @@ -158,13 +158,13 @@ public class PlayerControllerAi extends PlayerController { if (null == api) { throw new InvalidParameterException("SA is not api-based, this is not supported yet"); } - return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection)optionList, isOptional, targetedPlayer); + return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection)optionList, isOptional, targetedPlayer, params); } @Override public List chooseEntitiesForEffect( FCollectionView optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title, - Player targetedPlayer) { + Player targetedPlayer, Map params) { if (delayedReveal != null) { reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix()); } @@ -172,7 +172,7 @@ public class PlayerControllerAi extends PlayerController { List selecteds = new ArrayList<>(); T selected; do { - selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer); + selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer, params); if ( selected != null ) { remaining.remove(selected); selecteds.add(selected); @@ -182,7 +182,23 @@ public class PlayerControllerAi extends PlayerController { } @Override - public SpellAbility chooseSingleSpellForEffect(java.util.List spells, SpellAbility sa, String title, + public List chooseSpellAbilitiesForEffect(List spells, SpellAbility sa, String title, + int num, Map params) { + List remaining = Lists.newArrayList(spells); + List selecteds = Lists.newArrayList(); + SpellAbility selected; + do { + selected = chooseSingleSpellForEffect(remaining, sa, title, params); + if ( selected != null ) { + remaining.remove(selected); + selecteds.add(selected); + } + } while ( (selected != null ) && (selecteds.size() < num) ); + return selecteds; + } + + @Override + public SpellAbility chooseSingleSpellForEffect(List spells, SpellAbility sa, String title, Map params) { ApiType api = sa.getApi(); if (null == api) { @@ -208,15 +224,13 @@ public class PlayerControllerAi extends PlayerController { } @Override - public boolean confirmTrigger(WrappedAbility wrapper, Map triggerParams, boolean isMandatory) { + public boolean confirmTrigger(WrappedAbility wrapper) { final SpellAbility sa = wrapper.getWrappedAbility(); //final Trigger regtrig = wrapper.getTrigger(); if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) { return true; } - if (triggerParams.containsKey("DelayedTrigger") || isMandatory) { - //TODO: The only card with an optional delayed trigger is Shirei, Shizo's Caretaker, - // needs to be expanded when a more difficult cards comes up + if (wrapper.isMandatory()) { return true; } // Store/replace target choices more properly to get this SA cleared. @@ -503,7 +517,7 @@ public class PlayerControllerAi extends PlayerController { @Override public String chooseSomeType(String kindOfType, SpellAbility sa, Collection validTypes, List invalidTypes, boolean isOptional) { - String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), invalidTypes); + String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), validTypes, invalidTypes); if (StringUtils.isBlank(chosen) && !validTypes.isEmpty()) { chosen = validTypes.iterator().next(); System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to arbitrary element: chosen"); @@ -513,8 +527,8 @@ public class PlayerControllerAi extends PlayerController { } @Override - public Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes) { - return ComputerUtil.vote(player, options, sa, votes); + public Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes, Player forPlayer) { + return ComputerUtil.vote(player, options, sa, votes, forPlayer); } @Override @@ -603,6 +617,7 @@ public class PlayerControllerAi extends PlayerController { if (sa instanceof LandAbility) { if (sa.canPlay()) { sa.resolve(); + game.updateLastStateForCard(sa.getHostCard()); } } else { ComputerUtil.handlePlayingSpellAbility(player, sa, game); @@ -754,6 +769,7 @@ public class PlayerControllerAi extends PlayerController { return defaultVal != null && defaultVal.booleanValue(); case UntapTimeVault: return false; // TODO Should AI skip his turn for time vault? case LeftOrRight: return brains.chooseDirection(sa); + case OddsOrEvens: return brains.chooseEvenOdd(sa); // false is Odd, true is Even default: return MyRandom.getRandom().nextBoolean(); } @@ -1122,7 +1138,8 @@ public class PlayerControllerAi extends PlayerController { CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Creature", player, sa.getHostCard()); return ComputerUtilCard.getMostProminentCardName(cards); } else if (logic.equals("BestCreatureInComputerDeck")) { - return ComputerUtilCard.getBestCreatureAI(aiLibrary).getName(); + Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiLibrary); + return bestCreature != null ? bestCreature.getName() : "Plains"; } else if (logic.equals("RandomInComputerDeck")) { return Aggregates.random(aiLibrary).getName(); } else if (logic.equals("MostProminentSpellInComputerDeck")) { @@ -1213,7 +1230,7 @@ public class PlayerControllerAi extends PlayerController { public List chooseOptionalCosts(SpellAbility chosen, List optionalCostValues) { List chosenOptCosts = Lists.newArrayList(); - Cost costSoFar = chosen.getPayCosts() != null ? chosen.getPayCosts().copy() : Cost.Zero; + Cost costSoFar = chosen.getPayCosts().copy(); for (OptionalCostValue opt : optionalCostValues) { // Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps) @@ -1252,7 +1269,7 @@ public class PlayerControllerAi extends PlayerController { // TODO: improve the logic depending on the keyword and the playability of the cost-modified SA (enough targets present etc.) int chosenAmount = 0; - Cost costSoFar = sa.getPayCosts() != null ? sa.getPayCosts().copy() : Cost.Zero; + Cost costSoFar = sa.getPayCosts().copy(); for (int i = 0; i < max; i++) { costSoFar.add(cost); @@ -1268,13 +1285,16 @@ public class PlayerControllerAi extends PlayerController { } @Override - public CardCollection chooseCardsForEffectMultiple(Map validMap, SpellAbility sa, String title) { + public CardCollection chooseCardsForEffectMultiple(Map validMap, SpellAbility sa, String title, boolean isOptional) { CardCollection choices = new CardCollection(); for (String mapKey: validMap.keySet()) { CardCollection cc = validMap.get(mapKey); cc.removeAll(choices); - choices.add(ComputerUtilCard.getBestAI(cc)); // TODO: should the AI limit itself here with the max number of cards in hand? + Card chosen = ComputerUtilCard.getBestAI(cc); + if (chosen != null) { + choices.add(chosen); + } } return choices; diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index 27700e2455b..5ed62460108 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -327,7 +327,7 @@ public class SpecialCardAi { boolean canTrample = source.hasKeyword(Keyword.TRAMPLE); if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) { - int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterType.LOYALTY); + int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterEnumType.LOYALTY); int totalDamageToPW = 0; for (Card atk : (combat.getAttackersOf(combat.getDefenderByAttacker(source)))) { if (combat.isUnblocked(atk)) { @@ -407,7 +407,7 @@ public class SpecialCardAi { } public static Pair getPumpedPT(Player ai, int power, int toughness) { - int energy = ai.getCounters(CounterType.ENERGY); + int energy = ai.getCounters(CounterEnumType.ENERGY); if (energy > 0) { int numActivations = energy / 3; for (int i = 0; i < numActivations; i++) { @@ -708,7 +708,7 @@ public class SpecialCardAi { // if there's another reanimator card currently suspended, don't cast a new one until the previous // one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End) for (Card ex : ai.getCardsIn(ZoneType.Exile)) { - if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterType.TIME) > 0) { + if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) { return false; } } @@ -767,7 +767,7 @@ public class SpecialCardAi { Player controller = c.getController(); boolean wasCaged = false; for (Card caged : CardLists.filter(controller.getCardsIn(ZoneType.Exile), - CardPredicates.hasCounter(CounterType.CAGE))) { + CardPredicates.hasCounter(CounterEnumType.CAGE))) { if (c.getName().equals(caged.getName())) { wasCaged = true; break; @@ -1073,7 +1073,7 @@ public class SpecialCardAi { // Sarkhan the Mad public static class SarkhanTheMad { public static boolean considerDig(final Player ai, final SpellAbility sa) { - return sa.getHostCard().getCounters(CounterType.LOYALTY) == 1; + return sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1; } public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) { @@ -1109,7 +1109,7 @@ public class SpecialCardAi { // Sorin, Vengeful Bloodlord public static class SorinVengefulBloodlord { public static boolean consider(final Player ai, final SpellAbility sa) { - int loyalty = sa.getHostCard().getCounters(CounterType.LOYALTY); + int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY); CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.lessCMC(loyalty - 1), new Predicate() { @Override @@ -1295,6 +1295,26 @@ public class SpecialCardAi { } } + // Timmerian Fiends + public static class TimmerianFiends { + public static boolean consider(final Player ai, final SpellAbility sa) { + final Card targeted = sa.getParentTargetingCard().getTargetCard(); + if (targeted == null) { + return false; + } + + if (targeted.isCreature()) { + if (ComputerUtil.aiLifeInDanger(ai, true, 0)) { + return true; // do it, hoping to save a valuable potential blocker etc. + } + return ComputerUtilCard.evaluateCreature(targeted) >= 200; // might need tweaking + } else { + // TODO: this currently compares purely by CMC. To be somehow improved, especially for stuff like the Power Nine etc. + return ComputerUtilCard.evaluatePermanentList(new CardCollection(targeted)) >= 3; + } + } + } + // Volrath's Shapeshifter public static class VolrathsShapeshifter { public static boolean consider(final Player ai, final SpellAbility sa) { @@ -1345,7 +1365,7 @@ public class SpecialCardAi { Card source = sa.getHostCard(); Game game = source.getGame(); - final int loyalty = source.getCounters(CounterType.LOYALTY); + final int loyalty = source.getCounters(CounterEnumType.LOYALTY); int x = -1, best = 0; Card single = null; for (int i = 0; i < loyalty; i++) { diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index d86989d87a8..665e8881f4f 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -2,6 +2,7 @@ package forge.ai; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import forge.card.CardStateName; import forge.card.ICardFace; import forge.card.mana.ManaCost; import forge.card.mana.ManaCostParser; @@ -167,7 +168,8 @@ public abstract class SpellAbilityAi { // a mandatory SpellAbility with targeting but without candidates, // does not need to go any deeper - if (sa.usesTargeting() && mandatory && !sa.getTargetRestrictions().hasCandidates(sa, true)) { + if (sa.usesTargeting() && mandatory && !sa.isTargetNumberValid() + && !sa.getTargetRestrictions().hasCandidates(sa, true)) { return false; } @@ -247,6 +249,7 @@ public abstract class SpellAbilityAi { protected static boolean isSorcerySpeed(final SpellAbility sa) { return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery()) || (sa.getRootAbility().isAbility() && sa.getRestrictions().isSorcerySpeed()) + || (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery()) || (sa.isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed.")); } @@ -264,7 +267,7 @@ public abstract class SpellAbilityAi { // TODO probably also consider if winter orb or similar are out - if (sa.getPayCosts() == null || sa instanceof AbilitySub) { + if (sa instanceof AbilitySub) { return true; // This is only true for Drawbacks and triggers } @@ -304,7 +307,7 @@ public abstract class SpellAbilityAi { } @SuppressWarnings("unchecked") - public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer) { + public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer, Map params) { boolean hasPlayer = false; boolean hasCard = false; boolean hasPlaneswalker = false; @@ -321,11 +324,11 @@ public abstract class SpellAbilityAi { } if (hasPlayer && hasPlaneswalker) { - return (T) chooseSinglePlayerOrPlaneswalker(ai, sa, (Collection) options); + return (T) chooseSinglePlayerOrPlaneswalker(ai, sa, (Collection) options, params); } else if (hasCard) { - return (T) chooseSingleCard(ai, sa, (Collection) options, isOptional, targetedPlayer); + return (T) chooseSingleCard(ai, sa, (Collection) options, isOptional, targetedPlayer, params); } else if (hasPlayer) { - return (T) chooseSinglePlayer(ai, sa, (Collection) options); + return (T) chooseSinglePlayer(ai, sa, (Collection) options, params); } return null; @@ -336,17 +339,17 @@ public abstract class SpellAbilityAi { return spells.get(0); } - protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSingleCard is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method"); return Iterables.getFirst(options, null); } - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSinglePlayer is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method"); return Iterables.getFirst(options, null); } - protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable options) { + protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable options, Map params) { System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSinglePlayerOrPlaneswalker is used for " + this.getClass().getName() + ". Consider declaring an overloaded method"); return Iterables.getFirst(options, null); } diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index f3b3421c40b..460e0390e29 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -33,7 +33,7 @@ public enum SpellApiToAi { .put(ApiType.BidLife, BidLifeAi.class) .put(ApiType.Bond, BondAi.class) .put(ApiType.Branch, AlwaysPlayAi.class) - .put(ApiType.ChangeCombatants, CannotPlayAi.class) + .put(ApiType.ChangeCombatants, ChangeCombatantsAi.class) .put(ApiType.ChangeTargets, ChangeTargetsAi.class) .put(ApiType.ChangeX, AlwaysPlayAi.class) .put(ApiType.ChangeZone, ChangeZoneAi.class) @@ -42,6 +42,7 @@ public enum SpellApiToAi { .put(ApiType.ChooseCard, ChooseCardAi.class) .put(ApiType.ChooseColor, ChooseColorAi.class) .put(ApiType.ChooseDirection, ChooseDirectionAi.class) + .put(ApiType.ChooseEvenOdd, ChooseEvenOddAi.class) .put(ApiType.ChooseNumber, ChooseNumberAi.class) .put(ApiType.ChoosePlayer, ChoosePlayerAi.class) .put(ApiType.ChooseSource, ChooseSourceAi.class) @@ -83,7 +84,7 @@ public enum SpellApiToAi { .put(ApiType.FlipACoin, FlipACoinAi.class) .put(ApiType.Fog, FogAi.class) .put(ApiType.GainControl, ControlGainAi.class) - .put(ApiType.GainControlVariant, AlwaysPlayAi.class) + .put(ApiType.GainControlVariant, ControlGainVariantAi.class) .put(ApiType.GainLife, LifeGainAi.class) .put(ApiType.GainOwnership, CannotPlayAi.class) .put(ApiType.GameDrawn, CannotPlayAi.class) @@ -91,6 +92,7 @@ public enum SpellApiToAi { .put(ApiType.Goad, GoadAi.class) .put(ApiType.Haunt, HauntAi.class) .put(ApiType.ImmediateTrigger, AlwaysPlayAi.class) + .put(ApiType.Investigate, InvestigateAi.class) .put(ApiType.LoseLife, LifeLoseAi.class) .put(ApiType.LosesGame, GameLossAi.class) .put(ApiType.Mana, ManaEffectAi.class) diff --git a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java index 7699259abd1..ab54f83224d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.collect.Iterables; import com.google.common.collect.Sets; @@ -23,12 +25,12 @@ public class AmassAi extends SpellAbilityAi { final Game game = ai.getGame(); if (!aiArmies.isEmpty()) { - return CardLists.count(aiArmies, CardPredicates.canReceiveCounters(CounterType.P1P1)) > 0; + return CardLists.count(aiArmies, CardPredicates.canReceiveCounters(CounterEnumType.P1P1)) > 0; } else { final String tokenScript = "b_0_0_zombie_army"; final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa); - Card token = TokenInfo.getProtoType(tokenScript, sa); + Card token = TokenInfo.getProtoType(tokenScript, sa, false); if (token == null) { return false; @@ -44,8 +46,8 @@ public class AmassAi extends SpellAbilityAi { CardCollection preList = new CardCollection(token); game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList); - if (token.canReceiveCounters(CounterType.P1P1)) { - token.setCounters(CounterType.P1P1, amount); + if (token.canReceiveCounters(CounterEnumType.P1P1)) { + token.setCounters(CounterEnumType.P1P1, amount); } if (token.isCreature() && token.getNetToughness() < 1) { @@ -86,8 +88,8 @@ public class AmassAi extends SpellAbilityAi { } @Override - protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { - Iterable better = CardLists.filter(options, CardPredicates.canReceiveCounters(CounterType.P1P1)); + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { + Iterable better = CardLists.filter(options, CardPredicates.canReceiveCounters(CounterEnumType.P1P1)); if (Iterables.isEmpty(better)) { better = options; } diff --git a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java index baa0d00b7c5..b33948b293b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java @@ -247,7 +247,6 @@ public class AnimateAi extends SpellAbilityAi { final Player ai = sa.getActivatingPlayer(); final PhaseHandler ph = ai.getGame().getPhaseHandler(); final boolean alwaysActivatePWAbility = sa.hasParam("Planeswalker") - && sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPutCounter.class) && sa.getTargetRestrictions() != null && sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa) == 0; 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 23648c94401..37602483b41 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -398,7 +398,7 @@ public class AttachAi extends SpellAbilityAi { if (!c.isCreature() && !c.getType().hasSubtype("Vehicle") && !c.isTapped()) { // try to identify if this thing can actually tap for (SpellAbility ab : c.getAllSpellAbilities()) { - if (ab.getPayCosts() != null && ab.getPayCosts().hasTapCost()) { + if (ab.getPayCosts().hasTapCost()) { return true; } } @@ -560,7 +560,7 @@ public class AttachAi extends SpellAbilityAi { @Override public boolean apply(final Card c) { for (final SpellAbility sa : c.getSpellAbilities()) { - if (sa.isAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.isAbility() && sa.getPayCosts().hasTapCost()) { return false; } } @@ -1704,12 +1704,12 @@ public class AttachAi extends SpellAbilityAi { } @Override - protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { return attachToCardAIPreferences(ai, sa, true); } @Override - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { return attachToPlayerAIPreferences(ai, sa, true); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/BondAi.java b/forge-ai/src/main/java/forge/ai/ability/BondAi.java index 03c950d3f2e..8619983df69 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BondAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BondAi.java @@ -17,6 +17,8 @@ */ package forge.ai.ability; +import java.util.Map; + import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -50,7 +52,7 @@ public final class BondAi extends SpellAbilityAi { @Override - protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { return ComputerUtilCard.getBestCreatureAI(options); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeCombatantsAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeCombatantsAi.java new file mode 100644 index 00000000000..1187dc81cdc --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeCombatantsAi.java @@ -0,0 +1,66 @@ +package forge.ai.ability; + +import forge.ai.SpellAbilityAi; +import forge.game.GameEntity; +import forge.game.player.Player; +import forge.game.player.PlayerCollection; +import forge.game.player.PlayerPredicates; +import forge.game.spellability.SpellAbility; + +import java.util.Collection; +import java.util.Map; + +public class ChangeCombatantsAi extends SpellAbilityAi { + /* (non-Javadoc) + * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) + */ + @Override + protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + // TODO: Extend this if possible for cards that have this as an activated ability + return false; + } + + @Override + protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + return mandatory || canPlayAI(aiPlayer, sa); + } + + /* (non-Javadoc) + * @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player) + */ + @Override + public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + final String logic = sa.getParamOrDefault("AILogic", ""); + + if (logic.equals("WeakestOppExceptCtrl")) { + PlayerCollection targetableOpps = aiPlayer.getOpponents(); + targetableOpps.remove(sa.getHostCard().getController()); + if (targetableOpps.isEmpty()) { + return false; + } + + return true; + } + + return false; + } + + @Override + public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer, Map params) { + PlayerCollection targetableOpps = new PlayerCollection(); + for (GameEntity p : options) { + if (p instanceof Player && !p.equals(sa.getHostCard().getController())) { + Player pp = (Player)p; + if (pp.isOpponentOf(ai)) { + targetableOpps.add(pp); + } + } + } + + Player weakestTargetableOpp = targetableOpps.filter(PlayerPredicates.isTargetableBy(sa)) + .min(PlayerPredicates.compareByLife()); + + return (T)weakestTargetableOpp; + } +} + diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java index 40c59393086..2894660612a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -142,7 +142,7 @@ public class ChangeZoneAi extends SpellAbilityAi { *

* @param sa * a {@link forge.game.spellability.SpellAbility} object. - * + * * @return a boolean. */ @Override @@ -170,7 +170,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * a {@link forge.game.spellability.SpellAbility} object. * @param mandatory * a boolean. - * + * * @return a boolean. */ @Override @@ -370,10 +370,10 @@ public class ChangeZoneAi extends SpellAbilityAi { if (!activateForCost && list.isEmpty()) { return false; } - if ("Atarka's Command".equals(sourceName) - && (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) { - // be strict on playing lands off charms - return false; + if ("Atarka's Command".equals(sourceName) + && (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) { + // be strict on playing lands off charms + return false; } String num = sa.getParam("ChangeNum"); @@ -385,7 +385,7 @@ public class ChangeZoneAi extends SpellAbilityAi { source.setSVar("PayX", Integer.toString(xPay)); } } - + if (sourceName.equals("Temur Sabertooth")) { // activated bounce + pump if (ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible")) || @@ -400,9 +400,9 @@ public class ChangeZoneAi extends SpellAbilityAi { } } - + if (ComputerUtil.playImmediately(ai, sa)) { - return true; + return true; } // don't use fetching to top of library/graveyard before main2 @@ -418,9 +418,9 @@ public class ChangeZoneAi extends SpellAbilityAi { } if (ComputerUtil.waitForBlocking(sa)) { - return false; + return false; } - + final AbilitySub subAb = sa.getSubAbility(); return subAb == null || SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(ai, subAb); } @@ -551,7 +551,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * basicManaFixing. *

* @param ai - * + * * @param list * a List object. * @return a {@link forge.game.card.Card} object. @@ -584,7 +584,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (minType != null) { result = CardLists.getType(list, minType); } - + // pick dual lands if available if (Iterables.any(result, Predicates.not(CardPredicates.Presets.BASIC_LANDS))) { result = CardLists.filter(result, Predicates.not(CardPredicates.Presets.BASIC_LANDS)); @@ -597,7 +597,7 @@ public class ChangeZoneAi extends SpellAbilityAi { *

* areAllBasics. *

- * + * * @param types * a {@link java.lang.String} object. * @return a boolean. @@ -617,8 +617,8 @@ public class ChangeZoneAi extends SpellAbilityAi { * @return Card */ private static Card chooseCreature(final Player ai, CardCollection list) { - // Creating a new combat for testing purposes. - final Player opponent = ai.getWeakestOpponent(); + // Creating a new combat for testing purposes. + final Player opponent = ai.getWeakestOpponent(); Combat combat = new Combat(opponent); for (Card att : opponent.getCreaturesInPlay()) { combat.addAttacker(att, ai); @@ -742,7 +742,7 @@ public class ChangeZoneAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see * forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player, * forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler) @@ -781,7 +781,7 @@ public class ChangeZoneAi extends SpellAbilityAi { return false; } } - + //don't unearth after attacking is possible if (sa.hasParam("Unearth") && ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) { return false; @@ -895,7 +895,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) { return false; } - + immediately |= ComputerUtil.playImmediately(ai, sa); // Narrow down the list: @@ -926,7 +926,7 @@ public class ChangeZoneAi extends SpellAbilityAi { CardCollection blockers = currCombat.getBlockers(attacker); // Save my attacker by bouncing a blocker if (attacker.getController().equals(ai) && attacker.getShieldCount() == 0 - && ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, currCombat) + && ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, currCombat) && !currCombat.getBlockers(attacker).isEmpty()) { ComputerUtilCard.sortByEvaluateCreature(blockers); Combat combat = new Combat(ai); @@ -970,9 +970,9 @@ public class ChangeZoneAi extends SpellAbilityAi { sa.getTargets().add(tobounce); - boolean saheeliFelidarCombo = sa.getHostCard().getName().equals("Felidar Guardian") + boolean saheeliFelidarCombo = sa.getHostCard().getName().equals("Felidar Guardian") && tobounce.getName().equals("Saheeli Rai") - && CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Felidar Guardian")).size() < + && CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Felidar Guardian")).size() < CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Creature")).size() + ai.getOpponentsGreatestLifeTotal() + 10; // remember that the card was bounced already unless it's a special combo case @@ -985,20 +985,20 @@ public class ChangeZoneAi extends SpellAbilityAi { // bounce opponent's stuff list = CardLists.filterControlledBy(list, ai.getOpponents()); if (!CardLists.getNotType(list, "Land").isEmpty()) { - // When bouncing opponents stuff other than lands, don't bounce cards with CMC 0 - list = CardLists.filter(list, new Predicate() { - @Override - public boolean apply(final Card c) { - for (Card aura : c.getEnchantedBy()) { + // When bouncing opponents stuff other than lands, don't bounce cards with CMC 0 + list = CardLists.filter(list, new Predicate() { + @Override + public boolean apply(final Card c) { + for (Card aura : c.getEnchantedBy()) { return aura.getController().isOpponentOf(ai); - } - if (blink) { - return c.isToken(); - } else { - return c.isToken() || c.getCMC() > 0; - } - } - }); + } + if (blink) { + return c.isToken(); + } else { + return c.isToken() || c.getCMC() > 0; + } + } + }); } // TODO: Blink permanents with ETB triggers /*else if (!sa.isTrigger() && SpellAbilityAi.playReusable(ai, sa)) { @@ -1023,7 +1023,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } } else if (origin.contains(ZoneType.Graveyard)) { - if (destination.equals(ZoneType.Exile) || destination.equals(ZoneType.Library)) { + if (destination.equals(ZoneType.Exile) || destination.equals(ZoneType.Library)) { // Don't use these abilities before main 2 if possible if (!immediately && game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") && !ComputerUtil.castSpellInMain1(ai, sa)) { @@ -1035,7 +1035,7 @@ public class ChangeZoneAi extends SpellAbilityAi { && !ComputerUtil.activateForCost(sa, ai)) { return false; } - } else if (destination.equals(ZoneType.Hand)) { + } else if (destination.equals(ZoneType.Hand)) { // only retrieve cards from computer graveyard list = CardLists.filterControlledBy(list, ai); } else if (sa.hasParam("AttachedTo")) { @@ -1065,8 +1065,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (destination.equals(ZoneType.Exile) || origin.contains(ZoneType.Battlefield)) { // don't rush bouncing stuff when not going to attack - if (!immediately && sa.getPayCosts() != null - && game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) + if (!immediately && game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && game.getPhaseHandler().isPlayerTurn(ai) && ai.getCreaturesInPlay().isEmpty()) { return false; @@ -1097,14 +1096,14 @@ public class ChangeZoneAi extends SpellAbilityAi { // Only care about combatants during combat if (game.getPhaseHandler().inCombat() && origin.contains(ZoneType.Battlefield)) { - CardCollection newList = CardLists.getValidCards(list, "Card.attacking,Card.blocking", null, null); - if (!newList.isEmpty() || !sa.isTrigger()) { - list = newList; - } + CardCollection newList = CardLists.getValidCards(list, "Card.attacking,Card.blocking", null, null); + if (!newList.isEmpty() || !sa.isTrigger()) { + list = newList; + } } - boolean doWithoutTarget = sa.hasParam("Planeswalker") && sa.getTargetRestrictions() != null - && sa.getTargetRestrictions().getMinTargets(source, sa) == 0 && sa.getPayCosts() != null + boolean doWithoutTarget = sa.hasParam("Planeswalker") && sa.usesTargeting() + && sa.getTargetRestrictions().getMinTargets(source, sa) == 0 && sa.getPayCosts().hasSpecificCostType(CostPutCounter.class); if (list.isEmpty() && !doWithoutTarget) { @@ -1252,7 +1251,7 @@ public class ChangeZoneAi extends SpellAbilityAi { return true; } - + /** * Checks if a permanent threatened by a stack ability or in combat can * be saved by bouncing. @@ -1325,11 +1324,11 @@ public class ChangeZoneAi extends SpellAbilityAi { Collections.sort(aiPlaneswalkers, new Comparator() { @Override public int compare(final Card a, final Card b) { - return a.getCounters(CounterType.LOYALTY) - b.getCounters(CounterType.LOYALTY); + return a.getCounters(CounterEnumType.LOYALTY) - b.getCounters(CounterEnumType.LOYALTY); } }); for (Card pw : aiPlaneswalkers) { - int curLoyalty = pw.getCounters(CounterType.LOYALTY); + int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY); int freshLoyalty = Integer.valueOf(pw.getCurrentState().getBaseLoyalty()); if (freshLoyalty - curLoyalty >= loyaltyDiff && curLoyalty <= maxLoyaltyToConsider) { return pw; @@ -1507,10 +1506,10 @@ public class ChangeZoneAi extends SpellAbilityAi { if (type == null) { type = "Card"; } - + Card c = null; final Player activator = sa.getActivatingPlayer(); - + CardLists.shuffle(fetchList); // Save a card as a default, in case we can't find anything suitable. Card first = fetchList.get(0); @@ -1615,19 +1614,19 @@ public class ChangeZoneAi extends SpellAbilityAi { // AI was never asked return true; } - + @Override - public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { // Called when looking for creature to attach aura or equipment return ComputerUtilCard.getBestAI(options); } - + /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#chooseSinglePlayer(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List) */ @Override - public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { // Currently only used by Curse of Misfortunes, so this branch should never get hit // But just in case it does, just select the first option return Iterables.getFirst(options, null); @@ -1790,6 +1789,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } public boolean doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) { + @SuppressWarnings("unchecked") Map originalParams = (Map)sa.getReplacingObject(AbilityKey.OriginalParams); SpellAbility causeSa = (SpellAbility)originalParams.get(AbilityKey.Cause); SpellAbility causeSub = null; @@ -1801,7 +1801,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) { ApiType subApi = causeSub.getApi(); - + if (subApi == ApiType.ChangeZone && "Exile".equals(causeSub.getParam("Origin")) && "Battlefield".equals(causeSub.getParam("Destination"))) { // A blink effect implemented using ChangeZone API @@ -1817,7 +1817,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } else return causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card)) || !causeSa.getActivatingPlayer().equals(aiPlayer); } - + // Normally we want the commander back in Command zone to recast him later return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java index 1f9343599df..296a695b871 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java @@ -11,6 +11,7 @@ import forge.util.MyRandom; import forge.util.collect.FCollection; import java.util.List; +import java.util.Map; public class CharmAi extends SpellAbilityAi { @Override @@ -232,7 +233,7 @@ public class CharmAi extends SpellAbilityAi { } @Override - public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable opponents) { + public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable opponents, Map params) { return Aggregates.random(opponents); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java index 1beefd77b09..f8a48d2ec25 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.Collections; import java.util.List; +import java.util.Map; import com.google.common.base.Predicate; import com.google.common.base.Predicates; @@ -20,7 +21,7 @@ import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.card.CardPredicates.Presets; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.combat.Combat; import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; @@ -99,7 +100,7 @@ public class ChooseCardAi extends SpellAbilityAi { }); return !choices.isEmpty(); } else if (aiLogic.equals("Ashiok")) { - final int loyalty = host.getCounters(CounterType.LOYALTY) - 1; + final int loyalty = host.getCounters(CounterEnumType.LOYALTY) - 1; for (int i = loyalty; i >= 0; i--) { host.setSVar("ChosenX", "Number$" + i); choices = ai.getGame().getCardsIn(choiceZone); @@ -140,12 +141,12 @@ public class ChooseCardAi extends SpellAbilityAi { } return checkApiLogic(ai, sa); } - + /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean) */ @Override - public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { final Card host = sa.getHostCard(); final Player ctrl = host.getController(); final String logic = sa.getParam("AILogic"); @@ -191,7 +192,7 @@ public class ChooseCardAi extends SpellAbilityAi { if (combat == null || !combat.isAttacking(c, ai) || !combat.isUnblocked(c)) { return false; } - int ref = ComputerUtilAbility.getAbilitySourceName(sa).equals("Forcefield") ? 1 : 0; + int ref = ComputerUtilAbility.getAbilitySourceName(sa).equals("Forcefield") ? 1 : 0; return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > ref; } }); @@ -233,7 +234,7 @@ public class ChooseCardAi extends SpellAbilityAi { return false; } for (SpellAbility sa : c.getAllSpellAbilities()) { - if (sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.getPayCosts().hasTapCost()) { return false; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java index ff0bfc2ceaa..f9c3b3d9798 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -23,7 +24,6 @@ public class ChooseCardNameAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - Card source = sa.getHostCard(); if (sa.hasParam("AILogic")) { // Don't tap creatures that may be able to block if (ComputerUtil.waitForBlocking(sa)) { @@ -60,7 +60,7 @@ public class ChooseCardNameAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean) */ @Override - public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { return ComputerUtilCard.getBestAI(options); } @@ -86,7 +86,7 @@ public class ChooseCardNameAi extends SpellAbilityAi { if (rules.getSplitType() == CardSplitType.Split) { Card copy = CardUtil.getLKICopy(card); - // for calcing i need only one split side + // for calcing i need only one split side if (isOther) { copy.getCurrentState().copyFrom(card.getState(CardStateName.RightSplit), true); } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCompanionAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCompanionAi.java new file mode 100644 index 00000000000..1240d9ab54f --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCompanionAi.java @@ -0,0 +1,29 @@ +package forge.ai.ability; + +import com.google.common.collect.Lists; +import forge.ai.SpellAbilityAi; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class ChooseCompanionAi extends SpellAbilityAi { + + /* (non-Javadoc) + * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean) + */ + @Override + public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { + List cards = Lists.newArrayList(options); + if (cards.isEmpty()) { + return null; + } + + Collections.shuffle(cards); + return cards.get(0); + } +} + diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java index b3ae74d5274..c2cccc1f1f0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java @@ -46,6 +46,6 @@ public class ChooseDirectionAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return canPlayAI(ai, sa); + return mandatory || canPlayAI(ai, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java new file mode 100644 index 00000000000..e5ac8a99d6d --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java @@ -0,0 +1,36 @@ +package forge.ai.ability; + +import forge.ai.SpellAbilityAi; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.spellability.TargetRestrictions; +import forge.util.MyRandom; + +public class ChooseEvenOddAi extends SpellAbilityAi { + + @Override + protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + if (!sa.hasParam("AILogic")) { + return false; + } + TargetRestrictions tgt = sa.getTargetRestrictions(); + if (tgt != null) { + sa.resetTargets(); + Player opp = aiPlayer.getWeakestOpponent(); + if (sa.canTarget(opp)) { + sa.getTargets().add(opp); + } else { + return false; + } + } + boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); + return chance; + } + + @Override + protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + return mandatory || canPlayAI(ai, sa); + } + +} + diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java index 1f4e627bda8..b814b347224 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java @@ -180,25 +180,25 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { Card imprinted = host.getImprintedCards().getFirst(); int dmg = imprinted.getCMC(); Player owner = imprinted.getOwner(); - + //useless cards in hand if (imprinted.getName().equals("Bridge from Below") || imprinted.getName().equals("Haakon, Stromgald Scourge")) { return allow; } - + //bad cards when are thrown from the library to the graveyard, but Yixlid can prevent that if (!player.getGame().isCardInPlay("Yixlid Jailer") && ( imprinted.getName().equals("Gaea's Blessing") || imprinted.getName().equals("Narcomoeba"))) { return allow; } - + // milling against Tamiyo is pointless if (owner.isCardInCommand("Emblem - Tamiyo, the Moon Sage")) { return allow; } - + // milling a land against Gitrog result in card draw if (imprinted.isLand() && owner.isCardInPlay("The Gitrog Monster")) { // try to mill owner @@ -207,19 +207,19 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { } return allow; } - + // milling a creature against Sidisi result in more creatures if (imprinted.isCreature() && owner.isCardInPlay("Sidisi, Brood Tyrant")) { return allow; } - //if Iona does prevent from casting, allow it to draw + //if Iona does prevent from casting, allow it to draw for (final Card io : player.getCardsIn(ZoneType.Battlefield, "Iona, Shield of Emeria")) { if (CardUtil.getColors(imprinted).hasAnyColor(MagicColor.fromName(io.getChosenColor()))) { return allow; } } - + if (dmg == 0) { // If CMC = 0, mill it! return deny; @@ -244,7 +244,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { SpellAbility counterSA = spells.get(0), tokenSA = spells.get(1); // check for something which might prevent the counters to be placed on host - if (!host.canReceiveCounters(CounterType.P1P1)) { + if (!host.canReceiveCounters(CounterEnumType.P1P1)) { return tokenSA; } @@ -256,7 +256,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { // need a copy for one with extra +1/+1 counter boost, // without causing triggers to run final Card copy = CardUtil.getLKICopy(host); - copy.setCounters(CounterType.P1P1, copy.getCounters(CounterType.P1P1) + n); + copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + n); copy.setZone(host.getZone()); // if host would put into the battlefield attacking @@ -281,10 +281,10 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { // TODO check for trigger to turn token ETB into +1/+1 counter for host // TODO check for trigger to turn token ETB into damage or life loss for opponent // in this cases Token might be prefered even if they would not survive - final Card tokenCard = TokenAi.spawnToken(player, tokenSA, true); + final Card tokenCard = TokenAi.spawnToken(player, tokenSA); - // Token would not survive - if (tokenCard.getNetToughness() < 1) { + // Token would not survive + if (!tokenCard.isCreature() || tokenCard.getNetToughness() < 1) { return counterSA; } @@ -336,7 +336,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { filtered.add(sp); } } - + // TODO find better way to check if (!filtered.isEmpty()) { return filtered.get(0); @@ -362,7 +362,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { game.getAction().checkStaticAbilities(false); // can't gain counters, use Haste - if (!copy.canReceiveCounters(CounterType.P1P1)) { + if (!copy.canReceiveCounters(CounterEnumType.P1P1)) { return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java b/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java index ddb90e701f1..5b98b108cda 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java @@ -9,6 +9,7 @@ import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import java.util.List; +import java.util.Map; public class ChoosePlayerAi extends SpellAbilityAi { @Override @@ -27,7 +28,7 @@ public class ChoosePlayerAi extends SpellAbilityAi { } @Override - public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable choices) { + public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable choices, Map params) { Player chosen = null; if ("Curse".equals(sa.getParam("AILogic"))) { for (Player pc : choices) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java index f7a694e0c67..038f9a843ab 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import com.google.common.base.Predicate; import com.google.common.base.Predicates; @@ -126,7 +127,7 @@ public class ChooseSourceAi extends SpellAbilityAi { @Override - public Card chooseSingleCard(final Player aiChoser, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(final Player aiChoser, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { if ("NeedsPrevention".equals(sa.getParam("AILogic"))) { final Player ai = sa.getActivatingPlayer(); final Game game = ai.getGame(); diff --git a/forge-ai/src/main/java/forge/ai/ability/ClashAi.java b/forge-ai/src/main/java/forge/ai/ability/ClashAi.java index 5800ecefb32..b9f7e8d7e6f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ClashAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ClashAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.collect.Iterables; import forge.ai.ComputerUtilCard; @@ -56,7 +58,7 @@ public class ClashAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility, java.lang.Iterable) */ @Override - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { for (Player p : options) { if (p.getCardsIn(ZoneType.Library).size() == 0) return p; @@ -82,7 +84,7 @@ public class ClashAi extends SpellAbilityAi { PlayerCollection players = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); // use chooseSinglePlayer function to the select player - Player chosen = chooseSinglePlayer(ai, sa, players); + Player chosen = chooseSinglePlayer(ai, sa, players, null); if (chosen != null) { sa.resetTargets(); sa.getTargets().add(chosen); diff --git a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java index 3d6870d033d..3a3de604029 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java @@ -15,6 +15,7 @@ import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import java.util.List; +import java.util.Map; public class CloneAi extends SpellAbilityAi { @@ -169,7 +170,7 @@ public class CloneAi extends SpellAbilityAi { */ @Override protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, - Player targetedPlayer) { + Player targetedPlayer, Map params) { final Card host = sa.getHostCard(); final Player ctrl = host.getController(); 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 7a29ee98068..617da8098eb 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java @@ -18,6 +18,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; @@ -302,7 +303,7 @@ public class ControlGainAi extends SpellAbilityAi { } // pumpDrawbackAI() @Override - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { final List cards = Lists.newArrayList(); for (Player p : options) { cards.addAll(p.getCreaturesInPlay()); diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlGainVariantAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlGainVariantAi.java new file mode 100644 index 00000000000..894a6951c15 --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/ControlGainVariantAi.java @@ -0,0 +1,82 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.ai.ability; + +import java.util.List; +import java.util.Map; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; + +import forge.ai.ComputerUtilCard; +import forge.ai.SpellAbilityAi; +import forge.game.card.Card; +import forge.game.card.CardLists; +import forge.game.card.CardPredicates; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; + + +/** + *

+ * AbilityFactory_GainControlVariant class. + *

+ * + * @author Forge + * @version $Id: AbilityFactoryGainControl.java 17764 2012-10-29 11:04:18Z Sloth $ + */ +public class ControlGainVariantAi extends SpellAbilityAi { + @Override + protected boolean canPlayAI(final Player ai, final SpellAbility sa) { + + String logic = sa.getParam("AILogic"); + + if ("GainControlOwns".equals(logic)) { + List list = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield), new Predicate() { + @Override + public boolean apply(final Card crd) { + return crd.isCreature() && !crd.getController().equals(crd.getOwner()); + } + }); + if (list.isEmpty()) { + return false; + } + for (final Card c : list) { + if (ai.equals(c.getController())) { + return false; + } + } + } + + return true; + + } + + @Override + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { + Iterable otherCtrl = CardLists.filter(options, Predicates.not(CardPredicates.isController(ai))); + if (Iterables.isEmpty(otherCtrl)) { + return ComputerUtilCard.getWorstAI(options); + } else { + return ComputerUtilCard.getBestAI(otherCtrl); + } + } + +} diff --git a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java index 7ce9dfbc473..e455230988f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java @@ -19,6 +19,7 @@ import forge.game.zone.ZoneType; import java.util.Collection; import java.util.List; +import java.util.Map; public class CopyPermanentAi extends SpellAbilityAi { @Override @@ -204,7 +205,7 @@ public class CopyPermanentAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean) */ @Override - public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { // Select a card to attach to CardCollection betterOptions = getBetterOptions(ai, sa, options, isOptional); if (!betterOptions.isEmpty()) { @@ -223,7 +224,7 @@ public class CopyPermanentAi extends SpellAbilityAi { } @Override - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { final List cards = new PlayerCollection(options).getCreaturesInPlay(); Card chosen = ComputerUtilCard.getBestCreatureAI(cards); return chosen != null ? chosen.getController() : Iterables.getFirst(options, null); diff --git a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java index d130c18a666..25a05760964 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java @@ -29,8 +29,8 @@ public class CopySpellAbilityAi extends SpellAbilityAi { final SpellAbility top = game.getStack().peekAbility(); if (top != null - && top.getPayCosts() != null && top.getPayCosts().getCostMana() != null - && sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null + && top.getPayCosts().getCostMana() != null + && sa.getPayCosts().getCostMana() != null && top.getPayCosts().getCostMana().getMana().getCMC() >= sa.getPayCosts().getCostMana().getMana().getCMC() + diff) { // The copied spell has a significantly higher CMC than the copy spell, consider copying chance = 100; diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java index 2535c29505d..659b05de3cd 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -26,7 +26,7 @@ import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.keyword.Keyword; import forge.util.Aggregates; @@ -35,7 +35,7 @@ import forge.util.Aggregates; *

* AbilityFactory_Counters class. *

- * + * * @author Forge * @version $Id$ */ @@ -46,7 +46,7 @@ public abstract class CountersAi { *

* chooseCursedTarget. *

- * + * * @param list * a {@link forge.CardList} object. * @param type @@ -77,7 +77,7 @@ public abstract class CountersAi { *

* chooseBoonTarget. *

- * + * * @param list * a {@link forge.CardList} object. * @param type @@ -97,7 +97,7 @@ public abstract class CountersAi { final CardCollection boon = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { - return c.getCounters(CounterType.DIVINITY) == 0; + return c.getCounters(CounterEnumType.DIVINITY) == 0; } }); choice = ComputerUtilCard.getMostExpensivePermanentAI(boon, null, false); diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java index bf142184a33..b6e75e62b91 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java @@ -42,14 +42,14 @@ public class CountersMoveAi extends SpellAbilityAi { protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { final Card host = sa.getHostCard(); final String type = sa.getParam("CounterType"); - final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type); + final CounterType cType = "Any".equals(type) ? null : CounterType.getType(type); // Don't tap creatures that may be able to block if (ComputerUtil.waitForBlocking(sa)) { return false; } - if (CounterType.P1P1.equals(cType) && sa.hasParam("Source")) { + if (CounterEnumType.P1P1.equals(cType) && sa.hasParam("Source")) { int amount = calcAmount(sa, cType); final List srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa); if (ph.getPlayerTurn().isOpponentOf(ai)) { @@ -92,7 +92,7 @@ public class CountersMoveAi extends SpellAbilityAi { // for Simic Fluxmage and other return ph.getNextTurn().equals(ai) && !ph.getPhase().isBefore(PhaseType.END_OF_TURN); - } else if (CounterType.P1P1.equals(cType) && sa.hasParam("Defined")) { + } else if (CounterEnumType.P1P1.equals(cType) && sa.hasParam("Defined")) { // something like Cyptoplast Root-kin if (ph.getPlayerTurn().isOpponentOf(ai)) { if (ph.inCombat() && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) { @@ -115,6 +115,7 @@ public class CountersMoveAi extends SpellAbilityAi { protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { + sa.resetTargets(); if (!moveTgtAI(ai, sa) && !mandatory) { return false; @@ -142,7 +143,7 @@ public class CountersMoveAi extends SpellAbilityAi { final Card host = sa.getHostCard(); final String type = sa.getParam("CounterType"); - final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type); + final CounterType cType = "Any".equals(type) ? null : CounterType.getType(type); final List srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa); final List destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa); @@ -189,7 +190,7 @@ public class CountersMoveAi extends SpellAbilityAi { // check for some specific AI preferences if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) { - return cType != CounterType.P1P1 || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0; + return !cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0; } } // no target @@ -234,7 +235,7 @@ public class CountersMoveAi extends SpellAbilityAi { final Card host = sa.getHostCard(); final Game game = ai.getGame(); final String type = sa.getParam("CounterType"); - final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type); + final CounterType cType = "Any".equals(type) || "All".equals(type) ? null : CounterType.getType(type); List tgtCards = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa); @@ -278,7 +279,7 @@ public class CountersMoveAi extends SpellAbilityAi { // do not steal a P1P1 from Undying if it would die // this way - if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { + if (CounterEnumType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken(); } return true; @@ -321,13 +322,13 @@ public class CountersMoveAi extends SpellAbilityAi { } // try to remove P1P1 from undying or evolve - if (CounterType.P1P1.equals(cType)) { + if (CounterEnumType.P1P1.equals(cType)) { if (card.hasKeyword(Keyword.UNDYING) || card.hasKeyword(Keyword.EVOLVE) || card.hasKeyword(Keyword.ADAPT)) { return true; } } - if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) { + if (CounterEnumType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) { return true; } @@ -382,10 +383,10 @@ public class CountersMoveAi extends SpellAbilityAi { } if (cType != null) { - if (CounterType.P1P1.equals(cType) && card.hasKeyword(Keyword.UNDYING)) { + if (CounterEnumType.P1P1.equals(cType) && card.hasKeyword(Keyword.UNDYING)) { return false; } - if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) { + if (CounterEnumType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) { return false; } @@ -393,7 +394,7 @@ public class CountersMoveAi extends SpellAbilityAi { return false; } } - return false; + return true; } }); @@ -452,7 +453,7 @@ public class CountersMoveAi extends SpellAbilityAi { // or for source -> multiple defined @Override protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, - Player targetedPlayer) { + Player targetedPlayer, Map params) { if (sa.hasParam("AiLogic")) { String logic = sa.getParam("AiLogic"); diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java index b20e99ace3b..1dc29e1a89b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java @@ -16,6 +16,7 @@ import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.card.CardPredicates; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -77,7 +78,7 @@ public class CountersMultiplyAi extends SpellAbilityAi { protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { final CounterType counterType = getCounterType(sa); - if (!CounterType.P1P1.equals(counterType) && counterType != null) { + if (!CounterEnumType.P1P1.equals(counterType) && counterType != null) { if (!sa.hasParam("ActivationPhases")) { // Don't use non P1P1/M1M1 counters before main 2 if possible if (ph.getPhase().isBefore(PhaseType.MAIN2) && !ComputerUtil.castSpellInMain1(ai, sa)) { @@ -147,15 +148,15 @@ public class CountersMultiplyAi extends SpellAbilityAi { if (!aiList.isEmpty()) { // counter type list to check // first loyalty, then P1P!, then Charge Counter - List typeList = Lists.newArrayList(CounterType.LOYALTY, CounterType.P1P1, CounterType.CHARGE); - for (CounterType type : typeList) { + List typeList = Lists.newArrayList(CounterEnumType.LOYALTY, CounterEnumType.P1P1, CounterEnumType.CHARGE); + for (CounterEnumType type : typeList) { // enough targets if (!sa.canAddMoreTarget()) { break; } - if (counterType == null || counterType == type) { - addTargetsByCounterType(ai, sa, aiList, type); + if (counterType == null || counterType.is(type)) { + addTargetsByCounterType(ai, sa, aiList, CounterType.get(type)); } } } @@ -164,7 +165,7 @@ public class CountersMultiplyAi extends SpellAbilityAi { if (!oppList.isEmpty()) { // not enough targets if (sa.canAddMoreTarget()) { - final CounterType type = CounterType.M1M1; + final CounterType type = CounterType.get(CounterEnumType.M1M1); if (counterType == null || counterType == type) { addTargetsByCounterType(ai, sa, oppList, type); } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java index 52538a0827c..7dd9618dafe 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java @@ -15,6 +15,7 @@ import forge.game.GameEntity; import forge.game.card.Card; import forge.game.card.CardLists; import forge.game.card.CardUtil; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -32,7 +33,7 @@ public class CountersProliferateAi extends SpellAbilityAi { for (final Player p : allies) { // player has experience or energy counter - if (p.getCounters(CounterType.EXPERIENCE) + p.getCounters(CounterType.ENERGY) >= 1) { + if (p.getCounters(CounterEnumType.EXPERIENCE) + p.getCounters(CounterEnumType.ENERGY) >= 1) { allyExpOrEnergy = true; } cperms.addAll(CardLists.filter(p.getCardsIn(ZoneType.Battlefield), new Predicate() { @@ -115,17 +116,19 @@ public class CountersProliferateAi extends SpellAbilityAi { */ @SuppressWarnings("unchecked") @Override - public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer) { + public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer, Map params) { // Proliferate is always optional for all, no need to select best + final CounterType poison = CounterType.get(CounterEnumType.POISON); + // because countertype can't be chosen anymore, only look for posion counters for (final Player p : Iterables.filter(options, Player.class)) { if (p.isOpponentOf(ai)) { - if (p.getCounters(CounterType.POISON) > 0 && p.canReceiveCounters(CounterType.POISON)) { + if (p.getCounters(poison) > 0 && p.canReceiveCounters(poison)) { return (T)p; } } else { - if (p.getCounters(CounterType.POISON) <= 5 || p.canReceiveCounters(CounterType.POISON)) { + if (p.getCounters(poison) <= 5 || p.canReceiveCounters(poison)) { return (T)p; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index d139920d0a4..c3448d4af15 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -35,7 +35,7 @@ public class CountersPutAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#willPayCosts(forge.game.player.Player, * forge.game.spellability.SpellAbility, forge.game.cost.Cost, * forge.game.card.Card) @@ -56,17 +56,17 @@ public class CountersPutAi extends SpellAbilityAi { if (part instanceof CostRemoveCounter) { final CostRemoveCounter remCounter = (CostRemoveCounter) part; final CounterType counterType = remCounter.counter; - if (counterType.name().equals(type) && !aiLogic.startsWith("MoveCounter")) { + if (counterType.getName().equals(type) && !aiLogic.startsWith("MoveCounter")) { return false; } if (!part.payCostFromSource()) { - if (counterType.equals(CounterType.P1P1)) { + if (counterType.is(CounterEnumType.P1P1)) { return false; } continue; } // don't kill the creature - if (counterType.equals(CounterType.P1P1) && source.getLethalDamage() <= 1) { + if (counterType.is(CounterEnumType.P1P1) && source.getLethalDamage() <= 1) { return false; } } @@ -77,7 +77,7 @@ public class CountersPutAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see * forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player, * forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler) @@ -109,7 +109,7 @@ public class CountersPutAi extends SpellAbilityAi { } } int maxLevel = Integer.parseInt(sa.getParam("MaxLevel")); - return source.getCounters(CounterType.LEVEL) < maxLevel; + return source.getCounters(CounterEnumType.LEVEL) < maxLevel; } return super.checkPhaseRestrictions(ai, sa, ph); @@ -146,7 +146,7 @@ public class CountersPutAi extends SpellAbilityAi { if (abTgt.canTgtPlayer()) { // try to kill opponent with Poison PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); - PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterType.POISON, 9)); + PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterEnumType.POISON, 9)); if (!poisonList.isEmpty()) { sa.getTargets().add(poisonList.max(PlayerPredicates.compareByLife())); return true; @@ -157,13 +157,13 @@ public class CountersPutAi extends SpellAbilityAi { // try to kill creature with -1/-1 counters if it can // receive counters, execpt it has undying CardCollection oppCreat = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa); - CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterType.M1M1)); + CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1)); oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING); oppCreatM1 = CardLists.filter(oppCreatM1, new Predicate() { @Override public boolean apply(Card input) { - return input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.M1M1); + return input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1)); } }); @@ -220,6 +220,8 @@ public class CountersPutAi extends SpellAbilityAi { if ("Never".equals(logic)) { return false; + } else if ("AlwaysWithNoTgt".equals(logic)) { + return true; } else if ("AristocratCounters".equals(logic)) { return PumpAi.doAristocratWithCountersLogic(sa, ai); } else if ("PayEnergy".equals(logic)) { @@ -242,7 +244,7 @@ public class CountersPutAi extends SpellAbilityAi { int totBlkPower = Aggregates.sum(blocked, CardPredicates.Accessors.fnGetNetPower); int totBlkToughness = Aggregates.min(blocked, CardPredicates.Accessors.fnGetNetToughness); - int numActivations = ai.getCounters(CounterType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount(); + int numActivations = ai.getCounters(CounterEnumType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount(); if (sa.getHostCard().getNetToughness() + numActivations > totBlkPower || sa.getHostCard().getNetPower() + numActivations >= totBlkToughness) { return true; @@ -257,7 +259,7 @@ public class CountersPutAi extends SpellAbilityAi { AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); return true; } - } else if (ai.getCounters(CounterType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) { + } else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) { // outside of combat, this logic only works if the relevant AI profile option is enabled // and if there is enough energy saved if (!onlyInCombat) { @@ -291,7 +293,7 @@ public class CountersPutAi extends SpellAbilityAi { if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) { return false; } - + if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence CardCollection prot = ProtectAi.getProtectCreatures(ai, sa.getSubAbility()); if (!prot.isEmpty()) { @@ -320,7 +322,7 @@ public class CountersPutAi extends SpellAbilityAi { Game game = ai.getGame(); Combat combat = game.getCombat(); - if (!source.canReceiveCounters(CounterType.P1P1) || source.getCounters(CounterType.P1P1) > 0) { + if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) { return false; } else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return doCombatAdaptLogic(source, amount, combat); @@ -334,7 +336,7 @@ public class CountersPutAi extends SpellAbilityAi { } return FightAi.canFightAi(ai, sa, nPump, nPump); } - + if (amountStr.equals("X")) { if (source.getSVar(amountStr).equals("Count$xPaid")) { // By default, set PayX here to maximum value (used for most SAs of this type). @@ -343,7 +345,7 @@ public class CountersPutAi extends SpellAbilityAi { if (isClockwork) { // Clockwork Avian and other similar cards: do not tap all mana for X, // instead only rewind to max allowed value when the power gets low enough. - int curCtrs = source.getCounters(CounterType.P1P0); + int curCtrs = source.getCounters(CounterEnumType.P1P0); int maxCtrs = Integer.parseInt(sa.getParam("MaxFromEffect")); // This will "rewind" clockwork cards when they fall to 50% power or below, consider improving @@ -394,7 +396,7 @@ public class CountersPutAi extends SpellAbilityAi { return true; } } - + if (!ai.getGame().getStack().isEmpty() && !SpellAbilityAi.isSorcerySpeed(sa)) { final TargetRestrictions abTgt = sa.getTargetRestrictions(); // only evaluates case where all tokens are placed on a single target @@ -415,8 +417,8 @@ public class CountersPutAi extends SpellAbilityAi { // Targeting if (sa.usesTargeting()) { - sa.resetTargets(); - + sa.resetTargets(); + final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost); if (sa.isCurse()) { @@ -433,7 +435,7 @@ public class CountersPutAi extends SpellAbilityAi { if (sacSelf && c.equals(source)) { return false; } - return sa.canTarget(c) && c.canReceiveCounters(CounterType.valueOf(type)); + return sa.canTarget(c) && c.canReceiveCounters(CounterType.getType(type)); } }); @@ -452,7 +454,6 @@ public class CountersPutAi extends SpellAbilityAi { // but try to do it in Main 2 then so that the AI has a chance to play creatures first. if (list.isEmpty() && sa.hasParam("Planeswalker") - && sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class) && sa.isTargetNumberValid() && sa.getTargets().getNumTargeted() == 0 @@ -487,7 +488,7 @@ public class CountersPutAi extends SpellAbilityAi { } return false; } - + // target loop while (sa.canAddMoreTarget()) { if (list.isEmpty()) { @@ -557,7 +558,7 @@ public class CountersPutAi extends SpellAbilityAi { return false; } - final int currCounters = cards.get(0).getCounters(CounterType.valueOf(type)); + final int currCounters = cards.get(0).getCounters(CounterType.get(type)); // each non +1/+1 counter on the card is a 10% chance of not // activating this ability. @@ -573,11 +574,11 @@ public class CountersPutAi extends SpellAbilityAi { } boolean immediately = ComputerUtil.playImmediately(ai, sa); - + if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) { return false; } - + if (immediately) { return true; } @@ -611,7 +612,7 @@ public class CountersPutAi extends SpellAbilityAi { final boolean divided = sa.hasParam("DividedAsYouChoose"); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); - final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger()) + final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger()) || (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger()); if (sa.usesTargeting()) { @@ -691,12 +692,12 @@ public class CountersPutAi extends SpellAbilityAi { final boolean divided = sa.hasParam("DividedAsYouChoose"); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); int left = amount; - + if (!sa.usesTargeting()) { // No target. So must be defined list = new CardCollection(AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)); - - if (amountStr.equals("X") + + if (amountStr.equals("X") && !source.hasSVar("PayX") /* SubAbility on something that already had set PayX, e.g. Endless One ETB counters */ && ((sa.hasParam(amountStr) && sa.getSVar(amountStr).equals("Count$xPaid")) || source.getSVar(amountStr).equals("Count$xPaid") )) { @@ -704,7 +705,7 @@ public class CountersPutAi extends SpellAbilityAi { SpellAbility testSa = sa; int countX = 0; int nonXGlyphs = 0; - while (testSa != null && testSa.getPayCosts() != null && countX == 0) { + while (testSa != null && countX == 0) { countX = testSa.getPayCosts().getTotalMana().countX(); nonXGlyphs = testSa.getPayCosts().getTotalMana().getGlyphCount() - countX; testSa = testSa.getSubAbility(); @@ -726,12 +727,27 @@ public class CountersPutAi extends SpellAbilityAi { source.setSVar("PayX", Integer.toString(payX)); } - + if (!mandatory) { // TODO - If Trigger isn't mandatory, when wouldn't we want to // put a counter? // things like Powder Keg, which are way too complex for the AI } + } else if (sa.getTargetRestrictions().canOnlyTgtOpponent() && !sa.getTargetRestrictions().canTgtCreature()) { + // can only target opponent + List playerList = Lists.newArrayList(Iterables.filter( + sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class)); + + if (playerList.isEmpty() && mandatory) { + return false; + } + + // try to choose player with less creatures + Player choice = Collections.min(playerList, PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.Presets.CREATURES)); + + if (choice != null) { + sa.getTargets().add(choice); + } } else { if (sa.isCurse()) { list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); @@ -838,7 +854,7 @@ public class CountersPutAi extends SpellAbilityAi { List threatening = CardLists.filter(creats, new Predicate() { @Override public boolean apply(Card c) { - return CombatUtil.canBlock(source, c, !isHaste) + return CombatUtil.canBlock(source, c, !isHaste) && (c.getNetToughness() > source.getNetPower() + tributeAmount || c.hasKeyword(Keyword.DEATHTOUCH)); } }); @@ -873,24 +889,28 @@ public class CountersPutAi extends SpellAbilityAi { } @Override - public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { // used by Tribute, select player with lowest Life // TODO add more logic using TributeAILogic List list = Lists.newArrayList(options); return Collections.min(list, PlayerPredicates.compareByLife()); } - + @Override - protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { // Bolster does use this // TODO need more or less logic there? + final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1); + final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1); // no logic if there is no options or no to choice if (!isOptional && Iterables.size(options) <= 1) { return Iterables.getFirst(options, null); } - final CounterType type = CounterType.valueOf(sa.getParam("CounterType")); + final CounterType type = params.containsKey("CounterType") ? (CounterType)params.get("CounterType") + : CounterType.getType(sa.getParam("CounterType")); + final String amountStr = sa.getParamOrDefault("CounterNum", "1"); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); @@ -907,7 +927,7 @@ public class CountersPutAi extends SpellAbilityAi { return false; if (ComputerUtilCard.isUselessCreature(ai, input)) return false; - if (CounterType.M1M1.equals(type) && amount >= input.getNetToughness()) + if (type.is(CounterEnumType.M1M1) && amount >= input.getNetToughness()) return true; return ComputerUtil.isNegativeCounter(type, input); } @@ -931,6 +951,20 @@ public class CountersPutAi extends SpellAbilityAi { CardCollection filtered = mine; + // Try to filter out keywords that we already have on cards + if (type.isKeywordCounter()) { + Keyword kw = Keyword.smartValueOf(type.getName()); + final CardCollection doNotHaveKeyword = CardLists.filter(filtered, new Predicate() { + @Override + public boolean apply(Card card) { + return !card.hasKeyword(kw) && card.canBeTargetedBy(sa) && sa.canTarget(card); + } + }); + + if (doNotHaveKeyword.size() > 0) + filtered = doNotHaveKeyword; + } + final CardCollection notUseless = CardLists.filter(filtered, new Predicate() { @Override public boolean apply(Card input) { @@ -945,26 +979,26 @@ public class CountersPutAi extends SpellAbilityAi { } // some special logic to reload Persist/Undying - if (CounterType.P1P1.equals(type)) { + if (p1p1.equals(type)) { final CardCollection persist = CardLists.filter(filtered, new Predicate() { @Override public boolean apply(Card input) { if (!input.hasKeyword(Keyword.PERSIST)) return false; - return input.getCounters(CounterType.M1M1) <= amount; + return input.getCounters(m1m1) <= amount; } }); if (!persist.isEmpty()) { filtered = persist; } - } else if (CounterType.M1M1.equals(type)) { + } else if (m1m1.equals(type)) { final CardCollection undying = CardLists.filter(filtered, new Predicate() { @Override public boolean apply(Card input) { if (!input.hasKeyword(Keyword.UNDYING)) return false; - return input.getCounters(CounterType.P1P1) <= amount && input.getNetToughness() > amount; + return input.getCounters(p1p1) <= amount && input.getNetToughness() > amount; } }); @@ -987,8 +1021,8 @@ public class CountersPutAi extends SpellAbilityAi { if (e instanceof Card) { Card c = (Card) e; if (c.getController().isOpponentOf(ai)) { - if (options.contains(CounterType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) { - return CounterType.M1M1; + if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) { + return CounterType.get(CounterEnumType.M1M1); } for (CounterType type : options) { if (ComputerUtil.isNegativeCounter(type, c)) { @@ -1005,12 +1039,12 @@ public class CountersPutAi extends SpellAbilityAi { } else if (e instanceof Player) { Player p = (Player) e; if (p.isOpponentOf(ai)) { - if (options.contains(CounterType.POISON)) { - return CounterType.POISON; + if (options.contains(CounterType.get(CounterEnumType.POISON))) { + return CounterType.get(CounterEnumType.POISON); } } else { - if (options.contains(CounterType.EXPERIENCE)) { - return CounterType.EXPERIENCE; + if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) { + return CounterType.get(CounterEnumType.EXPERIENCE); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java index 6142b9c71ff..8c883637620 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java @@ -120,7 +120,7 @@ public class CountersPutAllAi extends SpellAbilityAi { //Check for cards that could profit from the ability PhaseHandler phase = ai.getGame().getPhaseHandler(); if (type.equals("P1P1") && sa.isAbility() && source.isCreature() - && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost() + && sa.getPayCosts().hasTapCost() && sa instanceof AbilitySub && (!phase.getNextTurn().equals(ai) || phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS))) { diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java index 4195a7924b7..a261270dec9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -36,7 +36,7 @@ import java.util.Map; *

* AbilityFactory_PutOrRemoveCountersAi class. *

- * + * * @author Forge * @version $Id$ */ @@ -44,7 +44,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, * forge.game.spellability.SpellAbility) */ @@ -75,7 +75,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { if (sa.hasParam("CounterType")) { // currently only Jhoira's Timebug - final CounterType type = CounterType.valueOf(sa.getParam("CounterType")); + final CounterType type = CounterType.getType(sa.getParam("CounterType")); CardCollection countersList = CardLists.filter(list, CardPredicates.hasCounter(type, amount)); @@ -100,7 +100,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { CardCollectionView depthsList = CardLists.filter(countersList, - CardPredicates.nameEquals("Dark Depths"), CardPredicates.hasCounter(CounterType.ICE)); + CardPredicates.nameEquals("Dark Depths"), CardPredicates.hasCounter(CounterEnumType.ICE)); if (!depthsList.isEmpty()) { sa.getTargets().add(depthsList.getFirst()); @@ -113,7 +113,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { CardCollection planeswalkerList = CardLists.filter( CardLists.filterControlledBy(countersList, ai.getOpponents()), CardPredicates.Presets.PLANESWALKERS, - CardPredicates.hasLessCounter(CounterType.LOYALTY, amount)); + CardPredicates.hasLessCounter(CounterEnumType.LOYALTY, amount)); if (!planeswalkerList.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList)); @@ -123,7 +123,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { // do as M1M1 part CardCollection aiList = CardLists.filterControlledBy(countersList, ai); - CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1)); + CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.M1M1)); CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST); if (!aiPersistList.isEmpty()) { @@ -136,7 +136,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { } // do as P1P1 part - CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1)); + CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.P1P1)); CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING); if (!aiUndyingList.isEmpty()) { @@ -183,7 +183,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#chooseCounterType(java.util.List, * forge.game.spellability.SpellAbility, java.util.Map) */ @@ -199,18 +199,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { Card tgt = (Card) params.get("Target"); // planeswalker has high priority for loyalty counters - if (tgt.isPlaneswalker() && options.contains(CounterType.LOYALTY)) { - return CounterType.LOYALTY; + if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) { + return CounterType.get(CounterEnumType.LOYALTY); } if (tgt.getController().isOpponentOf(ai)) { // creatures with BaseToughness below or equal zero might be // killed if their counters are removed if (tgt.isCreature() && tgt.getBaseToughness() <= 0) { - if (options.contains(CounterType.P1P1)) { - return CounterType.P1P1; - } else if (options.contains(CounterType.M1M1)) { - return CounterType.M1M1; + if (options.contains(CounterType.get(CounterEnumType.P1P1))) { + return CounterType.get(CounterEnumType.P1P1); + } else if (options.contains(CounterType.get(CounterEnumType.M1M1))) { + return CounterType.get(CounterEnumType.M1M1); } } @@ -222,14 +222,14 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { } } else { // this counters are treat first to be removed - if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.ICE)) { + if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { - return CounterType.ICE; + return CounterType.get(CounterEnumType.ICE); } - } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.P1P1)) { - return CounterType.P1P1; - } else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.M1M1)) { - return CounterType.M1M1; + } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) { + return CounterType.get(CounterEnumType.P1P1); + } else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) { + return CounterType.get(CounterEnumType.M1M1); } // fallback logic, select positive counter to add more @@ -246,7 +246,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see * forge.ai.SpellAbilityAi#chooseBinary(forge.game.player.PlayerController. * BinaryChoiceType, forge.game.spellability.SpellAbility, java.util.Map) @@ -262,19 +262,19 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule); if (tgt.getController().isOpponentOf(ai)) { - if (type.equals(CounterType.LOYALTY) && tgt.isPlaneswalker()) { + if (type.is(CounterEnumType.LOYALTY) && tgt.isPlaneswalker()) { return false; } return ComputerUtil.isNegativeCounter(type, tgt); } else { - if (type.equals(CounterType.ICE) && "Dark Depths".equals(tgt.getName())) { + if (type.is(CounterEnumType.ICE) && "Dark Depths".equals(tgt.getName())) { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { return false; } - } else if (type.equals(CounterType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) { + } else if (type.is(CounterEnumType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) { return false; - } else if (type.equals(CounterType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) { + } else if (type.is(CounterEnumType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java index 24a0ff0fa9d..2c4ad3bbfe0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java @@ -33,7 +33,7 @@ public class CountersRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see * forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player, * forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler) @@ -50,7 +50,7 @@ public class CountersRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see * forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player, * forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler, @@ -68,7 +68,7 @@ public class CountersRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, * forge.game.spellability.SpellAbility) */ @@ -82,7 +82,7 @@ public class CountersRemoveAi extends SpellAbilityAi { } if (!type.matches("Any") && !type.matches("All")) { - final int currCounters = sa.getHostCard().getCounters(CounterType.valueOf(type)); + final int currCounters = sa.getHostCard().getCounters(CounterType.getType(type)); if (currCounters < 1) { return false; } @@ -119,7 +119,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths"); depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa), - CardPredicates.hasCounter(CounterType.ICE, 3)); + CardPredicates.hasCounter(CounterEnumType.ICE, 3)); if (!depthsList.isEmpty()) { sa.getTargets().add(depthsList.getFirst()); @@ -132,7 +132,7 @@ public class CountersRemoveAi extends SpellAbilityAi { list = CardLists.filter(list, CardPredicates.isTargetableBy(sa)); CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS, - CardPredicates.hasCounter(CounterType.LOYALTY, 5)); + CardPredicates.hasCounter(CounterEnumType.LOYALTY, 5)); if (!planeswalkerList.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList)); @@ -159,11 +159,11 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!ai.isCardInPlay("Marit Lage") || noLegendary) { CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths"); depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa), - CardPredicates.hasCounter(CounterType.ICE)); + CardPredicates.hasCounter(CounterEnumType.ICE)); if (!depthsList.isEmpty()) { Card depth = depthsList.getFirst(); - int ice = depth.getCounters(CounterType.ICE); + int ice = depth.getCounters(CounterEnumType.ICE); if (amount >= ice) { sa.getTargets().add(depth); if (xPay) { @@ -180,7 +180,7 @@ public class CountersRemoveAi extends SpellAbilityAi { CardCollection planeswalkerList = CardLists.filter(list, Predicates.and(CardPredicates.Presets.PLANESWALKERS, CardPredicates.isControlledByAnyOf(ai.getOpponents())), - CardPredicates.hasLessCounter(CounterType.LOYALTY, amount)); + CardPredicates.hasLessCounter(CounterEnumType.LOYALTY, amount)); if (!planeswalkerList.isEmpty()) { Card best = ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList); @@ -196,7 +196,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // do as M1M1 part CardCollection aiList = CardLists.filterControlledBy(list, ai); - CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1)); + CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.M1M1)); CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST); if (!aiPersistList.isEmpty()) { @@ -209,7 +209,7 @@ public class CountersRemoveAi extends SpellAbilityAi { } // do as P1P1 part - CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasLessCounter(CounterType.P1P1, amount)); + CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasLessCounter(CounterEnumType.P1P1, amount)); CardCollection aiUndyingList = CardLists.getKeyword(aiP1P1List, Keyword.UNDYING); if (!aiUndyingList.isEmpty()) { @@ -220,7 +220,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // remove P1P1 counters from opposing creatures CardCollection oppP1P1List = CardLists.filter(list, Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isControlledByAnyOf(ai.getOpponents())), - CardPredicates.hasCounter(CounterType.P1P1)); + CardPredicates.hasCounter(CounterEnumType.P1P1)); if (!oppP1P1List.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List)); return true; @@ -244,7 +244,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // no special amount for that one yet int amount = AbilityUtils.calculateAmount(source, amountStr, sa); CardCollection aiList = CardLists.filterControlledBy(list, ai); - aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1, amount)); + aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.M1M1, amount)); CardCollection aiPersist = CardLists.getKeyword(aiList, Keyword.PERSIST); if (!aiPersist.isEmpty()) { @@ -263,7 +263,7 @@ public class CountersRemoveAi extends SpellAbilityAi { // no special amount for that one yet int amount = AbilityUtils.calculateAmount(source, amountStr, sa); - list = CardLists.filter(list, CardPredicates.hasCounter(CounterType.P1P1, amount)); + list = CardLists.filter(list, CardPredicates.hasCounter(CounterEnumType.P1P1, amount)); // currently only logic for Bloodcrazed Hoplite, but add logic for // targeting ai creatures too @@ -309,12 +309,12 @@ public class CountersRemoveAi extends SpellAbilityAi { amount = AbilityUtils.calculateAmount(source, amountStr, sa); } - CardCollection timeList = CardLists.filter(list, CardPredicates.hasLessCounter(CounterType.TIME, amount)); + CardCollection timeList = CardLists.filter(list, CardPredicates.hasLessCounter(CounterEnumType.TIME, amount)); if (!timeList.isEmpty()) { Card best = ComputerUtilCard.getBestAI(timeList); - int timeCount = best.getCounters(CounterType.TIME); + int timeCount = best.getCounters(CounterEnumType.TIME); sa.getTargets().add(best); if (xPay) { source.setSVar("PayX", Integer.toString(timeCount)); @@ -335,7 +335,7 @@ public class CountersRemoveAi extends SpellAbilityAi { CardCollection outlastCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.OUTLAST)); if (!outlastCreats.isEmpty()) { // outlast cards often benefit from having +1/+1 counters, try not to remove last one - CardCollection betterTargets = CardLists.filter(outlastCreats, CardPredicates.hasCounter(CounterType.P1P1, 2)); + CardCollection betterTargets = CardLists.filter(outlastCreats, CardPredicates.hasCounter(CounterEnumType.P1P1, 2)); if (!betterTargets.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets)); @@ -363,7 +363,7 @@ public class CountersRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#chooseNumber(forge.game.player.Player, * forge.game.spellability.SpellAbility, int, int, java.util.Map) */ @@ -377,8 +377,8 @@ public class CountersRemoveAi extends SpellAbilityAi { if (targetCard.getController().isOpponentOf(player)) { return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min; } else { - if (targetCard.hasKeyword(Keyword.UNDYING) && type == CounterType.P1P1 - && targetCard.getCounters(CounterType.P1P1) >= max) { + if (targetCard.hasKeyword(Keyword.UNDYING) && type.is(CounterEnumType.P1P1) + && targetCard.getCounters(CounterEnumType.P1P1) >= max) { return max; } @@ -387,9 +387,9 @@ public class CountersRemoveAi extends SpellAbilityAi { } else if (target instanceof Player) { Player targetPlayer = (Player) target; if (targetPlayer.isOpponentOf(player)) { - return !type.equals(CounterType.POISON) ? max : min; + return !type.equals(CounterEnumType.POISON) ? max : min; } else { - return type.equals(CounterType.POISON) ? max : min; + return type.equals(CounterEnumType.POISON) ? max : min; } } @@ -398,7 +398,7 @@ public class CountersRemoveAi extends SpellAbilityAi { /* * (non-Javadoc) - * + * * @see forge.ai.SpellAbilityAi#chooseCounterType(java.util.List, * forge.game.spellability.SpellAbility, java.util.Map) */ @@ -415,7 +415,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (targetCard.getController().isOpponentOf(ai)) { // if its a Planeswalker try to remove Loyality first if (targetCard.isPlaneswalker()) { - return CounterType.LOYALTY; + return CounterType.get(CounterEnumType.LOYALTY); } for (CounterType type : options) { if (!ComputerUtil.isNegativeCounter(type, targetCard)) { @@ -423,10 +423,10 @@ public class CountersRemoveAi extends SpellAbilityAi { } } } else { - if (options.contains(CounterType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) { - return CounterType.M1M1; - } else if (options.contains(CounterType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) { - return CounterType.P1P1; + if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) { + return CounterType.get(CounterEnumType.M1M1); + } else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) { + return CounterType.get(CounterEnumType.P1P1); } for (CounterType type : options) { if (ComputerUtil.isNegativeCounter(type, targetCard)) { @@ -438,13 +438,13 @@ public class CountersRemoveAi extends SpellAbilityAi { Player targetPlayer = (Player) target; if (targetPlayer.isOpponentOf(ai)) { for (CounterType type : options) { - if (!type.equals(CounterType.POISON)) { + if (!type.equals(CounterEnumType.POISON)) { return type; } } } else { for (CounterType type : options) { - if (type.equals(CounterType.POISON)) { + if (type.equals(CounterEnumType.POISON)) { return type; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java index cbede1c1a11..79e83ac65a4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java @@ -6,7 +6,7 @@ import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.cost.Cost; import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; @@ -39,7 +39,7 @@ public class DamageAllAi extends SpellAbilityAi { if (!ai.getGame().getStack().isEmpty()) { return false; } - + int x = -1; final String damage = sa.getParam("NumDmg"); int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa); @@ -50,10 +50,9 @@ public class DamageAllAi extends SpellAbilityAi { x = ComputerUtilMana.determineLeftoverMana(sa, ai); } if (damage.equals("ChosenX")) { - x = source.getCounters(CounterType.LOYALTY); + x = source.getCounters(CounterEnumType.LOYALTY); } if (x == -1) { - Player bestOpp = determineOppToKill(ai, sa, source, dmg); if (determineOppToKill(ai, sa, source, dmg) != null) { // we already know we can kill a player, so go for it return true; @@ -138,7 +137,7 @@ public class DamageAllAi extends SpellAbilityAi { } int minGain = 200; // The minimum gain in destroyed creatures - if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) { + if (sa.getPayCosts().isReusuableResource()) { if (computerList.isEmpty()) { minGain = 10; // nothing to lose // no creatures to lose and player can be damaged 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 797c18d441f..936e2187357 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -46,9 +46,9 @@ public class DamageDealAi extends DamageAiBase { if ("MadSarkhanDigDmg".equals(logic)) { return SpecialCardAi.SarkhanTheMad.considerDig(ai, sa); } - + if (damage.equals("X") && sa.getSVar(damage).equals("Count$ChosenNumber")) { - int energy = ai.getCounters(CounterType.ENERGY); + int energy = ai.getCounters(CounterEnumType.ENERGY); for (SpellAbility s : source.getSpellAbilities()) { if ("PayEnergy".equals(s.getParam("AILogic"))) { energy += AbilityUtils.calculateAmount(source, s.getParam("CounterNum"), sa); @@ -145,7 +145,7 @@ public class DamageDealAi extends DamageAiBase { if (sourceName.equals("Crater's Claws") && ai.hasFerocious()) { dmg += 2; } - + String logic = sa.getParamOrDefault("AILogic", ""); if ("DiscardLands".equals(logic)) { dmg = 2; @@ -165,7 +165,7 @@ public class DamageDealAi extends DamageAiBase { List wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source); dmg = Aggregates.sum(wolves, CardPredicates.Accessors.fnGetNetPower); } else if ("Triskelion".equals(logic)) { - final int n = source.getCounters(CounterType.P1P1); + final int n = source.getCounters(CounterEnumType.P1P1); if (n > 0) { if (ComputerUtil.playImmediately(ai, sa)) { /* @@ -196,9 +196,9 @@ public class DamageDealAi extends DamageAiBase { } return false; } - + if (sourceName.equals("Sorin, Grim Nemesis")) { - int loyalty = source.getCounters(CounterType.LOYALTY); + int loyalty = source.getCounters(CounterEnumType.LOYALTY); for (; loyalty > 0; loyalty--) { if (this.damageTargetAI(ai, sa, loyalty, false)) { dmg = ComputerUtilCombat.getEnoughDamageToKill(sa.getTargetCard(), loyalty, source, false, false); @@ -228,7 +228,7 @@ public class DamageDealAi extends DamageAiBase { if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) { return false; } - + if ("DiscardLands".equals(sa.getParam("AILogic")) && !ComputerUtilCost.checkDiscardCost(ai, abCost, source)) { return false; } @@ -285,7 +285,7 @@ public class DamageDealAi extends DamageAiBase { } } - if ("XCountersDamage".equals(logic) && sa.getPayCosts() != null) { + if ("XCountersDamage".equals(logic)) { // Check to ensure that we have enough counters to remove per the defined PayX for (CostPart part : sa.getPayCosts().getCostParts()) { if (part instanceof CostRemoveCounter) { @@ -309,7 +309,7 @@ public class DamageDealAi extends DamageAiBase { *

* dealDamageChooseTgtC. *

- * + * * @param d * a int. * @param noPrevention @@ -445,11 +445,11 @@ public class DamageDealAi extends DamageAiBase { // As of right now, ranks planeswalkers by their Current Loyalty * 10 + Big buff if close to "Ultimate" int bestScore = 0; for (Card pw : pws) { - int curLoyalty = pw.getCounters(CounterType.LOYALTY); + int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY); int pwScore = curLoyalty * 10; for (SpellAbility sa : pw.getSpellAbilities()) { - if (sa.hasParam("Ultimate") && sa.getPayCosts() != null) { + if (sa.hasParam("Ultimate")) { Integer loyaltyCost = 0; CostRemoveCounter remLoyalty = sa.getPayCosts().getCostPartByType(CostRemoveCounter.class); if (remLoyalty != null) { @@ -478,7 +478,7 @@ public class DamageDealAi extends DamageAiBase { int bestScore = Integer.MAX_VALUE; for (Card pw : pws) { - int curLoyalty = pw.getCounters(CounterType.LOYALTY); + int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY); if (curLoyalty < bestScore) { bestScore = curLoyalty; @@ -515,7 +515,7 @@ public class DamageDealAi extends DamageAiBase { *

* damageTargetAI. *

- * + * * @param saMe * a {@link forge.game.spellability.SpellAbility} object. * @param dmg @@ -543,7 +543,7 @@ public class DamageDealAi extends DamageAiBase { *

* damageChoosingTargets. *

- * + * * @param sa * a {@link forge.game.spellability.SpellAbility} object. * @param tgt @@ -587,7 +587,7 @@ public class DamageDealAi extends DamageAiBase { if (tgt.getMaxTargets(source, sa) <= 0 && !logic.equals("AssumeAtLeastOneTarget")) { return false; } - + immediately |= ComputerUtil.playImmediately(ai, sa); if (!(sa.getParent() != null && sa.getParent().isTargetNumberValid())) { @@ -623,7 +623,7 @@ public class DamageDealAi extends DamageAiBase { continue; } final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(humanCreature, dmg, source, false, noPrevention); - if (assignedDamage <= dmg + if (assignedDamage <= dmg && humanCreature.getShieldCount() == 0 && !ComputerUtil.canRegenerate(humanCreature.getController(), humanCreature)) { tcs.add(humanCreature); tgt.addDividedAllocation(humanCreature, assignedDamage); @@ -756,7 +756,7 @@ public class DamageDealAi extends DamageAiBase { break; } } - + } else if (tgt.canTgtCreature() || tgt.canTgtPlaneswalker()) { final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, enemy, mandatory); if (c != null) { @@ -794,8 +794,7 @@ public class DamageDealAi extends DamageAiBase { if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai)) || (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2)) || ("PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai)) - || sa.getPayCosts() == null || immediately - || this.shouldTgtP(ai, sa, dmg, noPrevention)) && + || immediately || shouldTgtP(ai, sa, dmg, noPrevention)) && (!avoidTargetP(ai, sa))) { tcs.add(enemy); if (divided) { @@ -826,8 +825,8 @@ public class DamageDealAi extends DamageAiBase { *

* damageChooseNontargeted. *

- * @param ai - * + * @param ai + * * @param saMe * a {@link forge.game.spellability.SpellAbility} object. * @param dmg @@ -882,7 +881,7 @@ public class DamageDealAi extends DamageAiBase { *

* damageChooseRequiredTargets. *

- * + * * @param sa * a {@link forge.game.spellability.SpellAbility} object. * @param tgt @@ -1007,7 +1006,7 @@ public class DamageDealAi extends DamageAiBase { // If I can kill my target by paying less mana, do it int actualPay = 0; final boolean noPrevention = sa.hasParam("NoPrevention"); - + //target is a player if (!sa.getTargets().isTargetingAnyCard()) { actualPay = dmg; @@ -1038,15 +1037,15 @@ public class DamageDealAi extends DamageAiBase { Player opponent = ai.getOpponents().min(PlayerPredicates.compareByLife()); - // TODO: somehow account for the possible cost reduction? + // TODO: somehow account for the possible cost reduction? int dmg = ComputerUtilMana.determineLeftoverMana(sa, ai, saTgt.getParam("XColor")); - + while (!ComputerUtilMana.canPayManaCost(sa, ai, dmg) && dmg > 0) { // TODO: ideally should never get here, currently put here as a precaution for complex mana base cases where the miscalculation might occur. Will remove later if it proves to never trigger. dmg--; System.out.println("Warning: AI could not pay mana cost for a XLifeDrain logic spell. Reducing X value to "+dmg); } - + // set the color map for black X for the purpose of Soul Burn // TODO: somehow generalize this calculation to allow other potential similar cards to function in the future if ("Soul Burn".equals(sourceName)) { @@ -1067,7 +1066,7 @@ public class DamageDealAi extends DamageAiBase { int toughness = c.getNetToughness(); boolean canDie = !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(c.getController(), c)); - // Currently will target creatures with toughness 3+ (or power 5+) + // Currently will target creatures with toughness 3+ (or power 5+) // and only if the creature can actually die, do not "underdrain" // unless the creature has high power if (canDie && toughness <= dmg && ((toughness == dmg && toughness >= 3) || power >= 5)) { @@ -1126,8 +1125,7 @@ public class DamageDealAi extends DamageAiBase { continue; } // currently works only with cards that don't have additional costs (only mana is supported) - if (ab.getPayCosts() != null - && (ab.getPayCosts().hasNoManaCost() || ab.getPayCosts().hasOnlySpecificCostType(CostPartMana.class))) { + if (ab.getPayCosts().hasNoManaCost() || ab.getPayCosts().hasOnlySpecificCostType(CostPartMana.class)) { String dmgDef = "0"; if (ab.getApi() == ApiType.DealDamage) { dmgDef = ab.getParamOrDefault("NumDmg", "0"); @@ -1151,7 +1149,7 @@ public class DamageDealAi extends DamageAiBase { } // FIXME: should it also check restrictions for targeting players? - ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST; + ManaCost costSa = sa.getPayCosts().getTotalMana(); ManaCost costAb = ab.getPayCosts().getTotalMana(); // checked for null above ManaCost total = ManaCost.combine(costSa, costAb); SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, 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 fd7632bc44d..8c3855385e6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -101,7 +101,7 @@ public class DestroyAi extends SpellAbilityAi { return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa); } else if (logic != null && logic.startsWith("MinLoyalty.")) { int minLoyalty = Integer.parseInt(logic.substring(logic.indexOf(".") + 1)); - if (source.getCounters(CounterType.LOYALTY) < minLoyalty) { + if (source.getCounters(CounterEnumType.LOYALTY) < minLoyalty) { return false; } } else if ("Polymorph".equals(logic)) { @@ -161,7 +161,7 @@ public class DestroyAi extends SpellAbilityAi { return false; } //Check for undying - return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterType.P1P1) > 0); + return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterEnumType.P1P1) > 0); } }); } @@ -231,7 +231,7 @@ public class DestroyAi extends SpellAbilityAi { } if ("Pongify".equals(logic)) { final Card token = TokenAi.spawnToken(choice.getController(), sa.getSubAbility()); - if (token == null) { + if (token == null || !token.isCreature() || token.getNetToughness() < 1) { return true; // becomes Terminate } else { if (source.getGame().getPhaseHandler().getPhase() @@ -256,7 +256,7 @@ public class DestroyAi extends SpellAbilityAi { } //option to hold removal instead only applies for single targeted removal if (!sa.isTrigger() && abTgt.getMaxTargets(sa.getHostCard(), sa) == 1) { - if (!ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) { + if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) { return false; } } @@ -277,6 +277,7 @@ public class DestroyAi extends SpellAbilityAi { SpellAbility sp = aura.getFirstSpellAbility(); if (sp != null && "GainControl".equals(sp.getParam("AILogic")) && aura.getController() != ai && sa.canTarget(aura)) { + list.remove(choice); choice = aura; } } @@ -387,7 +388,7 @@ public class DestroyAi extends SpellAbilityAi { if (CardLists.getNotType(list, "Creature").isEmpty()) { if (!sa.getUniqueTargets().isEmpty() && sa.getParent().getApi() == ApiType.Destroy && sa.getUniqueTargets().get(0) instanceof Card) { - // basic ai for Diaochan + // basic ai for Diaochan c = (Card) sa.getUniqueTargets().get(0); } else { c = ComputerUtilCard.getWorstCreatureAI(list); @@ -412,7 +413,7 @@ public class DestroyAi extends SpellAbilityAi { Player tgtPlayer = tgtLand.getController(); int oppLandsOTB = tgtPlayer.getLandsInPlay().size(); - + // AI profile-dependent properties AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); int amountNoTempoCheck = aic.getIntProperty(AiProps.STRIPMINE_MIN_LANDS_OTB_FOR_NO_TEMPO_CHECK); @@ -435,7 +436,7 @@ public class DestroyAi extends SpellAbilityAi { // Non-basic lands are currently not ranked in any way in ComputerUtilCard#getBestLandAI, so if a non-basic land is best target, // consider killing it off unless there's too much potential tempo loss. - // TODO: actually rank non-basics in that method and then kill off the potentially dangerous (manlands, Valakut) or lucrative + // TODO: actually rank non-basics in that method and then kill off the potentially dangerous (manlands, Valakut) or lucrative // (dual/triple mana that opens access to a certain color) lands boolean nonBasicTgt = !tgtLand.isBasicLand(); @@ -447,7 +448,7 @@ public class DestroyAi extends SpellAbilityAi { boolean isHighPriority = highPriorityIfNoLandDrop && oppSkippedLandDrop; boolean timingCheck = canManaLock || canColorLock || nonBasicTgt; - boolean tempoCheck = numLandsOTB >= amountNoTempoCheck + boolean tempoCheck = numLandsOTB >= amountNoTempoCheck || ((numLandsInHand >= amountLandsInHand || isHighPriority) && ((numLandsInHand + numLandsOTB >= amountNoTimingCheck) || timingCheck)); // For Ghost Quarter, only use it if you have either more lands in play than your opponent diff --git a/forge-ai/src/main/java/forge/ai/ability/DigAi.java b/forge-ai/src/main/java/forge/ai/ability/DigAi.java index b8b13d18fd3..7099250d925 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.base.Predicate; import com.google.common.collect.Iterables; @@ -132,7 +134,7 @@ public class DigAi extends SpellAbilityAi { } @Override - public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable valid, boolean isOptional, Player relatedPlayer) { + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable valid, boolean isOptional, Player relatedPlayer, Map params) { if ("DigForCreature".equals(sa.getParam("AILogic"))) { Card bestChoice = ComputerUtilCard.getBestCreatureAI(valid); if (bestChoice == null) { @@ -163,7 +165,7 @@ public class DigAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#chooseSinglePlayer(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List) */ @Override - public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { + public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { // an opponent choose a card from return Iterables.getFirst(options, null); } diff --git a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java index 59438a2fb52..4047d12ee77 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java @@ -25,6 +25,7 @@ import forge.game.ability.ApiType; import forge.game.card.Card; import forge.game.card.CardLists; import forge.game.card.CardPredicates; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.cost.*; import forge.game.phase.PhaseHandler; @@ -262,22 +263,20 @@ public class DrawAi extends SpellAbilityAi { // Draw up to max hand size but leave at least 3 in library numCards = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3); - if (sa.getPayCosts() != null) { - if (sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) { - // [Necrologia, Pay X Life : Draw X Cards] - // Don't draw more than what's "safe" and don't risk a near death experience - // Maybe would be better to check for "serious danger" and take more risk? - while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) { - numCards--; - } - } else if (sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { - // [e.g. Krav, the Unredeemed and other cases which say "Sacrifice X creatures: draw X cards] - // TODO: Add special logic to limit/otherwise modify the ChosenX value here + if (sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) { + // [Necrologia, Pay X Life : Draw X Cards] + // Don't draw more than what's "safe" and don't risk a near death experience + // Maybe would be better to check for "serious danger" and take more risk? + while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) { + numCards--; + } + } else if (sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { + // [e.g. Krav, the Unredeemed and other cases which say "Sacrifice X creatures: draw X cards] + // TODO: Add special logic to limit/otherwise modify the ChosenX value here - // Skip this ability if nothing is to be chosen for sacrifice - if (numCards <= 0) { - return false; - } + // Skip this ability if nothing is to be chosen for sacrifice + if (numCards <= 0) { + return false; } } @@ -350,7 +349,7 @@ public class DrawAi extends SpellAbilityAi { } // try to make opponent lose to poison // currently only Caress of Phyrexia - if (getPoison != null && oppA.canReceiveCounters(CounterType.POISON)) { + if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { if (oppA.getPoisonCounters() + numCards > 9) { sa.getTargets().add(oppA); return true; @@ -394,7 +393,7 @@ public class DrawAi extends SpellAbilityAi { } } - if (getPoison != null && ai.canReceiveCounters(CounterType.POISON)) { + if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { if (numCards + ai.getPoisonCounters() >= 8) { aiTarget = false; } @@ -453,7 +452,7 @@ public class DrawAi extends SpellAbilityAi { } // ally would lose because of poison - if (getPoison != null && ally.canReceiveCounters(CounterType.POISON)) { + if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { if (ally.getPoisonCounters() + numCards > 9) { continue; } diff --git a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java index 609aec7c92d..77a20350e69 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -113,7 +113,7 @@ public class EffectAi extends SpellAbilityAi { } else if (logic.equals("SpellCopy")) { // fetch Instant or Sorcery and AI has reason to play this turn // does not try to get itself - final ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST; + final ManaCost costSa = sa.getPayCosts().getTotalMana(); final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate() { @Override public boolean apply(final Card c) { @@ -135,7 +135,7 @@ public class EffectAi extends SpellAbilityAi { AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(ab); // see if we can pay both for this spell and for the Effect spell we're considering if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) { - ManaCost costAb = ab.getPayCosts() != null ? ab.getPayCosts().getTotalMana() : ManaCost.NO_COST; + ManaCost costAb = ab.getPayCosts().getTotalMana(); ManaCost total = ManaCost.combine(costSa, costAb); SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false)); // can we pay both costs? diff --git a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java index fcfe5c957ba..53587f59cd6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java @@ -18,6 +18,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import com.google.common.base.Predicate; @@ -84,7 +85,7 @@ public final class EncodeAi extends SpellAbilityAi { * forge.game.player.Player) */ @Override - public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { return chooseCard(ai, options, isOptional); } diff --git a/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java b/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java new file mode 100644 index 00000000000..1ae9da74c7b --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java @@ -0,0 +1,27 @@ +package forge.ai.ability; + + +import forge.ai.SpellAbilityAi; +import forge.game.phase.PhaseHandler; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.player.PlayerActionConfirmMode; +import forge.game.spellability.SpellAbility; + +public class InvestigateAi extends SpellAbilityAi { + /* (non-Javadoc) + * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) + */ + @Override + protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); + + return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer; + } + + @Override + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + return true; + } +} + diff --git a/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java b/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java index 87e9d2d9464..4ad913b7a25 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java @@ -1,9 +1,11 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.collect.Iterables; import forge.ai.SpellAbilityAi; import forge.game.card.Card; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -23,7 +25,7 @@ public class LegendaryRuleAi extends SpellAbilityAi { @Override - public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { // Choose a single legendary/planeswalker card to keep Card firstOption = Iterables.getFirst(options, null); boolean choosingFromPlanewalkers = firstOption.isPlaneswalker(); @@ -38,16 +40,16 @@ public class LegendaryRuleAi extends SpellAbilityAi { if (firstOption.getName().equals("Dark Depths")) { Card best = firstOption; for (Card c : options) { - if (c.getCounters(CounterType.ICE) < best.getCounters(CounterType.ICE)) { + if (c.getCounters(CounterEnumType.ICE) < best.getCounters(CounterEnumType.ICE)) { best = c; } } return best; - } else if (firstOption.getCounters(CounterType.KI) > 0) { + } else if (firstOption.getCounters(CounterEnumType.KI) > 0) { // Extra Rule for KI counter Card best = firstOption; for (Card c : options) { - if (c.getCounters(CounterType.KI) > best.getCounters(CounterType.KI)) { + if (c.getCounters(CounterEnumType.KI) > best.getCounters(CounterEnumType.KI)) { best = c; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java index 41751f85882..5ad2c283477 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java @@ -88,7 +88,6 @@ public class LifeGainAi extends SpellAbilityAi { if (lifeCritical && sa.isAbility() && sa.getHostCard() != null && sa.getHostCard().isCreature() - && sa.getPayCosts() != null && (sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class) || sa.getPayCosts().hasSpecificCostType(CostSacrifice.class))) { if (!game.getStack().isEmpty()) { SpellAbility saTop = game.getStack().peekAbility(); diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java index cc4d851bd9b..2afc4b47137 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java @@ -5,7 +5,7 @@ import forge.ai.ComputerUtilMana; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -130,7 +130,7 @@ public class LifeSetAi extends SpellAbilityAi { } if (sourceName.equals("Eternity Vessel") - && (opponent.isCardInPlay("Vampire Hexmage") || (source.getCounters(CounterType.CHARGE) == 0))) { + && (opponent.isCardInPlay("Vampire Hexmage") || (source.getCounters(CounterEnumType.CHARGE) == 0))) { return false; } 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 60eaeef180f..80f7b916f11 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java @@ -81,7 +81,7 @@ public class ManaEffectAi extends SpellAbilityAi { return true; // handled elsewhere, does not meet the standard requirements } - return sa.getPayCosts() != null && sa.getPayCosts().hasNoManaCost() && sa.getPayCosts().isReusuableResource() + return sa.getPayCosts().hasNoManaCost() && sa.getPayCosts().isReusuableResource() && sa.getSubAbility() == null && ComputerUtil.playImmediately(ai, sa); // return super.checkApiLogic(ai, sa); } @@ -119,8 +119,8 @@ public class ManaEffectAi extends SpellAbilityAi { int numCounters = 0; int manaSurplus = 0; if ("XChoice".equals(host.getSVar("X")) - && sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) { - CounterType ctrType = CounterType.KI; // Petalmane Baku + && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) { + CounterType ctrType = CounterType.get(CounterEnumType.KI); // Petalmane Baku for (CostPart part : sa.getPayCosts().getCostParts()) { if (part instanceof CostRemoveCounter) { ctrType = ((CostRemoveCounter)part).counter; diff --git a/forge-ai/src/main/java/forge/ai/ability/MillAi.java b/forge-ai/src/main/java/forge/ai/ability/MillAi.java index fba432c9d48..52f0e8cda25 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MillAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MillAi.java @@ -1,15 +1,10 @@ package forge.ai.ability; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - import com.google.common.collect.Lists; import com.google.common.collect.Maps; - import forge.ai.ComputerUtil; import forge.ai.ComputerUtilMana; +import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -24,6 +19,11 @@ import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + public class MillAi extends SpellAbilityAi { @Override @@ -196,6 +196,10 @@ public class MillAi extends SpellAbilityAi { */ @Override public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + if ("TimmerianFiends".equals(sa.getParam("AILogic"))) { + return SpecialCardAi.TimmerianFiends.consider(player, sa); + } + return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java b/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java index 925f8ce02a1..fe921f05e72 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java @@ -18,6 +18,7 @@ import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import java.util.List; +import java.util.Map; public class MustBlockAi extends SpellAbilityAi { @@ -167,7 +168,7 @@ public class MustBlockAi extends SpellAbilityAi { @Override protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, - Player targetedPlayer) { + Player targetedPlayer, Map params) { final Card host = sa.getHostCard(); Card attacker = host; diff --git a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java index 96c22f1d519..7168f4c911e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java @@ -20,6 +20,7 @@ import forge.game.zone.ZoneType; import forge.util.MyRandom; import java.util.List; +import java.util.Map; public class PlayAi extends SpellAbilityAi { @@ -84,11 +85,11 @@ public class PlayAi extends SpellAbilityAi { return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost")); } else if (logic.startsWith("NeedsChosenCard")) { int minCMC = 0; - if (sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null) { - minCMC = sa.getPayCosts().getCostMana().getMana().getCMC(); + if (sa.getPayCosts().getCostMana() != null) { + minCMC = sa.getPayCosts().getTotalMana().getCMC(); } validOpts = CardLists.filter(validOpts, CardPredicates.greaterCMC(minCMC)); - return chooseSingleCard(ai, sa, validOpts, sa.hasParam("Optional"), null) != null; + return chooseSingleCard(ai, sa, validOpts, sa.hasParam("Optional"), null, null) != null; } if (source != null && source.hasKeyword(Keyword.HIDEAWAY) && source.hasRemembered()) { @@ -142,8 +143,7 @@ public class PlayAi extends SpellAbilityAi { */ @Override public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, - final boolean isOptional, - Player targetedPlayer) { + final boolean isOptional, Player targetedPlayer, Map params) { List tgtCards = CardLists.filter(options, new Predicate() { @Override public boolean apply(final Card c) { @@ -156,9 +156,7 @@ public class PlayAi extends SpellAbilityAi { if (sa.hasParam("WithoutManaCost")) { // Try to avoid casting instants and sorceries with X in their cost, since X will be assumed to be 0. if (!(spell instanceof SpellPermanent)) { - if (spell.getPayCosts() != null - && spell.getPayCosts().getCostMana() != null - && spell.getPayCosts().getCostMana().getMana().countX() > 0) { + if (spell.getPayCosts().getTotalMana().countX() > 0) { continue; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java index 0eba3082f50..5761c79832c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java @@ -5,6 +5,7 @@ import com.google.common.base.Predicate; import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -59,7 +60,7 @@ public class PoisonAi extends SpellAbilityAi { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { return tgtPlayer(ai, sa, mandatory); - } else if (mandatory || !ai.canReceiveCounters(CounterType.POISON)) { + } else if (mandatory || !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { // mandatory or ai is uneffected return true; } else { @@ -92,7 +93,7 @@ public class PoisonAi extends SpellAbilityAi { public boolean apply(Player input) { if (input.cantLose()) { return false; - } else if (!input.canReceiveCounters(CounterType.POISON)) { + } else if (!input.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { return false; } return true; @@ -113,7 +114,7 @@ public class PoisonAi extends SpellAbilityAi { if (tgts.isEmpty()) { if (mandatory) { // AI is uneffected - if (ai.canBeTargetedBy(sa) && ai.canReceiveCounters(CounterType.POISON)) { + if (ai.canBeTargetedBy(sa) && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { sa.getTargets().add(ai); return true; } @@ -127,7 +128,7 @@ public class PoisonAi extends SpellAbilityAi { if (input.cantLose()) { return true; } - return !input.canReceiveCounters(CounterType.POISON); + return !input.canReceiveCounters(CounterType.get(CounterEnumType.POISON)); } }); diff --git a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java index 310fa6e32af..79e0f5bcbc7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java @@ -202,7 +202,7 @@ public class ProtectAi extends SpellAbilityAi { if (game.getStack().isEmpty()) { // If the cost is tapping, don't activate before declare // attack/block - if ((sa.getPayCosts() != null) && sa.getPayCosts().hasTapCost()) { + if (sa.getPayCosts().hasTapCost()) { if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && game.getPhaseHandler().isPlayerTurn(ai)) { list.remove(sa.getHostCard()); diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java index ce161caee80..ced79a57318 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -150,7 +150,7 @@ public class PumpAi extends PumpAiBase { } final String counterType = moveSA.getParam("CounterType"); - final CounterType cType = "Any".equals(counterType) ? null : CounterType.valueOf(counterType); + final CounterType cType = "Any".equals(counterType) ? null : CounterType.getType(counterType); final PhaseHandler ph = game.getPhaseHandler(); if (ph.inCombat() && ph.getPlayerTurn().isOpponentOf(ai)) { @@ -185,7 +185,7 @@ public class PumpAi extends PumpAiBase { // cant use substract on Copy srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount); - if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { + if (CounterEnumType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken(); } @@ -235,7 +235,7 @@ public class PumpAi extends PumpAiBase { // cant use substract on Copy srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount); - if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { + if (CounterEnumType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) { return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken(); } @@ -402,7 +402,7 @@ public class PumpAi extends PumpAiBase { if ("DebuffForXCounters".equals(sa.getParam("AILogic")) && sa.getTargetCard() != null) { // e.g. Skullmane Baku - CounterType ctrType = CounterType.KI; + CounterType ctrType = CounterType.get(CounterEnumType.KI); for (CostPart part : sa.getPayCosts().getCostParts()) { if (part instanceof CostRemoveCounter) { ctrType = ((CostRemoveCounter)part).counter; @@ -515,7 +515,7 @@ public class PumpAi extends PumpAiBase { if (game.getStack().isEmpty()) { // If the cost is tapping, don't activate before declare // attack/block - if (sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.getPayCosts().hasTapCost()) { if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && game.getPhaseHandler().isPlayerTurn(ai)) { list.remove(sa.getHostCard()); @@ -730,7 +730,7 @@ public class PumpAi extends PumpAiBase { final String numAttack = sa.hasParam("NumAtt") ? sa.getParam("NumAtt") : ""; if (numDefense.equals("-X") && sa.getSVar("X").equals("Count$ChosenNumber")) { - int energy = ai.getCounters(CounterType.ENERGY); + int energy = ai.getCounters(CounterEnumType.ENERGY); for (SpellAbility s : source.getSpellAbilities()) { if ("PayEnergy".equals(s.getParam("AILogic"))) { energy += AbilityUtils.calculateAmount(source, s.getParam("CounterNum"), sa); @@ -860,7 +860,7 @@ public class PumpAi extends PumpAiBase { final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); - if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) { + if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent } @@ -976,7 +976,7 @@ public class PumpAi extends PumpAiBase { final boolean isInfect = source.hasKeyword(Keyword.INFECT); int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); - if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) { + if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent } 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 eb2ab18e1d1..aba764dbe89 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java @@ -94,7 +94,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { List attackers = CardLists.filter(ai.getCreaturesInPlay(), new Predicate() { @Override public boolean apply(final Card c) { - if (c.equals(sa.getHostCard()) && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost() + if (c.equals(sa.getHostCard()) && sa.getPayCosts().hasTapCost() && (combat == null || !combat.isAttacking(c))) { return false; } @@ -112,7 +112,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { List attackers = CardLists.filter(ai.getCreaturesInPlay(), new Predicate() { @Override public boolean apply(final Card c) { - if (c.equals(sa.getHostCard()) && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost() + if (c.equals(sa.getHostCard()) && sa.getPayCosts().hasTapCost() && (combat == null || !combat.isAttacking(c))) { return false; } 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 e8092eccfa2..4de6ff75187 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java @@ -26,7 +26,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi { final PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); final Card source = sa.getHostCard(); - if (source.isPermanent() && sa.getRestrictions().isInstantSpeed() && sa.getPayCosts() != null + if (source.isPermanent() && sa.getRestrictions().isInstantSpeed() && (sa.getPayCosts().hasTapCost() || sa.getPayCosts().hasManaCost())) { // If it has an associated cost, try to only do this before own turn if (!(ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java index 8ba1494b666..fcbb622caf3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java @@ -1,6 +1,5 @@ package forge.ai.ability; -import com.google.common.base.Predicate; import forge.ai.ComputerUtilCard; import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; @@ -16,6 +15,7 @@ import forge.game.zone.ZoneType; import forge.util.TextUtil; import java.util.List; +import java.util.Map; public class RepeatEachAi extends SpellAbilityAi { @@ -47,21 +47,6 @@ public class RepeatEachAi extends SpellAbilityAi { return false; } } - } else if ("GainControlOwns".equals(logic)) { - List list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), new Predicate() { - @Override - public boolean apply(final Card crd) { - return crd.isCreature() && !crd.getController().equals(crd.getOwner()); - } - }); - if (list.isEmpty()) { - return false; - } - for (final Card c : list) { - if (aiPlayer.equals(c.getController())) { - return false; - } - } } else if ("OpponentHasCreatures".equals(logic)) { for (Player opp : aiPlayer.getOpponents()) { if (!opp.getCreaturesInPlay().isEmpty()){ @@ -118,7 +103,7 @@ public class RepeatEachAi extends SpellAbilityAi { } @Override - protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) { + protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { return ComputerUtilCard.getBestCreatureAI(options); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java index cf01c1d8026..2ec63fb82ac 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java @@ -46,12 +46,10 @@ public class ScryAi extends SpellAbilityAi { // and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to // try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible, // even if there's no mana cost. - if (sa.getPayCosts() != null) { - if (sa.getPayCosts().hasTapCost() - && (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature())) - && !SpellAbilityAi.isSorcerySpeed(sa)) { - return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN); - } + if (sa.getPayCosts().hasTapCost() + && (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature())) + && !SpellAbilityAi.isSorcerySpeed(sa)) { + return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN); } // AI logic to scry in Main 1 if there is no better option, otherwise scry at opponent's EOT @@ -76,8 +74,7 @@ public class ScryAi extends SpellAbilityAi { boolean hasSomethingElse = false; for (Card c : CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS))) { for (SpellAbility ab : c.getAllSpellAbilities()) { - if (ab.getPayCosts() != null - && ab.getPayCosts().hasManaCost() + if (ab.getPayCosts().hasManaCost() && ComputerUtilMana.hasEnoughManaSourcesToCast(ab, ai)) { // TODO: currently looks for non-Scry cards, can most certainly be made smarter. if (ab.getApi() != ApiType.Scry) { @@ -102,7 +99,7 @@ public class ScryAi extends SpellAbilityAi { } else if ("BrainJar".equals(aiLogic)) { final Card source = sa.getHostCard(); - int counterNum = source.getCounters(CounterType.CHARGE); + int counterNum = source.getCounters(CounterEnumType.CHARGE); // no need for logic if (counterNum == 0) { return false; diff --git a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java index 7e03c1cf0f5..ca7404dee76 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java @@ -248,7 +248,7 @@ public class SetStateAi extends SpellAbilityAi { final Card othercard = aiPlayer.getCardsIn(ZoneType.Battlefield, other.getName()).getFirst(); // for legendary KI counter creatures - if (othercard.getCounters(CounterType.KI) >= source.getCounters(CounterType.KI)) { + if (othercard.getCounters(CounterEnumType.KI) >= source.getCounters(CounterEnumType.KI)) { // if the other legendary is useless try to replace it return ComputerUtilCard.isUselessCreature(aiPlayer, othercard); } diff --git a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java index eb2beab480b..fb5e6efa0f7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java @@ -47,12 +47,10 @@ public class SurveilAi extends SpellAbilityAi { // and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to // try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible, // even if there's no mana cost. - if (sa.getPayCosts() != null) { - if (sa.getPayCosts().hasTapCost() - && (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature())) - && !SpellAbilityAi.isSorcerySpeed(sa)) { - return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN); - } + if (sa.getPayCosts().hasTapCost() + && (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature())) + && !SpellAbilityAi.isSorcerySpeed(sa)) { + return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN); } // in the player's turn Surveil should only be done in Main1 or in Upkeep if able diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAi.java index a4eb933dbe4..54da422ee87 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAi.java @@ -3,6 +3,7 @@ package forge.ai.ability; import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.card.Card; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.cost.Cost; import forge.game.cost.CostPart; @@ -68,7 +69,7 @@ public class TapAi extends TapAiBase { } else { if ("TapForXCounters".equals(sa.getParam("AILogic"))) { // e.g. Waxmane Baku - CounterType ctrType = CounterType.KI; + CounterType ctrType = CounterType.get(CounterEnumType.KI); for (CostPart part : sa.getPayCosts().getCostParts()) { if (part instanceof CostRemoveCounter) { ctrType = ((CostRemoveCounter)part).counter; 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 6a408821cbe..34fbe8f5c8b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java @@ -126,7 +126,7 @@ public abstract class TapAiBase extends SpellAbilityAi { } for (final SpellAbility sa : c.getSpellAbilities()) { - if (sa.isAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.isAbility() && sa.getPayCosts().hasTapCost()) { return true; } } @@ -147,7 +147,7 @@ public abstract class TapAiBase extends SpellAbilityAi { } for (final SpellAbility sa : c.getSpellAbilities()) { - if (sa.isAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (sa.isAbility() && sa.getPayCosts().hasTapCost()) { return true; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java index a5c3a57fad9..f6a62f567ef 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java @@ -1,11 +1,12 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import forge.ai.*; import forge.game.Game; import forge.game.GameEntity; -import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.Card; @@ -25,16 +26,9 @@ import forge.game.player.PlayerActionConfirmMode; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; -import forge.game.trigger.Trigger; -import forge.game.trigger.TriggerHandler; import forge.game.zone.ZoneType; -import forge.item.PaperToken; -import forge.util.MyRandom; -import forge.util.TextUtil; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import forge.util.MyRandom; /** *

@@ -45,35 +39,10 @@ import java.util.List; * @version $Id: AbilityFactoryToken.java 17656 2012-10-22 19:32:56Z Max mtg $ */ public class TokenAi extends SpellAbilityAi { - private String tokenAmount; - private String tokenPower; - private String tokenToughness; - private Card actualToken; - /** - *

- * Constructor for AbilityFactory_Token. - *

- * - * a {@link forge.game.ability.AbilityFactory} object. - */ - private void readParameters(final SpellAbility mapParams) { - this.tokenAmount = mapParams.getParamOrDefault("TokenAmount", "1"); - - this.actualToken = TokenInfo.getProtoType(mapParams.getParam("TokenScript"), mapParams); - - if (actualToken == null) { - this.tokenPower = mapParams.getParam("TokenPower"); - this.tokenToughness = mapParams.getParam("TokenToughness"); - } else { - this.tokenPower = actualToken.getBasePowerString(); - this.tokenToughness = actualToken.getBaseToughnessString(); - } - } - @Override protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { - readParameters(sa); // remember to call this somewhere! + final Card source = sa.getHostCard(); // Planeswalker-related flags boolean pwMinus = false; @@ -96,21 +65,23 @@ public class TokenAi extends SpellAbilityAi { } } } + String tokenAmount = sa.getParamOrDefault("TokenAmount", "1"); - if (actualToken == null) { - actualToken = spawnToken(ai, sa); - } + Card actualToken = spawnToken(ai, sa); - if (actualToken == null) { + if (actualToken == null || actualToken.getNetToughness() < 1) { final AbilitySub sub = sa.getSubAbility(); // useful // no token created return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub.getApi()).chkAIDrawback(sub, ai)); // planeswalker plus ability or sub-ability is } + String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString()); + String tokenToughness = sa.getParamOrDefault("TokenToughness", actualToken.getBaseToughnessString()); + // X-cost spells - if (this.tokenAmount.equals("X") || (this.tokenToughness != null && this.tokenToughness.equals("X"))) { - int x = AbilityUtils.calculateAmount(sa.getHostCard(), this.tokenAmount, sa); + if ("X".equals(tokenAmount) || "X".equals(tokenPower) || "X".equals(tokenToughness)) { + int x = AbilityUtils.calculateAmount(sa.getHostCard(), tokenAmount, sa); if (source.getSVar("X").equals("Count$Converge")) { x = ComputerUtilMana.getConvergeCount(sa, ai); } @@ -124,14 +95,14 @@ public class TokenAi extends SpellAbilityAi { } } - if (canInterruptSacrifice(ai, sa, actualToken)) { + if (canInterruptSacrifice(ai, sa, actualToken, tokenAmount)) { return true; } - boolean haste = this.actualToken.hasKeyword(Keyword.HASTE); + boolean haste = actualToken.hasKeyword(Keyword.HASTE); boolean oneShot = sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.DelayedTrigger; - boolean isCreature = this.actualToken.getType().isCreature(); + boolean isCreature = actualToken.getType().isCreature(); // Don't generate tokens without haste before main 2 if possible if (ph.getPhase().isBefore(PhaseType.MAIN2) && ph.isPlayerTurn(ai) && !haste && !sa.hasParam("ActivationPhases") @@ -166,9 +137,10 @@ public class TokenAi extends SpellAbilityAi { if (ComputerUtil.preventRunAwayActivations(sa)) { return false; // prevent infinite tokens? } + Card actualToken = spawnToken(ai, sa); // Don't kill AIs Legendary tokens - if (this.actualToken.getType().isLegendary() && ai.isCardInPlay(this.actualToken.getName())) { + if (actualToken.getType().isLegendary() && ai.isCardInPlay(actualToken.getName())) { // TODO Check if Token is useless due to an aura or counters? return false; } @@ -240,7 +212,7 @@ public class TokenAi extends SpellAbilityAi { /** * Checks if the token(s) can save a creature from a sacrifice effect */ - private boolean canInterruptSacrifice(final Player ai, final SpellAbility sa, final Card token) { + private boolean canInterruptSacrifice(final Player ai, final SpellAbility sa, final Card token, final String tokenAmount) { final Game game = ai.getGame(); if (game.getStack().isEmpty()) { return false; // nothing to interrupt @@ -249,7 +221,7 @@ public class TokenAi extends SpellAbilityAi { if (topStack.getApi() != ApiType.Sacrifice) { return false; // not sacrifice effect } - final int nTokens = AbilityUtils.calculateAmount(sa.getHostCard(), this.tokenAmount, sa); + final int nTokens = AbilityUtils.calculateAmount(sa.getHostCard(), tokenAmount, sa); final String valid = topStack.getParamOrDefault("SacValid", "Card.Self"); String num = sa.getParam("Amount"); num = (num == null) ? "1" : num; @@ -271,7 +243,8 @@ public class TokenAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - readParameters(sa); + String tokenAmount = sa.getParamOrDefault("TokenAmount", "1"); + final Card source = sa.getHostCard(); final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { @@ -282,8 +255,12 @@ public class TokenAi extends SpellAbilityAi { sa.getTargets().add(ai); } } - if ("X".equals(this.tokenAmount) || "X".equals(this.tokenPower) || "X".equals(this.tokenToughness)) { - int x = AbilityUtils.calculateAmount(source, this.tokenAmount, sa); + Card actualToken = spawnToken(ai, sa); + String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString()); + String tokenToughness = sa.getParamOrDefault("TokenToughness", actualToken.getBaseToughnessString()); + + if ("X".equals(tokenAmount) || "X".equals(tokenPower) || "X".equals(tokenToughness)) { + int x = AbilityUtils.calculateAmount(source, tokenAmount, sa); if (source.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. x = ComputerUtilMana.determineLeftoverMana(sa, ai); @@ -321,9 +298,7 @@ public class TokenAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#chooseSinglePlayer(forge.game.player.Player, forge.card.spellability.SpellAbility, Iterable options) */ @Override - protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) { - // TODO: AILogic - readParameters(sa); // remember to call this somewhere! + protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { Combat combat = ai.getGame().getCombat(); // TokenAttacking if (combat != null && sa.hasParam("TokenAttacking")) { @@ -341,9 +316,7 @@ public class TokenAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#chooseSinglePlayerOrPlaneswalker(forge.game.player.Player, forge.card.spellability.SpellAbility, Iterable options) */ @Override - protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable options) { - // TODO: AILogic - readParameters(sa); // remember to call this somewhere! + protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable options, Map params) { Combat combat = ai.getGame().getCombat(); // TokenAttacking if (combat != null && sa.hasParam("TokenAttacking")) { @@ -374,154 +347,22 @@ public class TokenAi extends SpellAbilityAi { * @param sa Token SpellAbility * @return token creature created by ability */ - @Deprecated public static Card spawnToken(Player ai, SpellAbility sa) { - return spawnToken(ai, sa, false); - } - - /** - * Create the token as a Card object. - * @param ai owner of the new token - * @param sa Token SpellAbility - * @param notNull if the token would not survive, still return it - * @return token creature created by ability - */ - // TODO Is this just completely copied from TokenEffect? Let's just call that thing - @Deprecated - public static Card spawnToken(Player ai, SpellAbility sa, boolean notNull) { - final Card host = sa.getHostCard(); - + if (!sa.hasParam("TokenScript")) { + throw new RuntimeException("Spell Ability has no TokenScript: " + sa); + } Card result = TokenInfo.getProtoType(sa.getParam("TokenScript"), sa); - if (result != null) { - result.setController(ai, 0); - return result; + if (result == null) { + throw new RuntimeException("don't find Token for TokenScript: " + sa.getParam("TokenScript")); } - String[] tokenKeywords = sa.hasParam("TokenKeywords") ? sa.getParam("TokenKeywords").split("<>") : new String[0]; - String tokenPower = sa.getParam("TokenPower"); - String tokenToughness = sa.getParam("TokenToughness"); - String tokenName = sa.getParam("TokenName"); - String[] tokenTypes = sa.getParam("TokenTypes").split(","); - StringBuilder cost = new StringBuilder(); - String[] tokenColors = sa.getParam("TokenColors").split(","); - String tokenImage = sa.hasParam("TokenImage") ? PaperToken.makeTokenFileName(sa.getParam("TokenImage")) : ""; - String[] tokenAbilities = sa.hasParam("TokenAbilities") ? sa.getParam("TokenAbilities").split(",") : null; - String[] tokenTriggers = sa.hasParam("TokenTriggers") ? sa.getParam("TokenTriggers").split(",") : null; - String[] tokenSVars = sa.hasParam("TokenSVars") ? sa.getParam("TokenSVars").split(",") : null; - String[] tokenStaticAbilities = sa.hasParam("TokenStaticAbilities") ? sa.getParam("TokenStaticAbilities").split(",") : null; - String[] tokenHiddenKeywords = sa.hasParam("TokenHiddenKeywords") ? sa.getParam("TokenHiddenKeywords").split("&") : null; - final String[] substitutedColors = Arrays.copyOf(tokenColors, tokenColors.length); - for (int i = 0; i < substitutedColors.length; i++) { - if (substitutedColors[i].equals("ChosenColor")) { - // this currently only supports 1 chosen color - substitutedColors[i] = host.getChosenColor(); - } - } - StringBuilder colorDesc = new StringBuilder(); - for (final String col : substitutedColors) { - if (col.equalsIgnoreCase("White")) { - colorDesc.append("W "); - } else if (col.equalsIgnoreCase("Blue")) { - colorDesc.append("U "); - } else if (col.equalsIgnoreCase("Black")) { - colorDesc.append("B "); - } else if (col.equalsIgnoreCase("Red")) { - colorDesc.append("R "); - } else if (col.equalsIgnoreCase("Green")) { - colorDesc.append("G "); - } else if (col.equalsIgnoreCase("Colorless")) { - colorDesc = new StringBuilder("C"); - } - } - - final List imageNames = new ArrayList<>(1); - if (tokenImage.equals("")) { - imageNames.add(PaperToken.makeTokenFileName(TextUtil.fastReplace(colorDesc.toString(), " ", ""), tokenPower, tokenToughness, tokenName)); - } else { - imageNames.add(0, tokenImage); - } + result.setOwner(ai); - for (final char c : colorDesc.toString().toCharArray()) { - cost.append(c + ' '); - } - - cost = new StringBuilder(colorDesc.toString().replace('C', '1').trim()); - - final int finalPower = AbilityUtils.calculateAmount(host, tokenPower, sa); - final int finalToughness = AbilityUtils.calculateAmount(host, tokenToughness, sa); - - final String[] substitutedTypes = Arrays.copyOf(tokenTypes, tokenTypes.length); - for (int i = 0; i < substitutedTypes.length; i++) { - if (substitutedTypes[i].equals("ChosenType")) { - substitutedTypes[i] = host.getChosenType(); - } - } - final String substitutedName = tokenName.equals("ChosenType") ? host.getChosenType() : tokenName; - final String imageName = imageNames.get(MyRandom.getRandom().nextInt(imageNames.size())); - final TokenInfo tokenInfo = new TokenInfo(substitutedName, imageName, - cost.toString(), substitutedTypes, tokenKeywords, finalPower, finalToughness); - Card token = tokenInfo.makeOneToken(ai); - - if (token == null) { - return null; - } - - // Grant rule changes - if (tokenHiddenKeywords != null) { - for (final String s : tokenHiddenKeywords) { - token.addHiddenExtrinsicKeyword(s); - } - } - - // Grant abilities - if (tokenAbilities != null) { - for (final String s : tokenAbilities) { - final String actualAbility = host.getSVar(s); - final SpellAbility grantedAbility = AbilityFactory.getAbility(actualAbility, token); - token.addSpellAbility(grantedAbility); - } - } - - // Grant triggers - if (tokenTriggers != null) { - for (final String s : tokenTriggers) { - final String actualTrigger = host.getSVar(s); - final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, token, true); - final String ability = host.getSVar(parsedTrigger.getParam("Execute")); - parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(ability, token)); - token.addTrigger(parsedTrigger); - } - } - - // Grant SVars - if (tokenSVars != null) { - for (final String s : tokenSVars) { - String actualSVar = host.getSVar(s); - String name = s; - if (actualSVar.startsWith("SVar")) { - actualSVar = actualSVar.split("SVar:")[1]; - name = actualSVar.split(":")[0]; - actualSVar = actualSVar.split(":")[1]; - } - token.setSVar(name, actualSVar); - } - } - - // Grant static abilities - if (tokenStaticAbilities != null) { - for (final String s : tokenStaticAbilities) { - token.addStaticAbility(host.getSVar(s)); - } - } - - // Apply static abilities and prune dead tokens + // Apply static abilities final Game game = ai.getGame(); - ComputerUtilCard.applyStaticContPT(game, token, null); - if (!notNull && token.isCreature() && token.getNetToughness() < 1) { - return null; - } else { - return token; - } + ComputerUtilCard.applyStaticContPT(game, result, null); + return result; } + } 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 5e2659fdacc..be7eb4f2ae9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java @@ -24,6 +24,7 @@ import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; import java.util.List; +import java.util.Map; public class UntapAi extends SpellAbilityAi { @Override @@ -153,12 +154,11 @@ public class UntapAi extends SpellAbilityAi { // Try to avoid potential infinite recursion, // e.g. Kiora's Follower untapping another Kiora's Follower and repeating infinitely - if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostTap.class)) { + if (sa.getPayCosts().hasOnlySpecificCostType(CostTap.class)) { CardCollection toRemove = new CardCollection(); for (Card c : untapList) { for (SpellAbility ab : c.getAllSpellAbilities()) { if (ab.getApi() == ApiType.Untap - && ab.getPayCosts() != null && ab.getPayCosts().hasOnlySpecificCostType(CostTap.class) && ab.canTarget(source)) { toRemove.add(c); @@ -312,7 +312,7 @@ public class UntapAi extends SpellAbilityAi { } @Override - public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable list, boolean isOptional, Player targetedPlayer) { + public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable list, boolean isOptional, Player targetedPlayer, Map params) { PlayerCollection pl = new PlayerCollection(); pl.add(ai); pl.addAll(ai.getAllies()); diff --git a/forge-ai/src/main/java/forge/ai/ability/VoteAi.java b/forge-ai/src/main/java/forge/ai/ability/VoteAi.java index 58aab03ecbb..33d23dd3b22 100644 --- a/forge-ai/src/main/java/forge/ai/ability/VoteAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/VoteAi.java @@ -46,6 +46,12 @@ public class VoteAi extends SpellAbilityAi { @Override public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map params) { + if (params.containsKey("Voter")) { + Player p = (Player)params.get("Voter"); + if (p.isOpponentOf(player)) { + return min; + } + } if (sa.getActivatingPlayer().isOpponentOf(player)) { return min; } diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java index cd99a5a6d94..1fac17f367a 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java @@ -76,7 +76,7 @@ public class GameCopier { newPlayer.addSpellCastThisTurn(); for (int j = 0; j < origPlayer.getLandsPlayedThisTurn(); j++) newPlayer.addLandPlayedThisTurn(); - newPlayer.setCounters(Maps.newEnumMap(origPlayer.getCounters())); + newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters())); newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn()); newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn()); newPlayer.setPreventNextDamage(origPlayer.getPreventNextDamage()); @@ -125,8 +125,6 @@ public class GameCopier { } } } - origGame.validateSpabCache(); - newGame.validateSpabCache(); // Undo effects first before calculating them below, to avoid them applying twice. for (StaticEffect effect : origGame.getStaticEffects().getEffects()) { @@ -330,7 +328,7 @@ public class GameCopier { Map counters = c.getCounters(); if (!counters.isEmpty()) { - newCard.setCounters(Maps.newEnumMap(counters)); + newCard.setCounters(Maps.newHashMap(counters)); } if (c.getChosenPlayer() != null) { newCard.setChosenPlayer(playerMap.get(c.getChosenPlayer())); diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java index f2d0f01bf85..e3dc9e8a64c 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java @@ -3,7 +3,7 @@ package forge.ai.simulation; import forge.ai.CreatureEvaluator; import forge.game.Game; import forge.game.card.Card; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.zone.ZoneType; @@ -154,7 +154,7 @@ public class GameStateEvaluator { // e.g. a 5 CMC permanent results in 200, whereas a 5/5 creature is ~225 int value = 50 + 30 * c.getCMC(); if (c.isPlaneswalker()) { - value += 2 * c.getCounters(CounterType.LOYALTY); + value += 2 * c.getCounters(CounterEnumType.LOYALTY); } return value; } diff --git a/forge-core/pom.xml b/forge-core/pom.xml index 9c13b0d69aa..620b62074e2 100644 --- a/forge-core/pom.xml +++ b/forge-core/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.36-SNAPSHOT forge-core diff --git a/forge-core/src/main/java/forge/ImageKeys.java b/forge-core/src/main/java/forge/ImageKeys.java index e07f836566d..4ea30d2a17d 100644 --- a/forge-core/src/main/java/forge/ImageKeys.java +++ b/forge-core/src/main/java/forge/ImageKeys.java @@ -6,6 +6,8 @@ import forge.util.ImageUtil; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; + + import java.io.File; import java.util.HashMap; import java.util.Map; @@ -113,7 +115,17 @@ public final class ImageKeys { } //try fullborder... if (filename.contains(".full")) { - file = findFile(dir, TextUtil.fastReplace(filename, ".full", ".fullborder")); + String fullborderFile = TextUtil.fastReplace(filename, ".full", ".fullborder"); + file = findFile(dir, fullborderFile); + if (file != null) { return file; } + // if there's a 1st art variant try without it for .fullborder images + file = findFile(dir, TextUtil.fastReplace(fullborderFile, "1.fullborder", ".fullborder")); + if (file != null) { return file; } + // if there's an art variant try without it for .full images + file = findFile(dir, filename.replaceAll("[0-9].full]",".full")); + if (file != null) { return file; } + // if there's a 1st art variant try with it for .full images + file = findFile(dir, filename.replaceAll("[0-9]*.full", "1.full")); if (file != null) { return file; } } //if an image, like phenomenon or planes is missing .full in their filenames but you have an existing images that have .full/.fullborder diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java index 6b65424daab..af533330a5b 100644 --- a/forge-core/src/main/java/forge/StaticData.java +++ b/forge-core/src/main/java/forge/StaticData.java @@ -54,11 +54,11 @@ public class StaticData { private static StaticData lastInstance = null; - public StaticData(CardStorageReader cardReader, String editionFolder, String blockDataFolder) { - this(cardReader, null, editionFolder, blockDataFolder); + public StaticData(CardStorageReader cardReader, String editionFolder, String blockDataFolder, boolean enableUnknownCards) { + this(cardReader, null, editionFolder, blockDataFolder, enableUnknownCards); } - public StaticData(CardStorageReader cardReader, CardStorageReader tokenReader, String editionFolder, String blockDataFolder) { + public StaticData(CardStorageReader cardReader, CardStorageReader tokenReader, String editionFolder, String blockDataFolder, boolean enableUnknownCards) { this.cardReader = cardReader; this.tokenReader = tokenReader; this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder))); @@ -84,8 +84,8 @@ public class StaticData { variantCards = new CardDb(variantsCards, editions); //must initialize after establish field values for the sake of card image logic - commonCards.initialize(false, false); - variantCards.initialize(false, false); + commonCards.initialize(false, false, enableUnknownCards); + variantCards.initialize(false, false, enableUnknownCards); } { @@ -215,7 +215,7 @@ public class StaticData { public Predicate getStandardPredicate() { return standardPredicate; } public Predicate getPioneerPredicate() { return pioneerPredicate; } - + public Predicate getModernPredicate() { return modernPredicate; } public Predicate getCommanderPredicate() { return commanderPredicate; } diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java index 2c4e54ad21a..a7b10ec600e 100644 --- a/forge-core/src/main/java/forge/card/CardDb.java +++ b/forge-core/src/main/java/forge/card/CardDb.java @@ -165,7 +165,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { reIndex(); } - public void initialize(boolean logMissingPerEdition, boolean logMissingSummary) { + public void initialize(boolean logMissingPerEdition, boolean logMissingSummary, boolean enableUnknownCards) { Set allMissingCards = new LinkedHashSet<>(); List missingCards = new ArrayList<>(); CardEdition upcomingSet = null; @@ -218,7 +218,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { if (!contains(cr.getName())) { if (upcomingSet != null) { addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown, 1)); - } else { + } else if(enableUnknownCards) { System.err.println("The card " + cr.getName() + " was not assigned to any set. Adding it to UNKNOWN set... to fix see res/editions/ folder. "); addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special, 1)); } @@ -312,17 +312,21 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { return tryGetCard(request); } - public int getCardCollectorNumber(String cardName, String reqEdition) { + public String getCardCollectorNumber(String cardName, String reqEdition, int artIndex) { cardName = getName(cardName); CardEdition edition = editions.get(reqEdition); if (edition == null) - return -1; + return null; + int numMatches = 0; for (CardInSet card : edition.getCards()) { if (card.name.equalsIgnoreCase(cardName)) { - return card.collectorNumber; + numMatches += 1; + if (numMatches == artIndex) { + return card.collectorNumber; + } } } - return -1; + return null; } private PaperCard tryGetCard(CardRequest request) { diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java index e2b786c0322..ce6289bb5f5 100644 --- a/forge-core/src/main/java/forge/card/CardEdition.java +++ b/forge-core/src/main/java/forge/card/CardEdition.java @@ -38,6 +38,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** @@ -75,10 +77,10 @@ public final class CardEdition implements Comparable { // immutable public static class CardInSet { public final CardRarity rarity; - public final int collectorNumber; + public final String collectorNumber; public final String name; - public CardInSet(final String name, final int collectorNumber, final CardRarity rarity) { + public CardInSet(final String name, final String collectorNumber, final CardRarity rarity) { this.name = name; this.collectorNumber = collectorNumber; this.rarity = rarity; @@ -86,7 +88,7 @@ public final class CardEdition implements Comparable { // immutable public String toString() { StringBuilder sb = new StringBuilder(); - if (collectorNumber != -1) { + if (collectorNumber != null) { sb.append(collectorNumber); sb.append(' '); } @@ -190,6 +192,7 @@ public final class CardEdition implements Comparable { // immutable public boolean getSmallSetOverride() { return smallSetOverride; } public String getBoosterMustContain() { return boosterMustContain; } public CardInSet[] getCards() { return cards; } + public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others public Map getTokens() { return tokenNormalized; } @@ -266,24 +269,33 @@ public final class CardEdition implements Comparable { // immutable Map tokenNormalized = new HashMap<>(); List processedCards = new ArrayList<>(); if (contents.containsKey("cards")) { + final Pattern pattern = Pattern.compile( + /* + The following pattern will match the WAR Japanese art entries, + it should also match the Un-set and older alternate art cards + like Merseine from FEM (should the editions files ever be updated) + */ + //"(^(?[0-9]+.?) )?((?[SCURML]) )?(?.*)$" + /* Ideally we'd use the named group above, but Android 6 and + earlier don't appear to support named groups. + So, untill support for those devices is officially dropped, + we'll have to suffice with numbered groups. + We are looking for: + * cnum - grouping #2 + * rarity - grouping #4 + * name - grouping #5 + */ + "(^([0-9]+.?) )?(([SCURML]) )?(.*)$" + ); for(String line : contents.get("cards")) { - if (StringUtils.isBlank(line)) - continue; - - // Optional collector number at the start. - String[] split = line.split(" ", 2); - int collectorNumber = -1; - if (split.length >= 2 && StringUtils.isNumeric(split[0])) { - collectorNumber = Integer.parseInt(split[0]); - line = split[1]; + Matcher matcher = pattern.matcher(line); + if (matcher.matches()) { + String collectorNumber = matcher.group(2); + CardRarity r = CardRarity.smartValueOf(matcher.group(4)); + String cardName = matcher.group(5); + CardInSet cis = new CardInSet(cardName, collectorNumber, r); + processedCards.add(cis); } - - // You may omit rarity for early development - CardRarity r = CardRarity.smartValueOf(line.substring(0, 1)); - boolean hadRarity = r != CardRarity.Unknown && line.charAt(1) == ' '; - String cardName = hadRarity ? line.substring(2) : line; - CardInSet cis = new CardInSet(cardName, collectorNumber, r); - processedCards.add(cis); } } diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java index 322c003b6f2..6cd5b4daaee 100644 --- a/forge-core/src/main/java/forge/card/CardRules.java +++ b/forge-core/src/main/java/forge/card/CardRules.java @@ -222,7 +222,12 @@ public final class CardRules implements ICardCharacteristics { public boolean canBeBrawlCommander() { CardType type = mainPart.getType(); - return (type.isLegendary() && type.isCreature()) || type.isPlaneswalker(); + return type.isLegendary() && (type.isCreature() || type.isPlaneswalker()); + } + + public boolean canBeTinyLeadersCommander() { + CardType type = mainPart.getType(); + return type.isLegendary() && (type.isCreature() || type.isPlaneswalker()); } public String getMeldWith() { @@ -526,12 +531,10 @@ public final class CardRules implements ICardCharacteristics { public final ManaCostShard next() { final String unparsed = st.nextToken(); // System.out.println(unparsed); - try { - int iVal = Integer.parseInt(unparsed); - this.genericCost += iVal; + if (StringUtils.isNumeric(unparsed)) { + this.genericCost += Integer.parseInt(unparsed); return null; } - catch (NumberFormatException nex) { } return ManaCostShard.parseNonGeneric(unparsed); } diff --git a/forge-core/src/main/java/forge/card/CardRulesPredicates.java b/forge-core/src/main/java/forge/card/CardRulesPredicates.java index 6679b3ad673..66dca47b592 100644 --- a/forge-core/src/main/java/forge/card/CardRulesPredicates.java +++ b/forge-core/src/main/java/forge/card/CardRulesPredicates.java @@ -594,8 +594,10 @@ public final class CardRulesPredicates { public static final Predicate IS_VANGUARD = CardRulesPredicates.coreType(true, CardType.CoreType.Vanguard); public static final Predicate IS_CONSPIRACY = CardRulesPredicates.coreType(true, CardType.CoreType.Conspiracy); public static final Predicate IS_NON_LAND = CardRulesPredicates.coreType(false, CardType.CoreType.Land); - public static final Predicate CAN_BE_BRAWL_COMMANDER = Predicates.or(Presets.IS_PLANESWALKER, - Predicates.and(Presets.IS_CREATURE, Presets.IS_LEGENDARY)); + public static final Predicate CAN_BE_BRAWL_COMMANDER = Predicates.and(Presets.IS_LEGENDARY, + Predicates.or(Presets.IS_CREATURE, Presets.IS_PLANESWALKER)); + public static final Predicate CAN_BE_TINY_LEADERS_COMMANDER = Predicates.and(Presets.IS_LEGENDARY, + Predicates.or(Presets.IS_CREATURE, Presets.IS_PLANESWALKER)); /** The Constant IS_NON_CREATURE_SPELL. **/ public static final Predicate IS_NON_CREATURE_SPELL = com.google.common.base.Predicates diff --git a/forge-core/src/main/java/forge/card/CardType.java b/forge-core/src/main/java/forge/card/CardType.java index 4a302735d76..09d6abbd2f9 100644 --- a/forge-core/src/main/java/forge/card/CardType.java +++ b/forge-core/src/main/java/forge/card/CardType.java @@ -72,6 +72,14 @@ public final class CardType implements Comparable, CardTypeView { private static Map stringToCoreType = EnumUtils.getEnumMap(CoreType.class); private static final Set allCoreTypeNames = stringToCoreType.keySet(); + public static CoreType getEnum(String name) { + return stringToCoreType.get(name); + } + + public static boolean isValidEnum(String name) { + return stringToCoreType.containsKey(name); + } + CoreType(final boolean permanent) { isPermanent = permanent; } @@ -87,6 +95,15 @@ public final class CardType implements Comparable, CardTypeView { private static Map stringToSupertype = EnumUtils.getEnumMap(Supertype.class); private static final Set allSuperTypeNames = stringToSupertype.keySet(); + + public static Supertype getEnum(String name) { + return stringToSupertype.get(name); + } + + public static boolean isValidEnum(String name) { + return stringToSupertype.containsKey(name); + } + } private final Set coreTypes = EnumSet.noneOf(CoreType.class); @@ -108,12 +125,12 @@ public final class CardType implements Comparable, CardTypeView { public boolean add(final String t) { boolean changed; - final CoreType ct = EnumUtils.getEnum(CoreType.class, t); + final CoreType ct = CoreType.getEnum(t); if (ct != null) { changed = coreTypes.add(ct); } else { - final Supertype st = EnumUtils.getEnum(Supertype.class, t); + final Supertype st = Supertype.getEnum(t); if (st != null) { changed = supertypes.add(st); } @@ -183,11 +200,11 @@ public final class CardType implements Comparable, CardTypeView { if (subtypes.remove(str)) { changed = true; } else { - Supertype st = EnumUtils.getEnum(Supertype.class, str); + Supertype st = Supertype.getEnum(str); if (st != null && supertypes.remove(st)) { changed = true; } - CoreType ct = EnumUtils.getEnum(CoreType.class, str); + CoreType ct = CoreType.getEnum(str); if (ct != null && coreTypes.remove(ct)) { changed = true; } @@ -265,11 +282,11 @@ public final class CardType implements Comparable, CardTypeView { } t = StringUtils.capitalize(t); - final CoreType type = EnumUtils.getEnum(CoreType.class, t); + final CoreType type = CoreType.getEnum(t); if (type != null) { return hasType(type); } - final Supertype supertype = EnumUtils.getEnum(Supertype.class, t); + final Supertype supertype = Supertype.getEnum(t); if (supertype != null) { return hasSupertype(supertype); } @@ -660,7 +677,7 @@ public final class CardType implements Comparable, CardTypeView { ///////// Utility methods public static boolean isACardType(final String cardType) { - return EnumUtils.isValidEnum(CoreType.class, cardType); + return CoreType.isValidEnum(cardType); } public static Set getAllCardTypes() { @@ -708,7 +725,7 @@ public final class CardType implements Comparable, CardTypeView { } public static boolean isASupertype(final String cardType) { - return EnumUtils.isValidEnum(Supertype.class, cardType); + return Supertype.isValidEnum(cardType); } public static boolean isASubType(final String cardType) { diff --git a/forge-core/src/main/java/forge/card/mana/ManaCostShard.java b/forge-core/src/main/java/forge/card/mana/ManaCostShard.java index 8c97c99e5b8..d4533f1abc5 100644 --- a/forge-core/src/main/java/forge/card/mana/ManaCostShard.java +++ b/forge-core/src/main/java/forge/card/mana/ManaCostShard.java @@ -289,13 +289,20 @@ public enum ManaCostShard { return BinaryUtil.bitCount(this.shard & COLORS_SUPERPOSITION) == 2; } + public boolean isGeneric() { + return isOfKind(ManaAtom.GENERIC)|| isOfKind(ManaAtom.IS_X) || this.isSnow() || this.isOr2Generic(); + } public boolean isOr2Generic() { return isOfKind(ManaAtom.OR_2_GENERIC); } + + public boolean isColor(byte colorCode) { + return (colorCode & this.shard) > 0; + } public boolean canBePaidWithManaOfColor(byte colorCode) { return this.isOr2Generic() || ((COLORS_SUPERPOSITION | ManaAtom.COLORLESS) & this.shard) == 0 || - (colorCode & this.shard) > 0; + this.isColor(colorCode); } public boolean isOfKind(int atom) { diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java index 3ea356c6a6d..e369fccc075 100644 --- a/forge-core/src/main/java/forge/deck/DeckFormat.java +++ b/forge-core/src/main/java/forge/deck/DeckFormat.java @@ -463,6 +463,9 @@ public enum DeckFormat { if (this.equals(DeckFormat.Brawl)) { return rules.canBeBrawlCommander(); } + if (this.equals(DeckFormat.TinyLeaders)) { + return rules.canBeTinyLeadersCommander(); + } return rules.canBeCommander(); } diff --git a/forge-core/src/main/java/forge/item/IPaperCard.java b/forge-core/src/main/java/forge/item/IPaperCard.java index 0c14064e059..53ea1d066e4 100644 --- a/forge-core/src/main/java/forge/item/IPaperCard.java +++ b/forge-core/src/main/java/forge/item/IPaperCard.java @@ -6,6 +6,7 @@ import forge.card.CardRarity; import forge.card.CardRules; import forge.card.CardType.CoreType; import forge.card.MagicColor; +import forge.util.PredicateCard; import forge.util.PredicateString; import org.apache.commons.lang3.StringUtils; @@ -60,6 +61,8 @@ public interface IPaperCard extends InventoryItem { return new PredicateNames(what); } + public static PredicateCards cards(final List what) { return new PredicateCards(what); } + private static final class PredicateColor implements Predicate { private final byte operand; @@ -161,6 +164,25 @@ public interface IPaperCard extends InventoryItem { } } + private static final class PredicateCards extends PredicateCard { + private final List operand; + + @Override + public boolean apply(final PaperCard card) { + for (final PaperCard element : this.operand) { + if (this.op(card, element)) { + return true; + } + } + return false; + } + + private PredicateCards(final List operand) { + super(StringOp.EQUALS); + this.operand = operand; + } + } + /** * Pre-built predicates are stored here to allow their re-usage and * easier access from code. diff --git a/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java b/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java index 896e8d6de6b..833ee7e6bf7 100644 --- a/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java +++ b/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java @@ -566,12 +566,8 @@ public class BoosterGenerator { toAdd = IPaperCard.Predicates.printedInSets(sets); } else if (operator.startsWith("fromSheet(") && invert) { String sheetName = StringUtils.strip(operator.substring(9), "()\" "); - Iterable src = StaticData.instance().getPrintSheets().get(sheetName).toFlatList(); - List cardNames = Lists.newArrayList(); - for (PaperCard card : src) { - cardNames.add(card.getName()); - } - toAdd = IPaperCard.Predicates.names(Lists.newArrayList(cardNames)); + Iterable cards = StaticData.instance().getPrintSheets().get(sheetName).toFlatList(); + toAdd = IPaperCard.Predicates.cards(Lists.newArrayList(cards)); } if (toAdd == null) { diff --git a/forge-core/src/main/java/forge/util/PredicateCard.java b/forge-core/src/main/java/forge/util/PredicateCard.java new file mode 100644 index 00000000000..308b34f68ca --- /dev/null +++ b/forge-core/src/main/java/forge/util/PredicateCard.java @@ -0,0 +1,83 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2020 Jamin W. Collins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.util; + +import com.google.common.base.Predicate; +import forge.item.PaperCard; + +/** + * Special predicate class to perform string operations. + * + * @param + * the generic type + */ +public abstract class PredicateCard implements Predicate { + /** Possible operators for string operands. */ + public enum StringOp { + /** The EQUALS. */ + EQUALS, + } + + /** The operator. */ + private final StringOp operator; + + /** + * Op. + * + * @param op1 + * the op1 + * @param op2 + * the op2 + * @return true, if successful + */ + protected final boolean op(final PaperCard op1, final PaperCard op2) { + switch (this.getOperator()) { + case EQUALS: + return op1.equals(op2); + default: + return false; + } + } + + /** + * Instantiates a new predicate string. + * + * @param operator + * the operator + */ + public PredicateCard(final StringOp operator) { + this.operator = operator; + } + + /** + * @return the operator + */ + public StringOp getOperator() { + return operator; + } + + public static PredicateCard equals(final PaperCard what) { + return new PredicateCard(StringOp.EQUALS) { + @Override + public boolean apply(PaperCard subject) { + return op(subject, what); + } + }; + } + +} diff --git a/forge-core/src/main/java/forge/util/TextUtil.java b/forge-core/src/main/java/forge/util/TextUtil.java index 4974a8e087f..06a706b20f0 100644 --- a/forge-core/src/main/java/forge/util/TextUtil.java +++ b/forge-core/src/main/java/forge/util/TextUtil.java @@ -5,6 +5,8 @@ import forge.item.PaperCard; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import com.google.common.collect.ImmutableSortedMap; + import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -17,6 +19,22 @@ import java.util.Map.Entry; */ public class TextUtil { + static ImmutableSortedMap romanMap = ImmutableSortedMap.naturalOrder() + .put(1000, "M").put(900, "CM") + .put(500, "D").put(400, "CD") + .put(100, "C").put(90, "XC") + .put(50, "L").put(40, "XL") + .put(10, "X").put(9, "IX") + .put(5, "V").put(4, "IV").put(1, "I").build(); + + public final static String toRoman(int number) { + if (number <= 0) { + return ""; + } + int l = romanMap.floorKey(number); + return romanMap.get(l) + toRoman(number-l); + } + /** * Safely converts an object to a String. * diff --git a/forge-game/pom.xml b/forge-game/pom.xml index 117c4abbebd..e5b60949db6 100644 --- a/forge-game/pom.xml +++ b/forge-game/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.36-SNAPSHOT forge-game diff --git a/forge-game/src/main/java/forge/game/CardTraitBase.java b/forge-game/src/main/java/forge/game/CardTraitBase.java index 1df09691ee1..1b6da08e1b8 100644 --- a/forge-game/src/main/java/forge/game/CardTraitBase.java +++ b/forge-game/src/main/java/forge/game/CardTraitBase.java @@ -5,7 +5,6 @@ import forge.card.mana.ManaAtom; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; -import forge.game.card.CardFactoryUtil; import forge.game.card.CardLists; import forge.game.card.CardUtil; import forge.game.card.CardView; @@ -243,7 +242,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView { final String type = params.get("Presence"); int revealed = AbilityUtils.calculateAmount(hostCard, "Revealed$Valid " + type, hostCard.getCastSA()); - int ctrl = AbilityUtils.calculateAmount(hostCard, "Count$Valid " + type + ".inZoneBattlefield+YouCtrl", hostCard.getCastSA()); + int ctrl = AbilityUtils.calculateAmount(hostCard, "Count$LastStateBattlefield " + type + ".YouCtrl", hostCard.getCastSA()); if (revealed + ctrl == 0) { return false; @@ -271,13 +270,8 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView { lifeCompare = params.get("LifeAmount"); } - int right = 1; final String rightString = lifeCompare.substring(2); - try { - right = Integer.parseInt(rightString); - } catch (final NumberFormatException nfe) { - right = CardFactoryUtil.xCount(this.getHostCard(), this.getHostCard().getSVar(rightString)); - } + int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this); if (!Expressions.compare(life, lifeCompare, right)) { return false; @@ -314,13 +308,9 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView { } list = CardLists.getValidCards(list, sIsPresent.split(","), this.getHostCard().getController(), this.getHostCard(), null); - int right = 1; + final String rightString = presentCompare.substring(2); - try { - right = Integer.parseInt(rightString); - } catch (final NumberFormatException nfe) { - right = CardFactoryUtil.xCount(this.getHostCard(), this.getHostCard().getSVar(rightString)); - } + int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this); final int left = list.size(); if (!Expressions.compare(left, presentCompare, right)) { @@ -355,13 +345,8 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView { list = CardLists.getValidCards(list, sIsPresent.split(","), this.getHostCard().getController(), this.getHostCard(), null); - int right = 1; final String rightString = presentCompare.substring(2); - try { - right = Integer.parseInt(rightString); - } catch (final NumberFormatException nfe) { - right = CardFactoryUtil.xCount(this.getHostCard(), this.getHostCard().getSVar(rightString)); - } + int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this); final int left = list.size(); if (!Expressions.compare(left, presentCompare, right)) { diff --git a/forge-game/src/main/java/forge/game/EvenOdd.java b/forge-game/src/main/java/forge/game/EvenOdd.java new file mode 100644 index 00000000000..89f759d0109 --- /dev/null +++ b/forge-game/src/main/java/forge/game/EvenOdd.java @@ -0,0 +1,6 @@ +package forge.game; + +public enum EvenOdd { + Even, + Odd +} diff --git a/forge-game/src/main/java/forge/game/ForgeScript.java b/forge-game/src/main/java/forge/game/ForgeScript.java index f21d1155fbd..06a3d41adee 100644 --- a/forge-game/src/main/java/forge/game/ForgeScript.java +++ b/forge-game/src/main/java/forge/game/ForgeScript.java @@ -81,7 +81,7 @@ public class ForgeScript { return !cardState.getTypeWithChanges().hasSubtype(subType); } else if (property.equals("hasActivatedAbilityWithTapCost")) { for (final SpellAbility sa : cardState.getSpellAbilities()) { - if (sa.isAbility() && (sa.getPayCosts() != null) && sa.getPayCosts().hasTapCost()) { + if (sa.isAbility() && sa.getPayCosts().hasTapCost()) { return true; } } @@ -108,14 +108,9 @@ public class ForgeScript { } return false; } else if (property.startsWith("cmc")) { - int x; String rhs = property.substring(5); int y = cardState.getManaCost().getCMC(); - try { - x = Integer.parseInt(rhs); - } catch (final NumberFormatException e) { - x = AbilityUtils.calculateAmount(source, rhs, spellAbility); - } + int x = AbilityUtils.calculateAmount(source, rhs, spellAbility); return Expressions.compare(y, property, x); } else return cardState.getTypeWithChanges().hasStringType(property); @@ -130,6 +125,8 @@ public class ForgeScript { return sa.isManaAbility(); } else if (property.equals("nonManaAbility")) { return !sa.isManaAbility(); + } else if (property.equals("withoutXCost")) { + return !sa.isXCost(); } else if (property.equals("Buyback")) { return sa.isBuyBackAbility(); } else if (property.equals("Cycling")) { diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 849e2de4a0d..47997c1b8bc 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -40,7 +40,6 @@ import forge.game.player.*; import forge.game.replacement.ReplacementHandler; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityStackInstance; -import forge.game.spellability.SpellAbilityView; import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerType; import forge.game.zone.CostPaymentStack; @@ -80,7 +79,7 @@ public class Game { private final GameLog gameLog = new GameLog(); private final Zone stackZone = new Zone(ZoneType.Stack, this); - + private CardCollection lastStateBattlefield = new CardCollection(); private CardCollection lastStateGraveyard = new CardCollection(); @@ -98,7 +97,7 @@ public class Game { private GameStage age = GameStage.BeforeMulligan; private GameOutcome outcome; - private final GameView view; + private final GameView view; private final Tracker tracker = new Tracker(); public Player getMonarch() { @@ -178,19 +177,6 @@ public class Game { playerCache.put(Integer.valueOf(id), player); } - private final GameEntityCache cardCache = new GameEntityCache<>(); - public Card getCard(CardView cardView) { - return cardCache.get(cardView); - } - public void addCard(int id, Card card) { - cardCache.put(Integer.valueOf(id), card); - } - public CardCollection getCardList(Iterable cardViews) { - CardCollection list = new CardCollection(); - cardCache.addToList(cardViews, list); - return list; - } - // methods that deal with saving, retrieving and clearing LKI information about cards on zone change private final HashMap changeZoneLKIInfo = new HashMap<>(); public final void addChangeZoneLKIInfo(Card c) { @@ -209,27 +195,6 @@ public class Game { changeZoneLKIInfo.clear(); } - private final GameEntityCache spabCache = new GameEntityCache<>(); - public SpellAbility getSpellAbility(final SpellAbilityView view) { - return spabCache.get(view); - } - public void addSpellAbility(SpellAbility spellAbility) { - spabCache.put(spellAbility.getId(), spellAbility); - } - public void removeSpellAbility(SpellAbility spellAbility) { - spabCache.remove(spellAbility.getId()); - } - public void validateSpabCache() { - for (SpellAbility sa : spabCache.getValues()) { - if (sa.getHostCard() != null && sa.getHostCard().getGame() != this) { - throw new RuntimeException(); - } - if (sa.getActivatingPlayer() != null && sa.getActivatingPlayer().getGame() != this) { - throw new RuntimeException(); - } - } - } - public Game(List players0, GameRules rules0, Match match0) { /* no more zones to map here */ rules = rules0; match = match0; @@ -407,7 +372,7 @@ public class Game { } }); } - + /** * The Direction in which the turn order of this Game currently proceeds. */ @@ -559,6 +524,48 @@ public class Game { return visit.getFound(notFound); } + private static class CardIdVisitor extends Visitor { + Card found = null; + int id; + + private CardIdVisitor(final int id) { + this.id = id; + } + + @Override + public boolean visit(Card object) { + if (this.id == object.getId()) { + found = object; + } + return found == null; + } + + public Card getFound() { + return found; + } + } + + public Card findByView(CardView view) { + if (view == null) { + return null; + } + CardIdVisitor visit = new CardIdVisitor(view.getId()); + if (ZoneType.Stack.equals(view.getZone())) { + visit.visitAll(getStackZone()); + } else if (view.getController() != null && view.getZone() != null) { + visit.visitAll(getPlayer(view.getController()).getZone(view.getZone())); + } else { // fallback if view doesn't has controller or zone set for some reason + forEachCardInGame(visit); + } + return visit.getFound(); + } + + public Card findById(int id) { + CardIdVisitor visit = new CardIdVisitor(id); + this.forEachCardInGame(visit); + return visit.getFound(); + } + // Allows visiting cards in game without allocating a temporary list. public void forEachCardInGame(Visitor visitor) { for (final Player player : getPlayers()) { @@ -672,21 +679,25 @@ public class Game { // Rule 800.4 Losing a Multiplayer game CardCollectionView cards = this.getCardsInGame(); boolean planarControllerLost = false; + boolean isMultiplayer = this.getPlayers().size() > 2; for(Card c : cards) { if (c.getController().equals(p) && (c.isPlane() || c.isPhenomenon())) { planarControllerLost = true; } - if (c.getOwner().equals(p)) { - c.ceaseToExist(); - } else { - c.removeTempController(p); - if (c.getController().equals(p)) { - this.getAction().exile(c, null); + if(isMultiplayer) { + if (c.getOwner().equals(p)) { + c.ceaseToExist(); + } else { + c.removeTempController(p); + if (c.getController().equals(p)) { + this.getAction().exile(c, null); + } } + } else { + c.forceTurnFaceUp(); } - } // 901.6: If the current planar controller would leave the game, instead the next player @@ -789,11 +800,11 @@ public class Game { public Multimap chooseCardsForAnte(final boolean matchRarity) { Multimap anteed = ArrayListMultimap.create(); - + if (matchRarity) { - + boolean onePlayerHasTimeShifted = false; - + List validRarities = new ArrayList<>(Arrays.asList(CardRarity.values())); for (final Player player : getPlayers()) { final Set playerRarity = getValidRarities(player.getCardsIn(ZoneType.Library)); @@ -809,24 +820,24 @@ public class Game { } return anteed; } - + //If possible, don't ante basic lands if (validRarities.size() > 1) { validRarities.remove(CardRarity.BasicLand); } - + if (validRarities.contains(CardRarity.Special)) { onePlayerHasTimeShifted = false; } - + CardRarity anteRarity = validRarities.get(MyRandom.getRandom().nextInt(validRarities.size())); - + System.out.println("Rarity chosen for ante: " + anteRarity.name()); - + for (final Player player : getPlayers()) { CardCollection library = new CardCollection(player.getCardsIn(ZoneType.Library)); CardCollection toRemove = new CardCollection(); - + //Remove all cards that aren't of the chosen rarity for (Card card : library) { if (onePlayerHasTimeShifted && card.getRarity() == CardRarity.Special) { @@ -845,16 +856,16 @@ public class Game { } } } - + library.removeAll(toRemove); - + if (library.size() > 0) { //Make sure that matches were found. If not, use the original method to choose antes Card ante = library.get(MyRandom.getRandom().nextInt(library.size())); anteed.put(player, ante); } else { chooseRandomCardsForAnte(player, anteed); } - + } } else { @@ -892,8 +903,9 @@ public class Game { } public void clearCaches() { - spabCache.clear(); - cardCache.clear(); + + lastStateBattlefield.clear(); + lastStateGraveyard.clear(); //playerCache.clear(); } @@ -914,4 +926,17 @@ public class Game { } return false; } + + public Player getControlVote() { + Player result = null; + long maxValue = 0; + for (Player p : getPlayers()) { + Long v = p.getHighestControlVote(); + if (v != null && v > maxValue) { + maxValue = v; + result = p; + } + } + return result; + } } diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 16b67a2d4e7..6456b345b3d 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -36,7 +36,6 @@ import forge.game.player.Player; import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementResult; import forge.game.replacement.ReplacementType; -import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityPredicates; import forge.game.staticability.StaticAbility; @@ -104,7 +103,7 @@ public class GameAction { boolean wasFacedown = c.isFaceDown(); //Rule 110.5g: A token that has left the battlefield can't move to another zone - if (c.isToken() && zoneFrom != null && !fromBattlefield && !zoneFrom.is(ZoneType.Command)) { + if (c.isToken() && zoneFrom != null && !fromBattlefield) { return c; } @@ -157,11 +156,6 @@ public class GameAction { c.removeSVar("EndOfTurnLeavePlay"); } - // Clean up temporary variables such as Sunburst value or announced PayX value - if (!(zoneTo.is(ZoneType.Stack) || zoneTo.is(ZoneType.Battlefield))) { - c.clearTemporaryVars(); - } - if (fromBattlefield && !toBattlefield) { c.getController().setRevolt(true); } @@ -173,7 +167,7 @@ public class GameAction { // if to Battlefield and it is caused by an replacement effect, // try to get previous LKI if able - if (zoneTo.is(ZoneType.Battlefield)) { + if (toBattlefield) { if (cause != null && cause.isReplacementAbility()) { ReplacementEffect re = cause.getReplacementEffect(); if (ReplacementType.Moved.equals(re.getMode())) { @@ -185,6 +179,10 @@ public class GameAction { if (lastKnownInfo == null) { lastKnownInfo = CardUtil.getLKICopy(c); } + + if (!lastKnownInfo.hasKeyword("Counters remain on CARDNAME as it moves to any zone other than a player's hand or library.") || zoneTo.is(ZoneType.Hand) || zoneTo.is(ZoneType.Library)) { + copied.clearCounters(); + } } else { // if from Battlefield to Graveyard and Card does exist in LastStateBattlefield // use that instead @@ -233,17 +231,20 @@ public class GameAction { for (final StaticAbility sa : copied.getStaticAbilities()) { sa.setHostCard(copied); } - if (c.getName().equals("Skullbriar, the Walking Grave")) { - copied.setCounters(c.getCounters()); - } - - // ensure that any leftover keyword/type changes are cleared in the state view - copied.updateStateForView(); } else { //Token copied = c; } } + // ensure that any leftover keyword/type changes are cleared in the state view + copied.updateStateForView(); + + // Clean up temporary variables such as Sunburst value or announced PayX value + if (!(zoneTo.is(ZoneType.Stack) || zoneTo.is(ZoneType.Battlefield))) { + copied.clearTemporaryVars(); + } + + if (!suppress) { if (zoneFrom == null) { copied.getOwner().addInboundToken(copied); @@ -389,7 +390,8 @@ public class GameAction { // play the change zone sound game.fireEvent(new GameEventCardChangeZone(c, zoneFrom, zoneTo)); - final Map runParams = AbilityKey.mapFromCard(lastKnownInfo); + final Map runParams = AbilityKey.mapFromCard(copied); + runParams.put(AbilityKey.CardLKI, lastKnownInfo); runParams.put(AbilityKey.Cause, cause); runParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType().name() : null); runParams.put(AbilityKey.Destination, zoneTo.getZoneType().name()); @@ -419,13 +421,6 @@ public class GameAction { return copied; } - // remove all counters from the card if destination is not the battlefield - // UNLESS we're dealing with Skullbriar, the Walking Grave - if (!c.isToken() && (zoneTo.is(ZoneType.Hand) || zoneTo.is(ZoneType.Library) || - (!toBattlefield && !c.getName().equals("Skullbriar, the Walking Grave")))) { - copied.clearCounters(); - } - if (!c.isToken() && !toBattlefield) { copied.clearDevoured(); copied.clearDelved(); @@ -547,6 +542,14 @@ public class GameAction { c.setCastSA(null); } else if (zoneTo.is(ZoneType.Stack)) { c.setCastFrom(zoneFrom.getZoneType()); + if (cause != null && cause.isSpell() && c.equals(cause.getHostCard()) && !c.isCopiedSpell()) { + cause.setLastStateBattlefield(game.getLastStateBattlefield()); + cause.setLastStateGraveyard(game.getLastStateGraveyard()); + + c.setCastSA(cause); + } else { + c.setCastSA(null); + } } else if (!(zoneTo.is(ZoneType.Battlefield) && zoneFrom.is(ZoneType.Stack))) { c.setCastFrom(null); c.setCastSA(null); @@ -962,6 +965,9 @@ public class GameAction { for (final Player p : game.getPlayers()) { for (final ZoneType zt : ZoneType.values()) { + if (zt == ZoneType.Command) + p.checkKeywordCard(); + if (zt == ZoneType.Battlefield) { continue; } @@ -985,7 +991,7 @@ public class GameAction { checkAgain = true; } else if (c.hasKeyword("CARDNAME can't be destroyed by lethal damage unless lethal damage dealt by a single source is marked on it.")) { for (final Integer dmg : c.getReceivedDamageFromThisTurn().values()) { - if (c.getNetToughness() <= dmg.intValue()) { + if (c.getLethal() <= dmg.intValue()) { if (desCreats == null) { desCreats = new CardCollection(); } @@ -997,7 +1003,7 @@ public class GameAction { } // Rule 704.5g - Destroy due to lethal damage // Rule 704.5h - Destroy due to deathtouch - else if (c.getNetToughness() <= c.getDamage() || c.hasBeenDealtDeathtouchDamage()) { + else if (c.getDamage() > 0 && (c.getLethal() <= c.getDamage() || c.hasBeenDealtDeathtouchDamage())) { if (desCreats == null) { desCreats = new CardCollection(); } @@ -1017,8 +1023,10 @@ public class GameAction { checkAgain |= stateBasedAction704_5r(c); // annihilate +1/+1 counters with -1/-1 ones - if (c.getCounters(CounterType.DREAM) > 7 && c.hasKeyword("CARDNAME can't have more than seven dream counters on it.")) { - c.subtractCounter(CounterType.DREAM, c.getCounters(CounterType.DREAM) - 7); + final CounterType dreamType = CounterType.get(CounterEnumType.DREAM); + + if (c.getCounters(dreamType) > 7 && c.hasKeyword("CARDNAME can't have more than seven dream counters on it.")) { + c.subtractCounter(dreamType, c.getCounters(dreamType) - 7); checkAgain = true; } } @@ -1110,7 +1118,7 @@ public class GameAction { if (!c.canBeSacrificed()) { return false; } - if (c.getCounters(CounterType.LORE) < c.getFinalChapterNr()) { + if (c.getCounters(CounterEnumType.LORE) < c.getFinalChapterNr()) { return false; } if (!game.getStack().hasSimultaneousStackEntries() && @@ -1151,16 +1159,18 @@ public class GameAction { private boolean stateBasedAction704_5r(Card c) { boolean checkAgain = false; - int plusOneCounters = c.getCounters(CounterType.P1P1); - int minusOneCounters = c.getCounters(CounterType.M1M1); + final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1); + final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1); + int plusOneCounters = c.getCounters(p1p1); + int minusOneCounters = c.getCounters(m1m1); if (plusOneCounters > 0 && minusOneCounters > 0) { int remove = Math.min(plusOneCounters, minusOneCounters); // If a permanent has both a +1/+1 counter and a -1/-1 counter on it, // N +1/+1 and N -1/-1 counters are removed from it, where N is the // smaller of the number of +1/+1 and -1/-1 counters on it. // This should fire remove counters trigger - c.subtractCounter(CounterType.P1P1, remove); - c.subtractCounter(CounterType.M1M1, remove); + c.subtractCounter(p1p1, remove); + c.subtractCounter(m1m1, remove); checkAgain = true; } return checkAgain; @@ -1171,7 +1181,7 @@ public class GameAction { boolean checkAgain = false; if (c.isToken()) { final Zone zoneFrom = game.getZoneOf(c); - if (!zoneFrom.is(ZoneType.Battlefield) && !zoneFrom.is(ZoneType.Command)) { + if (!zoneFrom.is(ZoneType.Battlefield)) { zoneFrom.remove(c); checkAgain = true; } @@ -1281,7 +1291,7 @@ public class GameAction { //final Multimap uniqueWalkers = ArrayListMultimap.create(); // Not used as of Ixalan for (Card c : list) { - if (c.getCounters(CounterType.LOYALTY) <= 0) { + if (c.getCounters(CounterEnumType.LOYALTY) <= 0) { sacrificeDestroy(c, null, table); // Play the Destroy sound game.fireEvent(new GameEventCardDestroyed()); @@ -1341,7 +1351,8 @@ public class GameAction { recheck = true; - Card toKeep = p.getController().chooseSingleEntityForEffect(new CardCollection(cc), new AbilitySub(ApiType.InternalLegendaryRule, null, null, null), "You have multiple legendary permanents named \""+name+"\" in play.\n\nChoose the one to stay on battlefield (the rest will be moved to graveyard)"); + Card toKeep = p.getController().chooseSingleEntityForEffect(new CardCollection(cc), new SpellAbility.EmptySa(ApiType.InternalLegendaryRule, null, p), + "You have multiple legendary permanents named \""+name+"\" in play.\n\nChoose the one to stay on battlefield (the rest will be moved to graveyard)", null); for (Card c: cc) { if (c != toKeep) { sacrificeDestroy(c, null, table); @@ -1453,12 +1464,18 @@ public class GameAction { revealTo(card, Collections.singleton(to)); } public void revealTo(final CardCollectionView cards, final Player to) { - revealTo(cards, Collections.singleton(to)); + revealTo(cards, to, null); + } + public void revealTo(final CardCollectionView cards, final Player to, String messagePrefix) { + revealTo(cards, Collections.singleton(to), messagePrefix); } public void revealTo(final Card card, final Iterable to) { revealTo(new CardCollection(card), to); } public void revealTo(final CardCollectionView cards, final Iterable to) { + revealTo(cards, to, null); + } + public void revealTo(final CardCollectionView cards, final Iterable to, String messagePrefix) { if (cards.isEmpty()) { return; } @@ -1466,7 +1483,7 @@ public class GameAction { final ZoneType zone = cards.getFirst().getZone().getZoneType(); final Player owner = cards.getFirst().getOwner(); for (final Player p : to) { - p.getController().reveal(cards, zone, owner); + p.getController().reveal(cards, zone, owner, messagePrefix); } } diff --git a/forge-game/src/main/java/forge/game/GameActionUtil.java b/forge-game/src/main/java/forge/game/GameActionUtil.java index b54c727370f..68c756c804d 100644 --- a/forge-game/src/main/java/forge/game/GameActionUtil.java +++ b/forge-game/src/main/java/forge/game/GameActionUtil.java @@ -22,19 +22,28 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import forge.card.MagicColor; import forge.card.mana.ManaCost; import forge.card.mana.ManaCostParser; +import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.*; import forge.game.card.CardPlayOption.PayManaCost; import forge.game.cost.Cost; +import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; import forge.game.player.Player; import forge.game.player.PlayerController; +import forge.game.replacement.ReplacementEffect; +import forge.game.replacement.ReplacementHandler; +import forge.game.replacement.ReplacementLayer; import forge.game.spellability.*; import forge.game.trigger.Trigger; +import forge.game.trigger.TriggerHandler; +import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; +import forge.util.Lang; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -210,23 +219,34 @@ public final class GameActionUtil { } } - if (!sa.isBasicSpell()) { - return alternatives; - } - + // below are for some special cases of activated abilities if (sa.isCycling() && activator.hasKeyword("CyclingForZero")) { - // set the cost to this directly to buypass non mana cost - final SpellAbility newSA = sa.copyWithDefinedCost("Discard<1/CARDNAME>"); - newSA.setActivatingPlayer(activator); - newSA.setBasicSpell(false); - newSA.getMapParams().put("CostDesc", ManaCostParser.parse("0")); - // makes new SpellDescription - final StringBuilder sb = new StringBuilder(); - sb.append(newSA.getCostDescription()); - sb.append(newSA.getParam("SpellDescription")); - newSA.setDescription(sb.toString()); - alternatives.add(newSA); + for (final KeywordInterface inst : source.getKeywords()) { + // need to find the correct Keyword from which this Ability is from + if (!inst.getAbilities().contains(sa)) { + continue; + } + + // set the cost to this directly to buypass non mana cost + final SpellAbility newSA = sa.copyWithDefinedCost("Discard<1/CARDNAME>"); + newSA.setActivatingPlayer(activator); + newSA.getMapParams().put("CostDesc", ManaCostParser.parse("0")); + + // need to build a new Keyword to get better Reminder Text + String data[] = inst.getOriginal().split(":"); + data[1] = "0"; + KeywordInterface newKi = Keyword.getInstance(StringUtils.join(data, ":")); + + // makes new SpellDescription + final StringBuilder sb = new StringBuilder(); + sb.append(newSA.getCostDescription()); + sb.append("(").append(newKi.getReminderText()).append(")"); + newSA.setDescription(sb.toString()); + + alternatives.add(newSA); + break; + } } if (sa.hasParam("Equip") && activator.hasKeyword("EquipInstantSpeed")) { @@ -363,10 +383,11 @@ public final class GameActionUtil { } SpellAbility result = null; final Card host = sa.getHostCard(); + final Game game = host.getGame(); final Player activator = sa.getActivatingPlayer(); final PlayerController pc = activator.getController(); - host.getGame().getAction().checkStaticAbilities(false); + game.getAction().checkStaticAbilities(false); boolean reset = false; @@ -429,7 +450,59 @@ public final class GameActionUtil { int v = pc.chooseNumberForKeywordCost(sa, cost, ki, str, Integer.MAX_VALUE); if (v > 0) { - host.addReplacementEffect(CardFactoryUtil.makeEtbCounter("etbCounter:P1P1:" + v, host, false)); + + final Card eff = new Card(game.nextCardId(), game); + eff.setTimestamp(game.getNextTimestamp()); + eff.setName(c.getName() + "'s Effect"); + eff.addType("Effect"); + eff.setOwner(activator); + + eff.setImageKey(c.getImageKey()); + eff.setColor(MagicColor.COLORLESS); + eff.setImmutable(true); + // try to get the SpellAbility from the mana ability + //eff.setEffectSource((SpellAbility)null); + + eff.addRemembered(host); + + String abStr = "DB$ PutCounter | Defined$ ReplacedCard | CounterType$ P1P1 | ETB$ True | CounterNum$ " + v; + + SpellAbility saAb = AbilityFactory.getAbility(abStr, c); + + CardFactoryUtil.setupETBReplacementAbility(saAb); + + String desc = "It enters the battlefield with "; + desc += Lang.nounWithNumeral(v, CounterEnumType.P1P1.getName() + " counter"); + desc += " on it."; + + String repeffstr = "Event$ Moved | ValidCard$ Card.IsRemembered | Destination$ Battlefield | Description$ " + desc; + + ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true); + re.setLayer(ReplacementLayer.Other); + re.setOverridingAbility(saAb); + + eff.addReplacementEffect(re); + + // Forgot Trigger + String trig = "Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Any | TriggerZones$ Command | Static$ True"; + String forgetEffect = "DB$ Pump | ForgetObjects$ TriggeredCard"; + String exileEffect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile" + + " | ConditionDefined$ Remembered | ConditionPresent$ Card | ConditionCompare$ EQ0"; + + SpellAbility saForget = AbilityFactory.getAbility(forgetEffect, eff); + AbilitySub saExile = (AbilitySub) AbilityFactory.getAbility(exileEffect, eff); + saForget.setSubAbility(saExile); + + final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, eff, true); + parsedTrigger.setOverridingAbility(saForget); + eff.addTrigger(parsedTrigger); + eff.updateStateForView(); + + // TODO: Add targeting to the effect so it knows who it's dealing with + game.getTriggerHandler().suppressMode(TriggerType.ChangesZone); + game.getAction().moveTo(ZoneType.Command, eff, null); + game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone); + if (result == null) { result = sa.copy(); } @@ -513,14 +586,8 @@ public final class GameActionUtil { // Mark SAs with subAbilities as undoable. These are generally things like damage, and other stuff // that's hard to track and remove sa.setUndoable(false); - } else { - try { - if ((sa.getParam("Amount") != null) && (amount != Integer.parseInt(sa.getParam("Amount")))) { - sa.setUndoable(false); - } - } catch (final NumberFormatException n) { - sa.setUndoable(false); - } + } else if ((sa.getParam("Amount") != null) && (amount != AbilityUtils.calculateAmount(sa.getHostCard(),sa.getParam("Amount"), sa))) { + sa.setUndoable(false); } final StringBuilder sb = new StringBuilder(); diff --git a/forge-game/src/main/java/forge/game/GameEntity.java b/forge-game/src/main/java/forge/game/GameEntity.java index f042378ec64..cbd00a9144f 100644 --- a/forge-game/src/main/java/forge/game/GameEntity.java +++ b/forge-game/src/main/java/forge/game/GameEntity.java @@ -24,6 +24,7 @@ import forge.game.card.CardCollectionView; import forge.game.card.CardDamageMap; import forge.game.card.CardLists; import forge.game.card.CardPredicates; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.event.GameEventCardAttachment; import forge.game.keyword.Keyword; @@ -38,6 +39,7 @@ import forge.util.collect.FCollection; import java.util.Map; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -48,7 +50,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { private int preventNextDamage = 0; protected CardCollection attachedCards; private Map> preventionShieldsWithEffects = Maps.newTreeMap(); - protected Map counters = Maps.newEnumMap(CounterType.class); + protected Map counters = Maps.newTreeMap(); protected GameEntity(int id0) { id = id0; @@ -315,7 +317,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { } // enchanted means attached by Aura - return CardLists.count(attachedCards, CardPredicates.Presets.AURA) > 0; + return Iterables.any(attachedCards, CardPredicates.Presets.AURA); } public final boolean hasCardAttachment(Card c) { @@ -453,6 +455,10 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { return value == null ? 0 : value; } + public final int getCounters(final CounterEnumType counterType) { + return getCounters(CounterType.get(counterType)); + } + public void setCounters(final CounterType counterType, final Integer num) { if (num <= 0) { counters.remove(counterType); @@ -461,6 +467,10 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { } } + public void setCounters(final CounterEnumType counterType, final Integer num) { + setCounters(CounterType.get(counterType), num); + } + abstract public void setCounters(final Map allCounters); abstract public boolean canReceiveCounters(final CounterType type); @@ -468,6 +478,16 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { abstract public void subtractCounter(final CounterType counterName, final int n); abstract public void clearCounters(); + public boolean canReceiveCounters(final CounterEnumType type) { + return canReceiveCounters(CounterType.get(type)); + } + + public int addCounter(final CounterEnumType counterType, final int n, final Player source, final boolean applyMultiplier, final boolean fireEvents, GameEntityCounterTable table) { + return addCounter(CounterType.get(counterType), n, source, applyMultiplier, fireEvents, table); + } + public void subtractCounter(final CounterEnumType counterName, final int n) { + subtractCounter(CounterType.get(counterName), n); + } @Override public final boolean equals(Object o) { diff --git a/forge-game/src/main/java/forge/game/GameEntityCache.java b/forge-game/src/main/java/forge/game/GameEntityCache.java index b5532f2bf96..de4716be035 100644 --- a/forge-game/src/main/java/forge/game/GameEntityCache.java +++ b/forge-game/src/main/java/forge/game/GameEntityCache.java @@ -13,6 +13,11 @@ public class GameEntityCache entities) { + for (Entity e : entities) { + put(e.getId(), e); + } + } public void remove(Integer id) { entityCache.remove(id); diff --git a/forge-game/src/main/java/forge/game/GameEntityView.java b/forge-game/src/main/java/forge/game/GameEntityView.java index 0349b4c8e54..5684e41d424 100644 --- a/forge-game/src/main/java/forge/game/GameEntityView.java +++ b/forge-game/src/main/java/forge/game/GameEntityView.java @@ -24,6 +24,12 @@ public abstract class GameEntityView extends TrackableObject { return collection; } + public static GameEntityViewMap getMap(Iterable spabs) { + GameEntityViewMap gameViewCache = new GameEntityViewMap(); + gameViewCache.putAll(spabs); + return gameViewCache; + } + protected GameEntityView(final int id0, final Tracker tracker) { super(id0, tracker); } diff --git a/forge-game/src/main/java/forge/game/GameEntityViewMap.java b/forge-game/src/main/java/forge/game/GameEntityViewMap.java new file mode 100644 index 00000000000..e30161405bc --- /dev/null +++ b/forge-game/src/main/java/forge/game/GameEntityViewMap.java @@ -0,0 +1,52 @@ +package forge.game; + +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ForwardingMap; +import com.google.common.collect.Maps; + +import forge.trackable.TrackableCollection; + +public class GameEntityViewMap extends ForwardingMap { + private Map dataMap = Maps.newLinkedHashMap(); + + @Override + protected Map delegate() { + return dataMap; + } + + @SuppressWarnings("unchecked") + public void put(Entity e) { + this.put((View) e.getView(), e); + } + + public void putAll(Iterable entities) { + for (Entity e : entities) { + put(e); + } + } + + public void remove(Entity e) { + this.remove(e.getView()); + } + + public void removeAll(Iterable entities) { + for (Entity e : entities) { + remove(e); + } + } + + public void addToList(Iterable views, List list) { + for (View view : views) { + Entity entity = get(view); + if (entity != null) { + list.add(entity); + } + } + } + + public TrackableCollection getTrackableKeys() { + return new TrackableCollection(this.keySet()); + } +} diff --git a/forge-game/src/main/java/forge/game/GameFormat.java b/forge-game/src/main/java/forge/game/GameFormat.java index e4efda21fb6..a66395ce300 100644 --- a/forge-game/src/main/java/forge/game/GameFormat.java +++ b/forge-game/src/main/java/forge/game/GameFormat.java @@ -47,7 +47,7 @@ import java.util.Map.Entry; public class GameFormat implements Comparable { private final String name; public enum FormatType {Sanctioned, Casual, Historic, Digital, Custom} - public enum FormatSubType {Block, Standard, Extended, Pioneer, Modern, Legacy, Vintage, Commander, Planechase, Videogame, MTGO, Custom} + public enum FormatSubType {Block, Standard, Extended, Pioneer, Modern, Legacy, Vintage, Commander, Planechase, Videogame, MTGO, Arena, Custom} // contains allowed sets, when empty allows all sets private FormatType formatType; diff --git a/forge-game/src/main/java/forge/game/GameLogFormatter.java b/forge-game/src/main/java/forge/game/GameLogFormatter.java index f098cb088e7..cb83ff742e6 100644 --- a/forge-game/src/main/java/forge/game/GameLogFormatter.java +++ b/forge-game/src/main/java/forge/game/GameLogFormatter.java @@ -95,7 +95,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base { String action = event.sa.isSpell() ? localizer.getMessage("lblCast") : event.sa.isTrigger() ? localizer.getMessage("lblTriggered") : localizer.getMessage("lblActivated"); - String object = event.sa.getStackDescription().startsWith("Morph ") + String object = event.si.getStackDescription().startsWith("Morph ") ? localizer.getMessage("lblMorph") : event.sa.getHostCard().toString(); diff --git a/forge-game/src/main/java/forge/game/GameView.java b/forge-game/src/main/java/forge/game/GameView.java index 0aad79f5cba..eda68175be6 100644 --- a/forge-game/src/main/java/forge/game/GameView.java +++ b/forge-game/src/main/java/forge/game/GameView.java @@ -24,7 +24,6 @@ import java.util.List; public class GameView extends TrackableObject { private static final long serialVersionUID = 8522884512960961528L; - private CombatView combatView; private final transient Game game; //TODO: Remove this when possible before network support added public GameView(final Game game0) { @@ -140,15 +139,18 @@ public class GameView extends TrackableObject { } public CombatView getCombat() { - return combatView; + return get(TrackableProperty.CombatView); + } + public void updateCombatView(CombatView combatView) { + set(TrackableProperty.CombatView, combatView); } void updateCombat(Combat combat) { if (combat == null) { - combatView = null; + set(TrackableProperty.CombatView, null); return; } - combatView = new CombatView(combat.getAttackingPlayer().getGame().getTracker()); + final CombatView combatView = new CombatView(combat.getAttackingPlayer().getGame().getTracker()); for (final AttackingBand b : combat.getAttackingBands()) { if (b == null) continue; final GameEntity defender = combat.getDefenderByAttacker(b); @@ -160,6 +162,7 @@ public class GameView extends TrackableObject { isBlocked ? CardView.getCollection(blockers) : null, CardView.getCollection(blockers)); } + updateCombatView(combatView); } public void serialize() { diff --git a/forge-game/src/main/java/forge/game/Match.java b/forge-game/src/main/java/forge/game/Match.java index 1ef3b279a23..59b99b724ee 100644 --- a/forge-game/src/main/java/forge/game/Match.java +++ b/forge-game/src/main/java/forge/game/Match.java @@ -183,7 +183,7 @@ public class Match { return myRemovedAnteCards; } - private static void preparePlayerLibrary(Player player, final ZoneType zoneType, CardPool section, boolean canRandomFoil) { + private static void preparePlayerZone(Player player, final ZoneType zoneType, CardPool section, boolean canRandomFoil) { PlayerZone library = player.getZone(zoneType); List newLibrary = new ArrayList<>(); for (final Entry stackOfCards : section) { @@ -234,16 +234,15 @@ public class Match { for (int i = 0; i < playersConditions.size(); i++) { final Player player = players.get(i); final RegisteredPlayer psc = playersConditions.get(i); + PlayerController person = player.getController(); if (canSideBoard) { - PlayerController person = player.getController(); if (sideboardProxy != null && person.isAI()) { person = sideboardProxy; } - String forPlayer = " for " + player.getName(); Deck toChange = psc.getDeck(); - List newMain = person.sideboard(toChange, rules.getGameType(), forPlayer); + List newMain = person.sideboard(toChange, rules.getGameType(), player.getName()); if (null != newMain) { CardPool allCards = new CardPool(); allCards.addAll(toChange.get(DeckSection.Main)); @@ -270,21 +269,37 @@ public class Match { } } - preparePlayerLibrary(player, ZoneType.Library, myDeck.getMain(), psc.useRandomFoil()); + preparePlayerZone(player, ZoneType.Library, myDeck.getMain(), psc.useRandomFoil()); if (myDeck.has(DeckSection.Sideboard)) { - preparePlayerLibrary(player, ZoneType.Sideboard, myDeck.get(DeckSection.Sideboard), psc.useRandomFoil()); + preparePlayerZone(player, ZoneType.Sideboard, myDeck.get(DeckSection.Sideboard), psc.useRandomFoil()); + + // Assign Companion + Card companion = player.assignCompanion(game, person); + // Create an effect that lets you cast your companion from your sideboard + if (companion != null) { + PlayerZone commandZone = player.getZone(ZoneType.Command); + companion = game.getAction().moveTo(ZoneType.Command, companion, null); + commandZone.add(Player.createCompanionEffect(game, companion)); + + player.updateZoneForView(commandZone); + } } player.initVariantsZones(psc); player.shuffle(null); - if (isFirstGame) { Collection cardsComplained = player.getController().complainCardsCantPlayWell(myDeck); if (null != cardsComplained) { rAICards.putAll(player, cardsComplained); } + } else { + //reset cards to fix weird issues on netplay nextgame client + for (Card c : player.getCardsIn(ZoneType.Library)) { + c.setTapped(false); + c.resetActivationsPerTurn(); + } } if (myRemovedAnteCards != null && !myRemovedAnteCards.isEmpty()) { diff --git a/forge-game/src/main/java/forge/game/StaticEffect.java b/forge-game/src/main/java/forge/game/StaticEffect.java index dc9d51094a0..d6ce1813d7e 100644 --- a/forge-game/src/main/java/forge/game/StaticEffect.java +++ b/forge-game/src/main/java/forge/game/StaticEffect.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -37,7 +37,7 @@ import com.google.common.collect.Maps; *

* StaticEffect class. *

- * + * * @author Forge * @version $Id$ */ @@ -72,7 +72,7 @@ public class StaticEffect { /** * setTimestamp TODO Write javadoc for this method. - * + * * @param t * a long */ @@ -82,7 +82,7 @@ public class StaticEffect { /** * getTimestamp. TODO Write javadoc for this method. - * + * * @return a long */ public final long getTimestamp() { @@ -93,7 +93,7 @@ public class StaticEffect { *

* Getter for the field source. *

- * + * * @return a {@link forge.game.card.Card} object. */ public final Card getSource() { @@ -104,7 +104,7 @@ public class StaticEffect { *

* Getter for the field affectedCards. *

- * + * * @return a {@link forge.CardList} object. */ public final CardCollectionView getAffectedCards() { @@ -115,7 +115,7 @@ public class StaticEffect { *

* Setter for the field affectedCards. *

- * + * * @param list * a {@link forge.CardList} object. */ @@ -125,7 +125,7 @@ public class StaticEffect { /** * Gets the affected players. - * + * * @return the affected players */ public final List getAffectedPlayers() { @@ -134,7 +134,7 @@ public class StaticEffect { /** * Sets the affected players. - * + * * @param list * the new affected players */ @@ -144,7 +144,7 @@ public class StaticEffect { /** * setParams. TODO Write javadoc for this method. - * + * * @param params * a HashMap */ @@ -154,7 +154,7 @@ public class StaticEffect { /** * Gets the params. - * + * * @return the params */ public final Map getParams() { @@ -171,13 +171,12 @@ public class StaticEffect { /** * Undo everything that was changed by this effect. - * + * * @return a {@link CardCollectionView} of all affected cards. */ final CardCollectionView remove() { final CardCollectionView affectedCards = getAffectedCards(); final List affectedPlayers = getAffectedPlayers(); - //final Map params = getParams(); String changeColorWordsTo = null; @@ -245,6 +244,10 @@ public class StaticEffect { p.removeMaxLandPlays(getTimestamp()); p.removeMaxLandPlaysInfinite(getTimestamp()); + + p.removeControlVote(getTimestamp()); + p.removeAdditionalVote(getTimestamp()); + p.removeAdditionalOptionalVote(getTimestamp()); } // modify the affected card diff --git a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java index 55bdfd789e6..7607398cd73 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java @@ -247,20 +247,7 @@ public final class AbilityFactory { if (mapParams.containsKey("SubAbility")) { final String name = mapParams.get("SubAbility"); - SpellAbility p = parent; - AbilitySub sub = null; - while (p != null) { - sub = p.getAdditionalAbility(name); - if (sub != null) { - break; - } - p = p.getParent(); - } - if (sub == null) { - sub = getSubAbility(state, name, spellAbility); - } - spellAbility.setSubAbility(sub); - spellAbility.setAdditionalAbility(name, sub); + spellAbility.setSubAbility(getSubAbility(state, name, spellAbility)); } for (final String key : additionalAbilityKeys) { 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 f5b40e4ca3a..cc160872bf6 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -13,6 +13,7 @@ import forge.card.mana.ManaCostParser; import forge.card.mana.ManaCostShard; import forge.game.CardTraitBase; import forge.game.Game; +import forge.game.GameEntity; import forge.game.GameObject; import forge.game.ability.AbilityFactory.AbilityRecordType; import forge.game.card.*; @@ -25,7 +26,10 @@ import forge.game.player.Player; import forge.game.player.PlayerCollection; import forge.game.player.PlayerPredicates; import forge.game.spellability.*; +import forge.game.trigger.Trigger; +import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; +import forge.util.Aggregates; import forge.util.Expressions; import forge.util.TextUtil; import forge.util.collect.FCollection; @@ -43,15 +47,16 @@ import java.util.regex.Pattern; public class AbilityUtils { - private final static ImmutableList cmpList = ImmutableList.of("LT", "LE", "EQ", "GE", "GT", "NE"); + private final static ImmutableList cmpList = ImmutableList.of("LT", "LE", "EQ", "GE", "GT", "NE"); public static CounterType getCounterType(String name, SpellAbility sa) throws Exception { CounterType counterType; if ("ReplacedCounterType".equals(name)) { - name = (String) sa.getReplacingObject(AbilityKey.CounterType); + name = (String) sa.getReplacingObject(AbilityKey.CounterType); } - try { + //try { counterType = CounterType.getType(name); + /* } catch (Exception e) { String type = sa.getSVar(name); if (type.equals("")) { @@ -63,6 +68,7 @@ public class AbilityUtils { } counterType = CounterType.getType(type); } + //*/ return counterType; } @@ -202,6 +208,17 @@ public class AbilityUtils { cards.add((Card) o); } } + } else if (defined.equals("DelayTriggerRememberedLKI")) { + SpellAbility trigSa = sa.getTriggeringAbility(); + if (trigSa != null) { + for (Object o : trigSa.getTriggerRemembered()) { + if (o instanceof Card) { + cards.add((Card)o); + } + } + } else { + System.err.println("Warning: couldn't find trigger SA in the chain of SpellAbility " + sa); + } } else if (defined.equals("DelayTriggerRemembered")) { SpellAbility trigSa = sa.getTriggeringAbility(); if (trigSa != null) { @@ -270,6 +287,8 @@ public class AbilityUtils { list = sa.getRootAbility().getPaidList("ExiledCards"); } else if (defined.startsWith("Exiled")) { list = sa.getRootAbility().getPaidList("Exiled"); + } else if (defined.startsWith("Milled")) { + list = sa.getRootAbility().getPaidList("Milled"); } else if (defined.startsWith("TappedCards")) { list = sa.getRootAbility().getPaidList("TappedCards"); } else if (defined.startsWith("Tapped")) { @@ -696,6 +715,16 @@ public class AbilityUtils { list = null; } } + else if (calcX[0].startsWith("TriggerRemembered")) { + final SpellAbility root = sa.getRootAbility(); + CardCollection result = new CardCollection(); + for (Object o : root.getTriggerRemembered()) { + if (o instanceof Card) { + result.add((Card) o); + } + } + list = result; + } else if (calcX[0].startsWith("TriggerObjects")) { final SpellAbility root = sa.getRootAbility(); list = (CardCollection) root.getTriggeringObject(AbilityKey.fromString(calcX[0].substring(14))); @@ -751,9 +780,18 @@ public class AbilityUtils { final FCollection objects = new FCollection<>(); final String defined = (def == null) ? "Self" : def; - objects.addAll(AbilityUtils.getDefinedPlayers(card, defined, sa)); + objects.addAll(getDefinedPlayers(card, defined, sa)); + objects.addAll(getDefinedCards(card, defined, sa)); + objects.addAll(getDefinedSpellAbilities(card, defined, sa)); + return objects; + } + + public static FCollection getDefinedEntities(final Card card, final String def, final SpellAbility sa) { + final FCollection objects = new FCollection<>(); + final String defined = (def == null) ? "Self" : def; + + objects.addAll(getDefinedPlayers(card, defined, sa)); objects.addAll(getDefinedCards(card, defined, sa)); - objects.addAll(AbilityUtils.getDefinedSpellAbilities(card, defined, sa)); return objects; } @@ -878,8 +916,8 @@ public class AbilityUtils { } } if (sa.hasParam("AbilityCount")) { // replace specific string other than "EQ" cases - String var = sa.getParam("AbilityCount"); - valid = TextUtil.fastReplace(valid, var, Integer.toString(calculateAmount(source, var, sa))); + String var = sa.getParam("AbilityCount"); + valid = TextUtil.fastReplace(valid, var, Integer.toString(calculateAmount(source, var, sa))); } return CardLists.getValidCards(list, valid.split(","), sa.getActivatingPlayer(), source, sa); } @@ -1187,7 +1225,7 @@ public class AbilityUtils { } } else if (defined.equals("ActivePlayer")) { - players.add(game.getPhaseHandler().getPlayerTurn()); + players.add(game.getPhaseHandler().getPlayerTurn()); } else if (defined.equals("You")) { players.add(player); @@ -1321,7 +1359,7 @@ public class AbilityUtils { Player pl = sa.getActivatingPlayer(); final Game game = pl.getGame(); - if (sa.isTrigger() && sa.getParent() == null && sa.getPayCosts() != null) { + if (sa.isTrigger() && sa.getParent() == null) { // when trigger cost are paid before the effect does resolve, need to clean the trigger game.getTriggerHandler().resetActiveTriggers(); } @@ -1416,11 +1454,11 @@ public class AbilityUtils { } } else if (unlessCost.equals("ChosenManaCost")) { - if (!source.hasChosenCard()) { + if (!source.hasChosenCard()) { cost = new Cost(ManaCost.ZERO, true); } - else { - cost = new Cost(Iterables.getFirst(source.getChosenCards(), null).getManaCost(), true); + else { + cost = new Cost(Iterables.getFirst(source.getChosenCards(), null).getManaCost(), true); } } else if (unlessCost.equals("ChosenNumber")) { @@ -1573,11 +1611,55 @@ public class AbilityUtils { // special logic for xPaid in SpellAbility if (sq[0].contains("xPaid")) { - // ETB effects of cloned cards have xPaid = 0 - if (sa.hasParam("ETB") && sa.getOriginalHost() != null) { - return 0; + SpellAbility root = sa.getRootAbility(); + + // 107.3i If an object gains an ability, the value of X within that ability is the value defined by that ability, + // or 0 if that ability doesn’t define a value of X. This is an exception to rule 107.3h. This may occur with ability-adding effects, text-changing effects, or copy effects. + if (root.getXManaCostPaid() != null) { + return CardFactoryUtil.doXMath(root.getXManaCostPaid(), expr, c); } - return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + + // If the chosen creature has X in its mana cost, that X is considered to be 0. + // The value of X in Altered Ego’s last ability will be whatever value was chosen for X while casting Altered Ego. + if (sa.getOriginalHost() != null || !sa.getHostCard().equals(c)) { + return CardFactoryUtil.doXMath(0, expr, c); + } + + if (root.isTrigger()) { + Trigger t = root.getTrigger(); + if (t == null) { + return CardFactoryUtil.doXMath(0, expr, c); + } + + // 107.3k If an object’s enters-the-battlefield triggered ability or replacement effect refers to X, + // and the spell that became that object as it resolved had a value of X chosen for any of its costs, + // the value of X for that ability is the same as the value of X for that spell, although the value of X for that permanent is 0. + if (TriggerType.ChangesZone.equals(t.getMode()) + && ZoneType.Battlefield.name().equals(t.getParam("Destination"))) { + return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + } else if (TriggerType.SpellCast.equals(t.getMode())) { + // Cast Trigger like Hydroid Krasis + SpellAbility castSA = (SpellAbility) root.getTriggeringObject(AbilityKey.SpellAbility); + if (castSA == null || castSA.getXManaCostPaid() == null) { + return CardFactoryUtil.doXMath(0, expr, c); + } + return CardFactoryUtil.doXMath(castSA.getXManaCostPaid(), expr, c); + } else if (TriggerType.Cycled.equals(t.getMode())) { + SpellAbility cycleSA = (SpellAbility) sa.getTriggeringObject(AbilityKey.Cause); + if (cycleSA == null || cycleSA.getXManaCostPaid() == null) { + return CardFactoryUtil.doXMath(0, expr, c); + } + return CardFactoryUtil.doXMath(cycleSA.getXManaCostPaid(), expr, c); + } + } + + if (root.isReplacementAbility()) { + if (sa.hasParam("ETB")) { + return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + } + } + + return CardFactoryUtil.doXMath(0, expr, c); } // Count$Kicked.. @@ -1652,22 +1734,26 @@ public class AbilityUtils { if (l[0].startsWith("LastStateBattlefield")) { final String[] k = l[0].split(" "); CardCollectionView list = null; - if (sa.getLastStateBattlefield() != null && !sa.getLastStateBattlefield().isEmpty()) { - list = new CardCollection(sa.getLastStateBattlefield()); + if (sa.getLastStateBattlefield() != null) { + list = sa.getLastStateBattlefield(); } else { // LastState is Empty - list = sa.getHostCard().getGame().getCardsIn(ZoneType.Battlefield); + return CardFactoryUtil.doXMath(0, expr, c); } list = CardLists.getValidCards(list, k[1].split(","), sa.getActivatingPlayer(), c, sa); - return CardFactoryUtil.doXMath(list.size(), expr, c); + if (k[0].contains("TotalToughness")) { + return CardFactoryUtil.doXMath(Aggregates.sum(list, CardPredicates.Accessors.fnGetNetToughness), expr, c); + } else { + return CardFactoryUtil.doXMath(list.size(), expr, c); + } } if (l[0].startsWith("LastStateGraveyard")) { final String[] k = l[0].split(" "); CardCollectionView list = null; - if (sa.getLastStateGraveyard() != null && !sa.getLastStateGraveyard().isEmpty()) { - list = new CardCollection(sa.getLastStateGraveyard()); + if (sa.getLastStateGraveyard() != null) { + list = sa.getLastStateGraveyard(); } else { // LastState is Empty - list = sa.getHostCard().getGame().getCardsIn(ZoneType.Graveyard); + return CardFactoryUtil.doXMath(0, expr, c); } list = CardLists.getValidCards(list, k[1].split(","), sa.getActivatingPlayer(), c, sa); return CardFactoryUtil.doXMath(list.size(), expr, c); @@ -1793,7 +1879,7 @@ public class AbilityUtils { // Color should not replace itself. if (e.getValue().equalsIgnoreCase(colorLowerCase)) { continue; - } + } value = getReplacedText(colorLowerCase, e.getValue(), isDescriptive); replaced = replaced.replaceAll("(?)" + colorLowerCase, value.toLowerCase()); value = getReplacedText(colorCaptCase, e.getValue(), isDescriptive); diff --git a/forge-game/src/main/java/forge/game/ability/ApiType.java b/forge-game/src/main/java/forge/game/ability/ApiType.java index e058a41c2c0..30f174e7da7 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -40,6 +40,7 @@ public enum ApiType { ChooseCard (ChooseCardEffect.class), ChooseColor (ChooseColorEffect.class), ChooseDirection (ChooseDirectionEffect.class), + ChooseEvenOdd (ChooseEvenOddEffect.class), ChooseNumber (ChooseNumberEffect.class), ChoosePlayer (ChoosePlayerEffect.class), ChooseSource (ChooseSourceEffect.class), @@ -47,6 +48,7 @@ public enum ApiType { Clash (ClashEffect.class), Cleanup (CleanUpEffect.class), Clone (CloneEffect.class), + CompanionChoose (ChooseCompanionEffect.class), CopyPermanent (CopyPermanentEffect.class), CopySpellAbility (CopySpellAbilityEffect.class), ControlSpell (ControlSpellEffect.class), @@ -88,6 +90,7 @@ public enum ApiType { GenericChoice (ChooseGenericEffect.class), Goad (GoadEffect.class), Haunt (HauntEffect.class), + Investigate (InvestigateEffect.class), ImmediateTrigger (ImmediateTriggerEffect.class), LookAt (LookAtEffect.class), LoseLife (LifeLoseEffect.class), diff --git a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java index d90b9c90396..ae557bfd3fe 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java @@ -13,6 +13,7 @@ import com.google.common.collect.Lists; import forge.GameCommand; import forge.card.CardType; import forge.game.Game; +import forge.game.GameEntity; import forge.game.GameObject; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -34,7 +35,7 @@ import forge.util.collect.FCollection; *

* AbilityFactory_AlterLife class. *

- * + * * @author Forge * @version $Id: AbilityFactoryAlterLife.java 17656 2012-10-22 19:32:56Z Max mtg $ */ @@ -93,7 +94,7 @@ public abstract class SpellAbilityEffect { final String baseDesc = this.getStackDescription(sa); if (conditionDesc != null) { sb.append(conditionDesc).append(" "); - } + } sb.append(baseDesc); } @@ -112,9 +113,8 @@ public abstract class SpellAbilityEffect { sb.append(" "); sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace(svar,"=",String.valueOf(amount)))); } else{ - if (sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null && - sa.getPayCosts().getCostMana().getAmountOfX() > 0) { - int amount = sa.getHostCard().getXManaCostPaid(); + if (sa.costHasManaX()) { + int amount = sa.getXManaCostPaid() == null ? 0 : sa.getXManaCostPaid(); sb.append(" "); sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace("X","=",String.valueOf(amount)))); } @@ -126,7 +126,7 @@ public abstract class SpellAbilityEffect { /** * Append the description of a {@link SpellAbility} to a * {@link StringBuilder}. - * + * * @param sa * a {@link SpellAbility}. * @param sb @@ -174,7 +174,7 @@ public abstract class SpellAbilityEffect { private static CardCollection getCards(final boolean definedFirst, final String definedParam, final SpellAbility sa) { final boolean useTargets = sa.usesTargeting() && (!definedFirst || !sa.hasParam(definedParam)); - return useTargets ? new CardCollection(sa.getTargets().getTargetCards()) + return useTargets ? new CardCollection(sa.getTargets().getTargetCards()) : AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam(definedParam), sa); } @@ -197,10 +197,22 @@ public abstract class SpellAbilityEffect { private static List getSpells(final boolean definedFirst, final String definedParam, final SpellAbility sa) { final boolean useTargets = sa.usesTargeting() && (!definedFirst || !sa.hasParam(definedParam)); - return useTargets ? Lists.newArrayList(sa.getTargets().getTargetSpells()) + return useTargets ? Lists.newArrayList(sa.getTargets().getTargetSpells()) : AbilityUtils.getDefinedSpellAbilities(sa.getHostCard(), sa.getParam(definedParam), sa); } + + // Targets of card or player type + protected final static List getTargetEntities(final SpellAbility sa) { return getEntities(false, "Defined", sa); } + protected final static List getTargetEntities(final SpellAbility sa, final String definedParam) { return getEntities(false, definedParam, sa); } + protected final static List getDefinedEntitiesOrTargeted(SpellAbility sa, final String definedParam) { return getEntities(true, definedParam, sa); } + + private static List getEntities(final boolean definedFirst, final String definedParam, final SpellAbility sa) { + final boolean useTargets = sa.usesTargeting() && (!definedFirst || !sa.hasParam(definedParam)); + return useTargets ? Lists.newArrayList(sa.getTargets().getTargetEntities()) + : AbilityUtils.getDefinedEntities(sa.getHostCard(), sa.getParam(definedParam), sa); + } + // Targets of unspecified type protected final static List getTargets(final SpellAbility sa) { return getTargetables(false, "Defined", sa); } protected final static List getTargets(final SpellAbility sa, final String definedParam) { return getTargetables(false, definedParam, sa); } @@ -208,10 +220,10 @@ public abstract class SpellAbilityEffect { private static List getTargetables(final boolean definedFirst, final String definedParam, final SpellAbility sa) { final boolean useTargets = sa.usesTargeting() && (!definedFirst || !sa.hasParam(definedParam)); - return useTargets ? Lists.newArrayList(sa.getTargets().getTargets()) + return useTargets ? Lists.newArrayList(sa.getTargets().getTargets()) : AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam(definedParam), sa); } - + protected static void registerDelayedTrigger(final SpellAbility sa, String location, final List crds) { boolean intrinsic = sa.isIntrinsic(); @@ -219,14 +231,14 @@ public abstract class SpellAbilityEffect { boolean combat = location.endsWith("Combat"); String desc = sa.hasParam("AtEOTDesc") ? sa.getParam("AtEOTDesc") : ""; - + if (your) { location = location.substring("Your".length()); } if (combat) { location = location.substring(0, location.length() - "Combat".length()); } - + if (desc.isEmpty()) { StringBuilder sb = new StringBuilder(); if (location.equals("Hand")) { @@ -254,12 +266,12 @@ public abstract class SpellAbilityEffect { StringBuilder delTrig = new StringBuilder(); delTrig.append("Mode$ Phase | Phase$ "); delTrig.append(combat ? "EndCombat " : "End Of Turn "); - + if (your) { delTrig.append("| ValidPlayer$ You "); } delTrig.append("| TriggerDescription$ ").append(desc); - + final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), sa.getHostCard(), intrinsic); for (final Card c : crds) { trig.addRemembered(c); @@ -290,7 +302,7 @@ public abstract class SpellAbilityEffect { trig.setOverridingAbility(newSa); sa.getActivatingPlayer().getGame().getTriggerHandler().registerDelayedTrigger(trig); } - + protected static void addSelfTrigger(final SpellAbility sa, String location, final Card card) { String trigStr = "Mode$ Phase | Phase$ End of Turn | TriggerZones$ Battlefield " + @@ -312,17 +324,17 @@ public abstract class SpellAbilityEffect { card.setSVar("EndOfTurnLeavePlay", "AtEOT"); } } - + protected static void addForgetOnMovedTrigger(final Card card, final String zone) { String trig = "Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ " + zone + " | Destination$ Any | TriggerZones$ Command | Static$ True"; String forgetEffect = "DB$ Pump | ForgetObjects$ TriggeredCard"; String exileEffect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile" + " | ConditionDefined$ Remembered | ConditionPresent$ Card | ConditionCompare$ EQ0"; - + SpellAbility saForget = AbilityFactory.getAbility(forgetEffect, card); AbilitySub saExile = (AbilitySub) AbilityFactory.getAbility(exileEffect, card); saForget.setSubAbility(saExile); - + final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true); parsedTrigger.setOverridingAbility(saForget); final Trigger addedTrigger = card.addTrigger(parsedTrigger); @@ -337,22 +349,22 @@ public abstract class SpellAbilityEffect { final Trigger addedTrigger = card.addTrigger(parsedTrigger); addedTrigger.setIntrinsic(true); } - + protected static void addForgetCounterTrigger(final Card card, final String counterType) { String trig = "Mode$ CounterRemoved | TriggerZones$ Command | ValidCard$ Card.IsRemembered | CounterType$ " + counterType + " | NewCounterAmount$ 0 | Static$ True"; String forgetEffect = "DB$ Pump | ForgetObjects$ TriggeredCard"; String exileEffect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile" + " | ConditionDefined$ Remembered | ConditionPresent$ Card | ConditionCompare$ EQ0"; - + SpellAbility saForget = AbilityFactory.getAbility(forgetEffect, card); AbilitySub saExile = (AbilitySub) AbilityFactory.getAbility(exileEffect, card); saForget.setSubAbility(saExile); - + final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true); parsedTrigger.setOverridingAbility(saForget); final Trigger addedTrigger = card.addTrigger(parsedTrigger); - addedTrigger.setIntrinsic(true); + addedTrigger.setIntrinsic(true); } protected static void addLeaveBattlefieldReplacement(final Card card, final SpellAbility sa, final String zone) { @@ -379,10 +391,10 @@ public abstract class SpellAbilityEffect { game.getAction().moveTo(ZoneType.Command, eff, sa); game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone); } - + protected static void addLeaveBattlefieldReplacement(final Card eff, final String zone) { final String repeffstr = "Event$ Moved | ValidCard$ Card.IsRemembered " - + "| Origin$ Battlefield | ExcludeDestination$ " + zone + + "| Origin$ Battlefield | ExcludeDestination$ " + zone + "| Description$ If Creature would leave the battlefield, " + " exile it instead of putting it anywhere else."; String effect = "DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Battlefield | Destination$ " + zone; @@ -393,7 +405,7 @@ public abstract class SpellAbilityEffect { re.setOverridingAbility(AbilityFactory.getAbility(effect, eff)); eff.addReplacementEffect(re); } - + // create a basic template for Effect to be used somewhere else protected static Card createEffect(final SpellAbility sa, final Player controller, final String name, final String image) { @@ -412,7 +424,6 @@ public abstract class SpellAbilityEffect { } } } - eff.setToken(true); // Set token to true, so when leaving play it gets nuked eff.setOwner(controller); eff.setImageKey(image); diff --git a/forge-game/src/main/java/forge/game/ability/effects/AmassEffect.java b/forge-game/src/main/java/forge/game/ability/effects/AmassEffect.java index b7dd51336e9..1612c2ae3da 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AmassEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AmassEffect.java @@ -1,16 +1,23 @@ package forge.game.ability.effects; +import java.util.Map; + +import org.apache.commons.lang3.mutable.MutableBoolean; + +import com.google.common.collect.Maps; + import forge.game.Game; import forge.game.GameEntityCounterTable; import forge.game.ability.AbilityUtils; -import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.card.CardZoneTable; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.card.token.TokenInfo; +import forge.game.event.GameEventCombatChanged; import forge.game.event.GameEventTokenCreated; import forge.game.player.Player; import forge.game.player.PlayerController; @@ -19,7 +26,7 @@ import forge.game.zone.ZoneType; import forge.util.Lang; import forge.util.Localizer; -public class AmassEffect extends SpellAbilityEffect { +public class AmassEffect extends TokenEffectBase { @Override protected String getStackDescription(SpellAbility sa) { @@ -57,21 +64,14 @@ public class AmassEffect extends SpellAbilityEffect { useZoneTable = true; } + MutableBoolean combatChanged = new MutableBoolean(false); // create army token if needed if (CardLists.count(activator.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Army")) == 0) { final String tokenScript = "b_0_0_zombie_army"; - final Card prototype = TokenInfo.getProtoType(tokenScript, sa); + final Card prototype = TokenInfo.getProtoType(tokenScript, sa, false); - for (final Card tok : TokenInfo.makeTokensFromPrototype(prototype, activator, 1, true)) { - - // Should this be catching the Card that's returned? - Card c = game.getAction().moveToPlay(tok, sa); - if (c.getZone() != null) { - triggerList.put(ZoneType.None, c.getZone().getZoneType(), c); - } - c.updateStateForView(); - } + makeTokens(prototype, activator, sa, 1, true, false, triggerList, combatChanged); if (!useZoneTable) { triggerList.triggerChangesZoneAll(game); @@ -80,12 +80,20 @@ public class AmassEffect extends SpellAbilityEffect { game.fireEvent(new GameEventTokenCreated()); } + if (combatChanged.isTrue()) { + game.updateCombatForView(); + game.fireEvent(new GameEventCombatChanged()); + } + Map params = Maps.newHashMap(); + params.put("CounterType", CounterType.get(CounterEnumType.P1P1)); + params.put("Amount", 1); + CardCollectionView tgtCards = CardLists.getType(activator.getCardsIn(ZoneType.Battlefield), "Army"); - tgtCards = pc.chooseCardsForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblChooseAnArmy"), 1, 1, false); + tgtCards = pc.chooseCardsForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblChooseAnArmy"), 1, 1, false, params); GameEntityCounterTable table = new GameEntityCounterTable(); for(final Card tgtCard : tgtCards) { - tgtCard.addCounter(CounterType.P1P1, amount, activator, true, table); + tgtCard.addCounter(CounterEnumType.P1P1, amount, activator, true, table); game.updateLastStateForCard(tgtCard); if (remember) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/AttachEffect.java b/forge-game/src/main/java/forge/game/ability/effects/AttachEffect.java index ff0c74c1020..cd8325cdba9 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AttachEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AttachEffect.java @@ -35,7 +35,8 @@ public class AttachEffect extends SpellAbilityEffect { sa.setHostCard(c); } - Card source = sa.getHostCard(); + final Card source = sa.getHostCard(); + final Game game = source.getGame(); CardCollection attachments; final List targets = getDefinedOrTargeted(sa, "Defined"); @@ -57,15 +58,22 @@ public class AttachEffect extends SpellAbilityEffect { final Player p = sa.getActivatingPlayer(); - if (sa.hasParam("Object")) { - attachments = AbilityUtils.getDefinedCards(source, sa.getParam("Object"), sa); - if (sa.hasParam("ChooseAnObject")) { - Card c = p.getController().chooseSingleEntityForEffect(attachments, sa, sa.getParam("ChooseAnObject")); - attachments.clear(); - if (c != null) { - attachments.add(c); - } + if (sa.hasParam("Choices")) { + ZoneType choiceZone = ZoneType.Battlefield; + if (sa.hasParam("ChoiceZone")) { + choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone")); } + String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChoose") + " "; + + CardCollection choices = CardLists.getValidCards(game.getCardsIn(choiceZone), sa.getParam("Choices"), p, source, sa); + + Card c = p.getController().chooseSingleEntityForEffect(choices, sa, title, null); + if (c == null) { + return; + } + attachments = new CardCollection(c); + } else if (sa.hasParam("Object")) { + attachments = AbilityUtils.getDefinedCards(source, sa.getParam("Object"), sa); } else { attachments = new CardCollection(source); } @@ -185,7 +193,8 @@ public class AttachEffect extends SpellAbilityEffect { players.add(player); } } - final Player pa = p.getController().chooseSingleEntityForEffect(players, aura, Localizer.getInstance().getMessage("lblSelectAPlayerAttachSourceTo", CardTranslation.getTranslatedName(source.getName()))); + final Player pa = p.getController().chooseSingleEntityForEffect(players, aura, + Localizer.getInstance().getMessage("lblSelectAPlayerAttachSourceTo", CardTranslation.getTranslatedName(source.getName())), null); if (pa != null) { handleAura(source, pa); return true; @@ -198,7 +207,8 @@ public class AttachEffect extends SpellAbilityEffect { return false; } - final Card o = p.getController().chooseSingleEntityForEffect(list, aura, Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(source.getName()))); + final Card o = p.getController().chooseSingleEntityForEffect(list, aura, + Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(source.getName())), null); if (o != null) { handleAura(source, o); //source.enchantEntity((Card) o); diff --git a/forge-game/src/main/java/forge/game/ability/effects/BalanceEffect.java b/forge-game/src/main/java/forge/game/ability/effects/BalanceEffect.java index 311fd3ae2a0..83a0eaadad5 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/BalanceEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/BalanceEffect.java @@ -1,6 +1,7 @@ package forge.game.ability.effects; import forge.game.Game; +import forge.game.ability.AbilityKey; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -8,11 +9,13 @@ import forge.game.card.CardLists; import forge.game.card.CardZoneTable; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.collect.FCollectionView; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * TODO: Write javadoc for this type. @@ -50,12 +53,24 @@ public class BalanceEffect extends SpellAbilityEffect { continue; } if (zone.equals(ZoneType.Hand)) { + boolean firstDiscard = p.getNumDiscardedThisTurn() == 0; + final CardCollection discardedByPlayer = new CardCollection(); for (Card card : p.getController().chooseCardsToDiscardFrom(p, sa, validCards.get(i), numToBalance, numToBalance)) { if ( null == card ) continue; - p.discard(card, sa, table); + if (p.discard(card, sa, table) != null) { + discardedByPlayer.add(card); + } + } + + if (!discardedByPlayer.isEmpty()) { + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Player, p); + runParams.put(AbilityKey.Cards, discardedByPlayer); + runParams.put(AbilityKey.Cause, sa); + runParams.put(AbilityKey.FirstTime, firstDiscard); + game.getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false); } } else { // Battlefield - // TODO: "can'e be sacrificed" for(Card card : p.getController().choosePermanentsToSacrifice(sa, numToBalance, numToBalance, validCards.get(i), valid)) { if ( null == card ) continue; game.getAction().sacrifice(card, sa, table); diff --git a/forge-game/src/main/java/forge/game/ability/effects/BondEffect.java b/forge-game/src/main/java/forge/game/ability/effects/BondEffect.java index b41e6a265c5..4d9789bac9f 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/BondEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/BondEffect.java @@ -31,7 +31,7 @@ public class BondEffect extends SpellAbilityEffect { Card partner = cards.getFirst(); // skip choice if only one card on list if (cards.size() > 1) { - partner = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(cards, sa, Localizer.getInstance().getMessage("lblSelectACardPair")); + partner = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(cards, sa, Localizer.getInstance().getMessage("lblSelectACardPair"), null); } // pair choices together diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeCombatantsEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeCombatantsEffect.java index 5d9d68b9a53..0b807bdd2bc 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeCombatantsEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeCombatantsEffect.java @@ -18,7 +18,10 @@ import forge.util.CardTranslation; import org.apache.commons.lang3.StringUtils; +import com.google.common.collect.Maps; + import java.util.List; +import java.util.Map; public class ChangeCombatantsEffect extends SpellAbilityEffect { @@ -45,8 +48,12 @@ public class ChangeCombatantsEffect extends SpellAbilityEffect { final Combat combat = game.getCombat(); final GameEntity originalDefender = combat.getDefenderByAttacker(c); final FCollectionView defs = combat.getDefenders(); - final GameEntity defender = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(defs, sa, - Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())), false); + + String title = Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())); + Map params = Maps.newHashMap(); + params.put("Attacker", c); + + final GameEntity defender = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(defs, sa, title, false, params); if (originalDefender != null && !originalDefender.equals(defender)) { AttackingBand ab = combat.getBandOfAttacker(c); if (ab != null) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java index c68a48b9836..8711a63ad8e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java @@ -507,7 +507,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect { list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("AttachedTo"), tgtC.getController(), tgtC); } if (!list.isEmpty()) { - Card attachedTo = player.getController().chooseSingleEntityForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", tgtC.toString())); + Map params = Maps.newHashMap(); + params.put("Attach", tgtC); + Card attachedTo = player.getController().chooseSingleEntityForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", tgtC.toString()), params); tgtC.attachToEntity(attachedTo); } else { // When it should enter the battlefield attached to an illegal permanent it fails continue; @@ -517,7 +519,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect { if (sa.hasParam("AttachedToPlayer")) { FCollectionView list = AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("AttachedToPlayer"), sa); if (!list.isEmpty()) { - Player attachedTo = player.getController().chooseSingleEntityForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectAPlayerAttachSourceTo", tgtC.toString())); + Map params = Maps.newHashMap(); + params.put("Attach", tgtC); + Player attachedTo = player.getController().chooseSingleEntityForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectAPlayerAttachSourceTo", tgtC.toString()), params); tgtC.attachToEntity(attachedTo); } else { // When it should enter the battlefield attached to an illegal player it fails @@ -578,7 +582,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } } } else { - defender = player.getController().chooseSingleEntityForEffect(e, sa, Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(movedCard.getName()))); + String title = Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(movedCard.getName())); + Map params = Maps.newHashMap(); + params.put("Attacker", movedCard); + defender = player.getController().chooseSingleEntityForEffect(e, sa, title, params); } if (defender != null) { combat.addAttacker(movedCard, defender); @@ -1039,7 +1046,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect { if (!list.isEmpty()) { Card attachedTo = null; if (list.size() > 1) { - attachedTo = decider.getController().chooseSingleEntityForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName()))); + String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName())); + Map params = Maps.newHashMap(); + params.put("Attach", c); + attachedTo = decider.getController().chooseSingleEntityForEffect(list, sa, title, params); } else { attachedTo = list.get(0); @@ -1057,7 +1067,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect { if (sa.hasParam("AttachedToPlayer")) { FCollectionView list = AbilityUtils.getDefinedPlayers(source, sa.getParam("AttachedToPlayer"), sa); if (!list.isEmpty()) { - Player attachedTo = player.getController().chooseSingleEntityForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName()))); + String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName())); + Map params = Maps.newHashMap(); + params.put("Attach", c); + Player attachedTo = player.getController().chooseSingleEntityForEffect(list, sa, title, params); c.attachToEntity(attachedTo); } else { // When it should enter the battlefield attached to an illegal permanent it fails @@ -1080,7 +1093,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } } } else { - defender = player.getController().chooseSingleEntityForEffect(e, sa, Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName()))); + String title = Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())); + Map params = Maps.newHashMap(); + params.put("Attacker", c); + defender = player.getController().chooseSingleEntityForEffect(e, sa, title, params); } if (defender != null) { combat.addAttacker(c, defender); diff --git a/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java index ee1cca795e7..faed7543bcc 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java @@ -189,7 +189,7 @@ public class CharmEffect extends SpellAbilityEffect { //String choosers = sa.getParam("Chooser"); FCollection opponents = activator.getOpponents(); // all cards have Choser$ Opponent, so it's hardcoded here - chooser = activator.getController().chooseSingleEntityForEffect(opponents, sa, "Choose an opponent"); + chooser = activator.getController().chooseSingleEntityForEffect(opponents, sa, "Choose an opponent", null); source.setChosenPlayer(chooser); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseCardEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseCardEffect.java index 1a601c45a2a..3d55ced65bc 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChooseCardEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseCardEffect.java @@ -84,7 +84,7 @@ public class ChooseCardEffect extends SpellAbilityEffect { final CardCollectionView cl = CardLists.getType(land, type); if (!cl.isEmpty()) { final String prompt = Localizer.getInstance().getMessage("lblChoose") + " " + Lang.nounWithAmount(1, type); - Card c = p.getController().chooseSingleEntityForEffect(cl, sa, prompt, false); + Card c = p.getController().chooseSingleEntityForEffect(cl, sa, prompt, false, null); if (c != null) { chosen.add(c); } @@ -100,7 +100,7 @@ public class ChooseCardEffect extends SpellAbilityEffect { while (!creature.isEmpty()) { Card c = p.getController().chooseSingleEntityForEffect(creature, sa, Localizer.getInstance().getMessage("lblSelectCreatureWithTotalPowerLessOrEqualTo", (totP - chosenP - negativeNum)) - + "\r\n(" + Localizer.getInstance().getMessage("lblSelected") + ":" + chosenPool + ")\r\n(" + Localizer.getInstance().getMessage("lblTotalPowerNum", chosenP) + ")", chosenP <= totP); + + "\r\n(" + Localizer.getInstance().getMessage("lblSelected") + ":" + chosenPool + ")\r\n(" + Localizer.getInstance().getMessage("lblTotalPowerNum", chosenP) + ")", chosenP <= totP, null); if (c == null) { if (p.getController().confirmAction(sa, PlayerActionConfirmMode.OptionalChoose, Localizer.getInstance().getMessage("lblCancelChooseConfirm"))) { break; @@ -120,7 +120,7 @@ public class ChooseCardEffect extends SpellAbilityEffect { Aggregates.random(choices, validAmount, chosen); } else { String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChooseaCard") + " "; - chosen.addAll(p.getController().chooseCardsForEffect(choices, sa, title, minAmount, validAmount, !sa.hasParam("Mandatory"))); + chosen.addAll(p.getController().chooseCardsForEffect(choices, sa, title, minAmount, validAmount, !sa.hasParam("Mandatory"), null)); } } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseColorEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseColorEffect.java index 86c01b45ada..c308e29ee8a 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChooseColorEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseColorEffect.java @@ -40,6 +40,11 @@ public class ChooseColorEffect extends SpellAbilityEffect { String[] restrictedChoices = sa.getParam("Choices").split(","); colorChoices = Arrays.asList(restrictedChoices); } + if (sa.hasParam("Exclude")) { + for (String s : sa.getParam("Exclude").split(",")) { + colorChoices.remove(s); + } + } final List tgtPlayers = getTargetPlayers(sa); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseCompanionEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseCompanionEffect.java new file mode 100644 index 00000000000..1282dc98b89 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseCompanionEffect.java @@ -0,0 +1,12 @@ +package forge.game.ability.effects; + +import forge.game.ability.SpellAbilityEffect; +import forge.game.spellability.SpellAbility; + +public class ChooseCompanionEffect extends SpellAbilityEffect { + + @Override + public void resolve(SpellAbility sa) { + // This isn't a real effect. Just need it for AI choosing. + } +} \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseEvenOddEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseEvenOddEffect.java new file mode 100644 index 00000000000..6d7388ddfd4 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseEvenOddEffect.java @@ -0,0 +1,43 @@ +package forge.game.ability.effects; + +import forge.game.EvenOdd; +import forge.game.ability.SpellAbilityEffect; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.player.PlayerController.BinaryChoiceType; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +public class ChooseEvenOddEffect extends SpellAbilityEffect { + + /* (non-Javadoc) + * @see forge.card.abilityfactory.SpellEffect#getStackDescription(java.util.Map, forge.card.spellability.SpellAbility) + */ + @Override + protected String getStackDescription(SpellAbility sa) { + final StringBuilder sb = new StringBuilder(); + + for (final Player p : getTargetPlayers(sa)) { + sb.append(p).append(" "); + } + sb.append("chooses even or odd."); + + return sb.toString(); + } + + @Override + public void resolve(SpellAbility sa) { + final Card card = sa.getHostCard(); + + for (final Player p : getTargetPlayers(sa)) { + if ((!sa.usesTargeting()) || p.canBeTargetedBy(sa)) { + EvenOdd chosen = p.getController().chooseBinary(sa, "odd or even", BinaryChoiceType.OddsOrEvens) ? EvenOdd.Odd : EvenOdd.Even; + card.setChosenEvenOdd(chosen); + if (sa.hasParam("Notify")) { + p.getGame().getAction().nofityOfValue(sa, card, Localizer.getInstance().getMessage("lblPlayerPickedChosen", p.getName(), chosen), p); + } + } + } + card.updateStateForView(); + } +} \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java index c5a46346887..f5f59dfb4e5 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java @@ -8,8 +8,7 @@ import forge.game.card.Card; import forge.game.event.GameEventCardModeChosen; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.util.MyRandom; -import forge.util.Localizer; +import forge.util.Aggregates; import java.util.List; @@ -33,6 +32,7 @@ public class ChooseGenericEffect extends SpellAbilityEffect { final List abilities = Lists.newArrayList(sa.getAdditionalAbilityList("Choices")); final SpellAbility fallback = sa.getAdditionalAbility("FallbackAbility"); + final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ChoiceAmount", "1"), sa); final List tgtPlayers = getDefinedPlayersOrTargeted(sa); @@ -43,8 +43,7 @@ public class ChooseGenericEffect extends SpellAbilityEffect { for (SpellAbility saChoice : abilities) { if (!saChoice.getRestrictions().checkOtherRestrictions(host, saChoice, sa.getActivatingPlayer()) ) { saToRemove.add(saChoice); - } else if (saChoice.hasParam("UnlessCost") && - "Player.IsRemembered".equals(saChoice.getParam("Defined"))) { + } else if (saChoice.hasParam("UnlessCost")) { String unlessCost = saChoice.getParam("UnlessCost"); // Sac a permanent in presence of Sigarda, Host of Herons // TODO: generalize this by testing if the unless cost can be paid @@ -65,32 +64,32 @@ public class ChooseGenericEffect extends SpellAbilityEffect { continue; } - SpellAbility chosenSA = null; + List chosenSAs = Lists.newArrayList(); if (sa.hasParam("AtRandom")) { - int idxChosen = MyRandom.getRandom().nextInt(abilities.size()); - chosenSA = abilities.get(idxChosen); + Aggregates.random(abilities, amount, chosenSAs); } else { - chosenSA = p.getController().chooseSingleSpellForEffect(abilities, sa, Localizer.getInstance().getMessage("lblChooseOne"), - ImmutableMap.of()); + chosenSAs = p.getController().chooseSpellAbilitiesForEffect(abilities, sa, "Choose", amount, ImmutableMap.of()); } - - if (chosenSA != null) { - String chosenValue = chosenSA.getDescription(); - if (sa.hasParam("ShowChoice")) { - boolean dontNotifySelf = sa.getParam("ShowChoice").equals("ExceptSelf"); - p.getGame().getAction().nofityOfValue(sa, p, chosenValue, dontNotifySelf ? sa.getActivatingPlayer() : null); + + if (!chosenSAs.isEmpty()) { + for (SpellAbility chosenSA : chosenSAs) { + String chosenValue = chosenSA.getDescription(); + if (sa.hasParam("ShowChoice")) { + boolean dontNotifySelf = sa.getParam("ShowChoice").equals("ExceptSelf"); + p.getGame().getAction().nofityOfValue(sa, p, chosenValue, dontNotifySelf ? sa.getActivatingPlayer() : null); + } + if (sa.hasParam("SetChosenMode")) { + sa.getHostCard().setChosenMode(chosenValue); + } + p.getGame().fireEvent(new GameEventCardModeChosen(p, host.getName(), chosenValue, sa.hasParam("ShowChoice"))); + AbilityUtils.resolve(chosenSA); } - if (sa.hasParam("SetChosenMode")) { - sa.getHostCard().setChosenMode(chosenValue); - } - p.getGame().fireEvent(new GameEventCardModeChosen(p, host.getName(), chosenValue, sa.hasParam("ShowChoice"))); - AbilityUtils.resolve(chosenSA); } else { // no choices are valid, e.g. maybe all Unless costs are unpayable if (fallback != null) { p.getGame().fireEvent(new GameEventCardModeChosen(p, host.getName(), fallback.getDescription(), sa.hasParam("ShowChoice"))); AbilityUtils.resolve(fallback); - } else { + } else if (!sa.hasParam("AtRandom")) { System.err.println("Warning: all Unless costs were unpayable for " + host.getName() +", but it had no FallbackAbility defined. Doing nothing (this is most likely incorrect behavior)."); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChoosePlayerEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChoosePlayerEffect.java index 0d57e4a4c2c..6f25e23d7b4 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChoosePlayerEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChoosePlayerEffect.java @@ -51,7 +51,7 @@ public class ChoosePlayerEffect extends SpellAbilityEffect { if (random) { chosen = choices.isEmpty() ? null : Aggregates.random(choices); } else { - chosen = choices.isEmpty() ? null : p.getController().chooseSingleEntityForEffect(choices, sa, choiceDesc); + chosen = choices.isEmpty() ? null : p.getController().chooseSingleEntityForEffect(choices, sa, choiceDesc, null); } if( null != chosen ) { card.setChosenPlayer(chosen); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseSourceEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseSourceEffect.java index b35e17c0a25..34004b1a310 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChooseSourceEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseSourceEffect.java @@ -138,7 +138,7 @@ public class ChooseSourceEffect extends SpellAbilityEffect { final String choiceTitle = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChooseSource") + " "; Card o = null; do { - o = p.getController().chooseSingleEntityForEffect(sourcesToChooseFrom, sa, choiceTitle); + o = p.getController().chooseSingleEntityForEffect(sourcesToChooseFrom, sa, choiceTitle, null); } while (o == null); chosen.add(o); sourcesToChooseFrom.remove(o); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ClashEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ClashEffect.java index 191b1db43e9..a775ddd281d 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ClashEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ClashEffect.java @@ -75,7 +75,7 @@ public class ClashEffect extends SpellAbilityEffect { */ final Card source = sa.getHostCard(); final Player player = source.getController(); - final Player opponent = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(player.getOpponents(), sa, Localizer.getInstance().getMessage("lblChooseOpponent")) ; + final Player opponent = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(player.getOpponents(), sa, Localizer.getInstance().getMessage("lblChooseOpponent"), null); final ZoneType lib = ZoneType.Library; if (sa.hasParam("RememberClasher")) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/CloneEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CloneEffect.java index 9cf8118d66b..0b27812a57e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CloneEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CloneEffect.java @@ -81,7 +81,7 @@ public class CloneEffect extends SpellAbilityEffect { choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host); String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChooseaCard") + " "; - cardToCopy = activator.getController().chooseSingleEntityForEffect(choices, sa, title, false); + cardToCopy = activator.getController().chooseSingleEntityForEffect(choices, sa, title, false, null); } else if (sa.hasParam("Defined")) { List cloneSources = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa); if (!cloneSources.isEmpty()) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ControlExchangeVariantEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ControlExchangeVariantEffect.java index f39c38616d8..92db1453cb4 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ControlExchangeVariantEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ControlExchangeVariantEffect.java @@ -36,9 +36,9 @@ public class ControlExchangeVariantEffect extends SpellAbilityEffect { CardCollectionView list2 = AbilityUtils.filterListByType(player2.getCardsIn(zone), type, sa); int max = Math.min(list1.size(), list2.size()); // choose the same number of cards - CardCollectionView chosen1 = activator.getController().chooseCardsForEffect(list1, sa, Localizer.getInstance().getMessage("lblChooseCards") + ":" + player1, 0, max, true); + CardCollectionView chosen1 = activator.getController().chooseCardsForEffect(list1, sa, Localizer.getInstance().getMessage("lblChooseCards") + ":" + player1, 0, max, true, null); int num = chosen1.size(); - CardCollectionView chosen2 = activator.getController().chooseCardsForEffect(list2, sa, Localizer.getInstance().getMessage("lblChooseCards") + ":" + player2, num, num, true); + CardCollectionView chosen2 = activator.getController().chooseCardsForEffect(list2, sa, Localizer.getInstance().getMessage("lblChooseCards") + ":" + player2, num, num, true, null); // check all cards can be controlled by the other player for (final Card c : chosen1) { if (!c.canBeControlledBy(player2)) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java index a69a02623f3..e0e2f7ca921 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java @@ -2,8 +2,10 @@ package forge.game.ability.effects; import java.util.Arrays; import java.util.List; +import java.util.Map; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import forge.GameCommand; import forge.game.Game; @@ -211,9 +213,10 @@ public class ControlGainEffect extends SpellAbilityEffect { final Combat combat = game.getCombat(); if ( null != combat ) { final FCollectionView e = combat.getDefenders(); - - final GameEntity defender = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(e, sa, - Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(tgtC.getName()))); + String title = Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(tgtC.getName())); + Map params = Maps.newHashMap(); + params.put("Attacker", tgtC); + final GameEntity defender = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(e, sa, title, params); if (defender != null) { combat.addAttacker(tgtC, defender); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ControlGainVariantEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ControlGainVariantEffect.java index 44c2deddebe..b5e861c8cf7 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ControlGainVariantEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ControlGainVariantEffect.java @@ -1,13 +1,22 @@ package forge.game.ability.effects; +import java.util.Collections; +import java.util.Map; + +import com.google.common.collect.Maps; + +import forge.game.Direction; import forge.game.Game; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; +import forge.game.card.CardPredicates; import forge.game.player.Player; +import forge.game.player.PlayerCollection; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; +import forge.util.Aggregates; public class ControlGainVariantEffect extends SpellAbilityEffect { /* (non-Javadoc) @@ -20,29 +29,69 @@ public class ControlGainVariantEffect extends SpellAbilityEffect { @Override public void resolve(SpellAbility sa) { - // Aminatou, the Fateshifter (multiple players gain control of multiple permanents in an effect) - // Consider migrating cards with similar effects + // Multiple players gain control of multiple permanents in an effect // GainControl embedded in RepeatEach effects don't work well with timestamps final Card source = sa.getHostCard(); final Game game = source.getGame(); + long tStamp = game.getNextTimestamp(); + final String controller = sa.getParam("ChangeController"); + final Map gainControl = Maps.newHashMap(); // {newController, CardCollection} + final PlayerCollection players = game.getPlayers(); + + int aidx = players.indexOf(sa.getActivatingPlayer()); + if (aidx != -1) { + Collections.rotate(players, -aidx); + } CardCollection tgtCards = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("AllValid"), source.getController(), source); - if ("NextPlayerInChosenDirection".equals(sa.getParam("ChangeController")) && (source.getChosenDirection() != null) ) { - long tStamp = game.getNextTimestamp(); - for (final Player p : game.getPlayers()) { - - CardCollection valid = CardLists.filterControlledBy(tgtCards, game.getNextPlayerAfter(p, source.getChosenDirection())); - - for (Card tgtC : valid) { - if (!tgtC.isInPlay() || !tgtC.canBeControlledBy(p)) { - continue; - } - tgtC.setController(p, tStamp); - tgtCards.remove(tgtC); // remove from the list if controller is changed + if ("NextPlayerInChosenDirection".equals(controller) && (source.getChosenDirection() != null) ) {// Aminatou, the Fateshifter + for (final Player p : players) { + gainControl.put(p, CardLists.filterControlledBy(tgtCards, game.getNextPlayerAfter(p, source.getChosenDirection()))); + } + } else if ("CardOwner".equals(controller)) {// Homeward Path, Trostani Discordant etc. + for (final Player p : players) { + gainControl.put(p, CardLists.filter(tgtCards, CardPredicates.isOwner(p))); + } + } else if ("Random".equals(controller)) {// Scrambleverse + for (final Card c : tgtCards) { + final Player p = Aggregates.random(players); + if (gainControl.containsKey(p)) { + gainControl.get(p).add(0, c); + } else { + gainControl.put(p, new CardCollection(c)); } } + } else if ("ChooseNextPlayerInChosenDirection".equals(controller) && (source.getChosenDirection() != null)) {// Order of Succession + Player p = sa.getActivatingPlayer(); + do { + final CardCollection valid = CardLists.filterControlledBy(tgtCards, game.getNextPlayerAfter(p, source.getChosenDirection())); + final Card c = p.getController().chooseSingleEntityForEffect(valid, sa, " ", null); + if (c != null) { + gainControl.put(p, new CardCollection(c)); + } + p = game.getNextPlayerAfter(p, source.getChosenDirection()); + } while (!p.equals(sa.getActivatingPlayer())); + } else if ("ChooseFromPlayerToTheirRight".equals(controller)) {// Inniaz, the Gale Force + for (final Player p : players) { + final CardCollection valid = CardLists.filterControlledBy(tgtCards, game.getNextPlayerAfter(p, Direction.Right)); + final Card c = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(valid, sa, + "Choose one for the new Controller: " + p.getName(), null); + if (c != null) { + gainControl.put(p, new CardCollection(c)); + } + } + } + + for (Map.Entry e : gainControl.entrySet()) { + final Player newController = e.getKey(); + for (Card tgtC : e.getValue()) { + if (!tgtC.isInPlay() || !tgtC.canBeControlledBy(newController)) { + continue; + } + tgtC.setController(newController, tStamp); + } } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CopyPermanentEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CopyPermanentEffect.java index ce831f04f3a..c83bca2d61d 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CopyPermanentEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CopyPermanentEffect.java @@ -8,16 +8,12 @@ import com.google.common.collect.Lists; import forge.StaticData; import forge.card.CardRulesPredicates; import forge.game.Game; -import forge.game.GameEntity; import forge.game.ability.AbilityUtils; -import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollectionView; import forge.game.card.CardFactory; import forge.game.card.CardLists; import forge.game.card.CardZoneTable; -import forge.game.card.token.TokenInfo; -import forge.game.combat.Combat; import forge.game.event.GameEventCombatChanged; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -28,14 +24,14 @@ import forge.util.TextUtil; import forge.util.collect.FCollectionView; import forge.util.PredicateString.StringOp; import forge.util.Localizer; -import forge.util.CardTranslation; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.mutable.MutableBoolean; import java.util.Arrays; import java.util.List; -public class CopyPermanentEffect extends SpellAbilityEffect { +public class CopyPermanentEffect extends TokenEffectBase { @Override protected String getStackDescription(SpellAbility sa) { @@ -63,8 +59,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect { final Game game = host.getGame(); final List pumpKeywords = Lists.newArrayList(); - final long timestamp = game.getNextTimestamp(); - if (sa.hasParam("Optional")) { if (!activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblCopyPermanentConfirm"))) { return; @@ -149,7 +143,7 @@ public class CopyPermanentEffect extends SpellAbilityEffect { if (!choices.isEmpty()) { String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChooseaCard") +" "; - Card choosen = chooser.getController().chooseSingleEntityForEffect(choices, sa, title, false); + Card choosen = chooser.getController().chooseSingleEntityForEffect(choices, sa, title, false, null); if (choosen != null) { tgtCards.add(choosen); @@ -170,108 +164,26 @@ public class CopyPermanentEffect extends SpellAbilityEffect { useZoneTable = true; } + MutableBoolean combatChanged = new MutableBoolean(false); for (final Card c : tgtCards) { // if it only targets player, it already got all needed cards from defined - if (!sa.usesTargeting() || sa.getTargetRestrictions().canTgtPlayer() || c.canBeTargetedBy(sa)) { - Card proto = getProtoType(sa, c); - List token = TokenInfo.makeToken(proto, controller, true, numCopies); + if (sa.usesTargeting() && !sa.getTargetRestrictions().canTgtPlayer() && !c.canBeTargetedBy(sa)) { + continue; + } - final List crds = Lists.newArrayListWithCapacity(token.size()); - - for (final Card t : token) { - t.setCopiedPermanent(proto); - - // Temporarily register triggers of an object created with CopyPermanent - //game.getTriggerHandler().registerActiveTrigger(copy, false); - final Card copyInPlay = game.getAction().moveToPlay(t, sa); - - if (copyInPlay.getZone() != null) { - triggerList.put(ZoneType.None, copyInPlay.getZone().getZoneType(), copyInPlay); - } - - // when copying something stolen: - //copyInPlay.setSetCode(c.getSetCode()); - - copyInPlay.setCloneOrigin(host); - if (!pumpKeywords.isEmpty()) { - copyInPlay.addChangedCardKeywords(pumpKeywords, Lists.newArrayList(), false, false, timestamp); - } - crds.add(copyInPlay); - if (sa.hasParam("RememberCopied")) { - host.addRemembered(copyInPlay); - } - if (sa.hasParam("Tapped")) { - copyInPlay.setTapped(true); - } - if (sa.hasParam("CopyAttacking") && game.getPhaseHandler().inCombat()) { - final String attacked = sa.getParam("CopyAttacking"); - GameEntity defender; - if ("True".equals(attacked)) { - FCollectionView defs = game.getCombat().getDefenders(); - defender = c.getController().getController().chooseSingleEntityForEffect(defs, sa, Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())), false); - } else { - defender = AbilityUtils.getDefinedPlayers(host, sa.getParam("CopyAttacking"), sa).get(0); - if (sa.hasParam("ChoosePlayerOrPlaneswalker") && defender != null) { - FCollectionView defs = game.getCombat().getDefendersControlledBy((Player) defender); - defender = c.getController().getController().chooseSingleEntityForEffect(defs, sa, Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())) + " {" + Localizer.getInstance().getMessage("lblDefender") + ": " + defender + "}", false); - } - } - game.getCombat().addAttacker(copyInPlay, defender); - game.fireEvent(new GameEventCombatChanged()); - } - - if (sa.hasParam("CopyBlocking") && game.getPhaseHandler().inCombat() && copyInPlay.isCreature()) { - final Combat combat = game.getPhaseHandler().getCombat(); - final Card attacker = Iterables.getFirst(AbilityUtils.getDefinedCards(host, sa.getParam("CopyBlocking"), sa), null); - if (attacker != null) { - final boolean wasBlocked = combat.isBlocked(attacker); - combat.addBlocker(attacker, copyInPlay); - combat.orderAttackersForDamageAssignment(copyInPlay); - - // Add it to damage assignment order - if (!wasBlocked) { - combat.setBlocked(attacker, true); - combat.addBlockerToDamageAssignmentOrder(attacker, copyInPlay); - } - - game.fireEvent(new GameEventCombatChanged()); - } - } - - if (sa.hasParam("AttachedTo")) { - CardCollectionView list = AbilityUtils.getDefinedCards(host, sa.getParam("AttachedTo"), sa); - if (list.isEmpty()) { - list = copyInPlay.getController().getGame().getCardsIn(ZoneType.Battlefield); - list = CardLists.getValidCards(list, sa.getParam("AttachedTo"), copyInPlay.getController(), copyInPlay); - } - if (!list.isEmpty()) { - Card attachedTo = activator.getController().chooseSingleEntityForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", copyInPlay.toString())); - - copyInPlay.attachToEntity(attachedTo); - } else { - continue; - } - } - // need to be done otherwise the token has no State in Details - copyInPlay.updateStateForView(); - } - - if (sa.hasParam("AtEOT")) { - registerDelayedTrigger(sa, sa.getParam("AtEOT"), crds); - } - if (sa.hasParam("ImprintCopied")) { - host.addImprintedCards(crds); - } - } // end canBeTargetedBy + makeTokens(getProtoType(sa, c), controller, sa, numCopies, true, true, triggerList, combatChanged); } // end foreach Card if (!useZoneTable) { triggerList.triggerChangesZoneAll(game); triggerList.clear(); } + if (combatChanged.isTrue()) { + game.updateCombatForView(); + game.fireEvent(new GameEventCombatChanged()); + } } // end resolve - private Card getProtoType(final SpellAbility sa, final Card original) { final Card host = sa.getHostCard(); final Player newOwner = sa.getActivatingPlayer(); @@ -293,10 +205,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect { copy.setState(copy.getCurrentStateName(), true, true); copy.setToken(true); - if (sa.hasParam("AtEOTTrig")) { - addSelfTrigger(sa, sa.getParam("AtEOTTrig"), copy); - } - return copy; } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CopySpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CopySpellAbilityEffect.java index 9f050cb5b0d..43e13b56480 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CopySpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CopySpellAbilityEffect.java @@ -146,7 +146,7 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect { valid.remove(originalTarget); mayChooseNewTargets = false; if (sa.hasParam("ChooseOnlyOne")) { - Card choice = controller.getController().chooseSingleEntityForEffect(valid, sa, Localizer.getInstance().getMessage("lblChooseOne")); + Card choice = controller.getController().chooseSingleEntityForEffect(valid, sa, Localizer.getInstance().getMessage("lblChooseOne"), null); SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(card, chosenSA.getHostCard(), chosenSA, true); resetFirstTargetOnCopy(copy, choice, targetedSA); copies.add(copy); diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersMoveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersMoveEffect.java index 30245cc137b..9bef78eab96 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersMoveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersMoveEffect.java @@ -7,6 +7,7 @@ import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; +import forge.game.card.CardPredicates; import forge.game.card.CounterType; import forge.game.player.Player; import forge.game.player.PlayerController; @@ -26,17 +27,30 @@ public class CountersMoveEffect extends SpellAbilityEffect { @Override protected String getStackDescription(SpellAbility sa) { + final Card host = sa.getHostCard(); final StringBuilder sb = new StringBuilder(); - Card source = null; - List srcCards = getDefinedCardsOrTargeted(sa, "Source"); - - if (srcCards.size() > 0) { - source = srcCards.get(0); - } final List tgtCards = getDefinedCardsOrTargeted(sa); + + Card source = null; + if (sa.usesTargeting() && sa.getTargetRestrictions().getMinTargets(host, sa) == 2) { + if (tgtCards.size() < 2) { + return ""; + } + source = tgtCards.remove(0); + } else { + List srcCards = getDefinedCardsOrTargeted(sa, "Source"); + + if (srcCards.size() > 0) { + source = srcCards.get(0); + } + } final String countername = sa.getParam("CounterType"); - final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa); + final String counterAmount = sa.getParam("CounterNum"); + int amount = 0; + if (!"Any".equals(counterAmount) && !"All".equals(counterAmount)) { + amount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa); + } sb.append("Move "); if ("Any".matches(countername)) { @@ -45,16 +59,18 @@ public class CountersMoveEffect extends SpellAbilityEffect { } else { sb.append(amount).append(" ").append(" counter"); } - } else { + } else if ("All".equals(countername)) { + sb.append("all counter"); + } else { sb.append(amount).append(" ").append(countername).append(" counter"); } if (amount != 1) { sb.append("s"); } sb.append(" from ").append(source).append(" to "); - try{ + try { sb.append(tgtCards.get(0)); - } catch(final IndexOutOfBoundsException exception) { + } catch (final IndexOutOfBoundsException exception) { System.out.println(TextUtil.concatWithSpace("Somehow this is missing targets?", source.toString())); } @@ -70,12 +86,12 @@ public class CountersMoveEffect extends SpellAbilityEffect { final Player player = sa.getActivatingPlayer(); final PlayerController pc = player.getController(); final Game game = host.getGame(); - + CounterType cType = null; - try { - cType = AbilityUtils.getCounterType(counterName, sa); - } catch (Exception e) { - if (!counterName.matches("Any")) { + if (!counterName.matches("Any") && !counterName.matches("All")) { + try { + cType = AbilityUtils.getCounterType(counterName, sa); + } catch (Exception e) { System.out.println("Counter type doesn't match, nor does an SVar exist with the type name."); return; } @@ -89,17 +105,12 @@ public class CountersMoveEffect extends SpellAbilityEffect { CardCollectionView srcCards = game.getCardsIn(ZoneType.Battlefield); srcCards = CardLists.getValidCards(srcCards, sa.getParam("ValidSource"), player, host, sa); List tgtCards = getDefinedCardsOrTargeted(sa); - + if (tgtCards.isEmpty()) { return; } Card dest = tgtCards.get(0); - // target cant receive this counter type - if (!dest.canReceiveCounters(cType)) { - return; - } - Card cur = game.getCardState(dest, null); if (cur == null || !cur.equalsWithTimestamp(dest)) { // Test to see if the card we're trying to add is in the expected state @@ -107,48 +118,57 @@ public class CountersMoveEffect extends SpellAbilityEffect { } dest = cur; - int csum = 0; + Map params = Maps.newHashMap(); + params.put("Target", dest); - // only select cards if the counterNum is any - if (counterNum.equals("Any")) { - srcCards = player.getController().chooseCardsForEffect(srcCards, sa, Localizer.getInstance().getMessage("lblChooseTakeCountersCard", cType.getName()), 0, srcCards.size(), true); + if ("All".equals(counterName)) { + // only select cards if the counterNum is any + if (counterNum.equals("Any")) { + srcCards = CardLists.filter(srcCards, CardPredicates.hasCounters()); + srcCards = player.getController().chooseCardsForEffect(srcCards, sa, + Localizer.getInstance().getMessage("lblChooseTakeCountersCard", "any"), 0, + srcCards.size(), true, params); + } + } else { + // target cant receive this counter type + if (!dest.canReceiveCounters(cType)) { + return; + } + srcCards = CardLists.filter(srcCards, CardPredicates.hasCounter(cType)); + + // only select cards if the counterNum is any + if (counterNum.equals("Any")) { + params.put("CounterType", cType); + srcCards = player.getController().chooseCardsForEffect(srcCards, sa, + Localizer.getInstance().getMessage("lblChooseTakeCountersCard", cType.getName()), 0, + srcCards.size(), true, params); + } } + Map countersToAdd = Maps.newHashMap(); + for (Card src : srcCards) { - // rule 121.5: If the first and second objects are the same object, nothing happens + // rule 121.5: If the first and second objects are the same object, nothing + // happens if (src.equals(dest)) { continue; } - int cmax = src.getCounters(cType); - if (cmax <= 0) { - continue; - } - - int cnum = 0; - if (counterNum.equals("All")) { - cnum = cmax; - } else if (counterNum.equals("Any")) { - Map params = Maps.newHashMap(); - params.put("CounterType", cType); - params.put("Source", src); - params.put("Target", dest); - cnum = player.getController().chooseNumber(sa, Localizer.getInstance().getMessage("lblTakeHowManyTargetCounterFromCard", cType.getName(), CardTranslation.getTranslatedName(src.getName())), 0, cmax, params); + if ("All".equals(counterName)) { + final Map tgtCounters = Maps.newHashMap(src.getCounters()); + for (Map.Entry e : tgtCounters.entrySet()) { + removeCounter(sa, src, dest, e.getKey(), counterNum, countersToAdd); + } } else { - cnum = AbilityUtils.calculateAmount(host, counterNum, sa); - } - if(cnum > 0) { - src.subtractCounter(cType, cnum); - game.updateLastStateForCard(src); - csum += cnum; + removeCounter(sa, src, dest, cType, counterNum, countersToAdd); } } + for (Map.Entry e : countersToAdd.entrySet()) { + dest.addCounter(e.getKey(), e.getValue(), player, true, table); + } - if (csum > 0) { - dest.addCounter(cType, csum, player, true, table); - game.updateLastStateForCard(dest); - table.triggerCountersPutAll(game); - } + game.updateLastStateForCard(dest); + table.triggerCountersPutAll(game); return; } else if (sa.hasParam("ValidDefined")) { // one Source to many Targets @@ -163,18 +183,25 @@ public class CountersMoveEffect extends SpellAbilityEffect { if (source.getCounters(cType) <= 0) { return; } + Map params = Maps.newHashMap(); + params.put("CounterType", cType); + params.put("Source", source); + CardCollectionView tgtCards = game.getCardsIn(ZoneType.Battlefield); tgtCards = CardLists.getValidCards(tgtCards, sa.getParam("ValidDefined"), player, host, sa); if (counterNum.equals("Any")) { - tgtCards = player.getController().chooseCardsForEffect(tgtCards, sa, - Localizer.getInstance().getMessage("lblChooseCardToGetCountersFrom", cType.getName(), CardTranslation.getTranslatedName(source.getName())), 0, tgtCards.size(), true); + tgtCards = player.getController().chooseCardsForEffect( + tgtCards, sa, Localizer.getInstance().getMessage("lblChooseCardToGetCountersFrom", + cType.getName(), CardTranslation.getTranslatedName(source.getName())), + 0, tgtCards.size(), true, params); } boolean updateSource = false; for (final Card dest : tgtCards) { - // rule 121.5: If the first and second objects are the same object, nothing happens + // rule 121.5: If the first and second objects are the same object, nothing + // happens if (source.equals(dest)) { continue; } @@ -188,11 +215,14 @@ public class CountersMoveEffect extends SpellAbilityEffect { continue; } - Map params = Maps.newHashMap(); + params = Maps.newHashMap(); params.put("CounterType", cType); params.put("Source", source); params.put("Target", cur); - int cnum = player.getController().chooseNumber(sa, Localizer.getInstance().getMessage("lblPutHowManyTargetCounterOnCard", cType.getName(), CardTranslation.getTranslatedName(cur.getName())), 0, source.getCounters(cType), params); + int cnum = player.getController().chooseNumber(sa, + Localizer.getInstance().getMessage("lblPutHowManyTargetCounterOnCard", cType.getName(), + CardTranslation.getTranslatedName(cur.getName())), + 0, source.getCounters(cType), params); if (cnum > 0) { source.subtractCounter(cType, cnum); @@ -207,96 +237,133 @@ public class CountersMoveEffect extends SpellAbilityEffect { table.triggerCountersPutAll(game); } return; - } - - Card source = null; - int cntToMove = 0; - List srcCards = getDefinedCardsOrTargeted(sa, "Source"); - if (srcCards.size() > 0) { - source = srcCards.get(0); - } - - // source doesn't has any counters to move - if (!source.hasCounters()) { - return; - } - - if (!counterNum.equals("All") && !counterNum.equals("Any")) { - cntToMove = AbilityUtils.calculateAmount(host, counterNum, sa); } else { - cntToMove = source.getCounters(cType); - } - List tgtCards = getDefinedCardsOrTargeted(sa); - - for (final Card dest : tgtCards) { - if (null != source && null != dest) { - // rule 121.5: If the first and second objects are the same object, nothing happens - if (source.equals(dest)) { - continue; + Card source = null; + List tgtCards = getDefinedCardsOrTargeted(sa); + // special logic for moving from Target to Target + if (sa.usesTargeting() && sa.getTargetRestrictions().getMinTargets(host, sa) == 2) { + if (tgtCards.size() < 2) { + return; } - Card cur = game.getCardState(dest, null); - if (cur == null || !cur.equalsWithTimestamp(dest)) { - // Test to see if the card we're trying to add is in the expected state - continue; + source = tgtCards.remove(0); + } else { + List srcCards = getDefinedCardsOrTargeted(sa, "Source"); + if (srcCards.size() > 0) { + source = srcCards.get(0); } + } + if (source == null) { + return; + } - if (!"Any".matches(counterName)) { - if (!cur.canReceiveCounters(cType)) { + // source doesn't has any counters to move + if (!source.hasCounters()) { + return; + } + + for (final Card dest : tgtCards) { + if (null != source && null != dest) { + // rule 121.5: If the first and second objects are the same object, nothing + // happens + if (source.equals(dest)) { + continue; + } + Card cur = game.getCardState(dest, null); + if (cur == null || !cur.equalsWithTimestamp(dest)) { + // Test to see if the card we're trying to add is in the expected state continue; } - if (counterNum.equals("Any")) { - Map params = Maps.newHashMap(); - params.put("CounterType", cType); - params.put("Source", source); - params.put("Target", cur); - cntToMove = pc.chooseNumber(sa, Localizer.getInstance().getMessage("lblTakeHowManyTargetCounterFromCard", cType.getName(), CardTranslation.getTranslatedName(source.getName())), 0, cntToMove, params); - } - - if (source.getCounters(cType) >= cntToMove) { - source.subtractCounter(cType, cntToMove); - cur.addCounter(cType, cntToMove, player, true, table); - game.updateLastStateForCard(cur); - } - } else { - // any counterType currently only Leech Bonder - final Map tgtCounters = source.getCounters(); - - final List typeChoices = Lists.newArrayList(); - // get types of counters - for (CounterType ct : tgtCounters.keySet()) { - if (dest.canReceiveCounters(ct)) { - typeChoices.add(ct); + Map countersToAdd = Maps.newHashMap(); + if ("All".equals(counterName)) { + final Map tgtCounters = Maps.newHashMap(source.getCounters()); + for (Map.Entry e : tgtCounters.entrySet()) { + removeCounter(sa, source, cur, e.getKey(), counterNum, countersToAdd); } - } - if (typeChoices.isEmpty()) { - return; + + } else if ("Any".equals(counterName)) { + // any counterType currently only Leech Bonder + final Map tgtCounters = source.getCounters(); + + final List typeChoices = Lists.newArrayList(); + // get types of counters + for (CounterType ct : tgtCounters.keySet()) { + if (dest.canReceiveCounters(ct)) { + typeChoices.add(ct); + } + } + if (typeChoices.isEmpty()) { + return; + } + + Map params = Maps.newHashMap(); + params.put("Source", source); + params.put("Target", dest); + String title = Localizer.getInstance().getMessage("lblSelectRemoveCounterType"); + CounterType chosenType = pc.chooseCounterType(typeChoices, sa, title, params); + + removeCounter(sa, source, cur, chosenType, counterNum, countersToAdd); + } else { + if (!cur.canReceiveCounters(cType)) { + continue; + } + + removeCounter(sa, source, cur, cType, counterNum, countersToAdd); } - Map params = Maps.newHashMap(); - params.put("Source", source); - params.put("Target", dest); - String title = Localizer.getInstance().getMessage("lblSelectRemoveCounterType"); - CounterType chosenType = pc.chooseCounterType(typeChoices, sa, title, params); - - params = Maps.newHashMap(); - params.put("CounterType", chosenType); - params.put("Source", source); - params.put("Target", dest); - int chosenAmount = pc.chooseNumber(sa, Localizer.getInstance().getMessage("lblTakeHowManyTargetCounters", chosenType.getName()), - 0, Math.min(tgtCounters.get(chosenType), cntToMove), params); - - if (chosenAmount > 0) { - dest.addCounter(chosenType, chosenAmount, player, true, table); - source.subtractCounter(chosenType, chosenAmount); - game.updateLastStateForCard(dest); - cntToMove -= chosenAmount; + for (Map.Entry e : countersToAdd.entrySet()) { + cur.addCounter(e.getKey(), e.getValue(), player, true, table); } + game.updateLastStateForCard(cur); } } + // update source + game.updateLastStateForCard(source); } - // update source - game.updateLastStateForCard(source); table.triggerCountersPutAll(game); } // moveCounterResolve + + protected void removeCounter(SpellAbility sa, final Card src, final Card dest, CounterType cType, String counterNum, Map countersToAdd) { + final Card host = sa.getHostCard(); + //final String counterNum = sa.getParam("CounterNum"); + final Player player = sa.getActivatingPlayer(); + final PlayerController pc = player.getController(); + final Game game = host.getGame(); + + // rule 121.5: If the first and second objects are the same object, nothing + // happens + if (src.equals(dest)) { + return; + } + + if (!dest.canReceiveCounters(cType)) { + return; + } + + int cmax = src.getCounters(cType); + if (cmax <= 0) { + return; + } + + int cnum = 0; + if (counterNum.equals("All")) { + cnum = cmax; + } else if (counterNum.equals("Any")) { + Map params = Maps.newHashMap(); + params.put("CounterType", cType); + params.put("Source", src); + params.put("Target", dest); + cnum = pc.chooseNumber( + sa, Localizer.getInstance().getMessage("lblTakeHowManyTargetCounterFromCard", + cType.getName(), CardTranslation.getTranslatedName(src.getName())), + 0, cmax, params); + } else { + cnum = Math.min(cmax, AbilityUtils.calculateAmount(host, counterNum, sa)); + } + if (cnum > 0) { + src.subtractCounter(cType, cnum); + game.updateLastStateForCard(src); + countersToAdd.put(cType, (countersToAdd.containsKey(cType) ? countersToAdd.get(cType) : 0) + cnum); + } + } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java index b2705b69516..9cbd6c7258e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersNoteEffect.java @@ -28,9 +28,9 @@ public class CountersNoteEffect extends SpellAbilityEffect { GameEntityCounterTable table = new GameEntityCounterTable(); for (Card c : getDefinedCardsOrTargeted(sa)) { if (mode.equals(MODE_STORE)) { - noteCounters(c, source); + noteCounters(c, c); } else if (mode.equals(MODE_LOAD)) { - loadCounters(c, source, p, table); + loadCounters(c, c, p, table); } } table.triggerCountersPutAll(game); diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersProliferateEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersProliferateEffect.java index 587cc53995c..fd81398e5e9 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersProliferateEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersProliferateEffect.java @@ -43,7 +43,7 @@ public class CountersProliferateEffect extends SpellAbilityEffect { list.addAll(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.hasCounters())); List result = pc.chooseEntitiesForEffect(list, 0, list.size(), null, sa, - Localizer.getInstance().getMessage("lblChooseProliferateTarget"), p); + Localizer.getInstance().getMessage("lblChooseProliferateTarget"), p, null); GameEntityCounterTable table = new GameEntityCounterTable(); for (final GameEntity ge : result) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersPutAllEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersPutAllEffect.java index d611df3baf0..4d69ba3d4e1 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersPutAllEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersPutAllEffect.java @@ -18,7 +18,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect { protected String getStackDescription(SpellAbility sa) { final StringBuilder sb = new StringBuilder(); - final CounterType cType = CounterType.valueOf(sa.getParam("CounterType")); + final CounterType cType = CounterType.getType(sa.getParam("CounterType")); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa); final String zone = sa.hasParam("ValidZone") ? sa.getParam("ValidZone") : "Battlefield"; @@ -67,7 +67,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect { GameEntityCounterTable table = new GameEntityCounterTable(); for (final Card tgtCard : cards) { boolean inBattlefield = game.getZoneOf(tgtCard).is(ZoneType.Battlefield); - tgtCard.addCounter(CounterType.valueOf(type), counterAmount, placer, inBattlefield, table); + tgtCard.addCounter(CounterType.getType(type), counterAmount, placer, inBattlefield, table); game.updateLastStateForCard(tgtCard); } table.triggerCountersPutAll(game); 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 2f48a5a20f6..61d58891566 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 @@ -1,5 +1,6 @@ package forge.game.ability.effects; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; @@ -8,20 +9,25 @@ import forge.game.Game; import forge.game.GameEntity; import forge.game.GameEntityCounterTable; import forge.game.GameObject; +import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollection; +import forge.game.card.CardFactoryUtil; import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.card.CardUtil; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.card.CardPredicates.Presets; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerController; import forge.game.spellability.SpellAbility; +import forge.game.trigger.Trigger; +import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerType; import forge.game.zone.Zone; import forge.game.zone.ZoneType; @@ -30,6 +36,8 @@ import forge.util.Localizer; import forge.util.CardTranslation; import java.util.Map; +import java.util.Map.Entry; +import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -59,8 +67,10 @@ public class CountersPutEffect extends SpellAbilityEffect { String type = spellAbility.getParam("CounterType"); if (type.equals("ExistingCounter")) { stringBuilder.append("of an existing counter"); + } else if (type.equals("EachFromSource")) { + stringBuilder.append("each counter"); } else { - stringBuilder.append(CounterType.valueOf(type).getName()).append(" counter"); + stringBuilder.append(CounterType.getType(type).getName()).append(" counter"); } if (amount != 1) { @@ -78,7 +88,9 @@ public class CountersPutEffect extends SpellAbilityEffect { final List targetCards = SpellAbilityEffect.getTargetCards(spellAbility); for(int i = 0; i < targetCards.size(); i++) { Card targetCard = targetCards.get(i); - stringBuilder.append(targetCard).append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetCard)).append(" counter)"); + stringBuilder.append(targetCard); + if (spellAbility.getTargetRestrictions().getDividedMap().get(targetCard) != null) // fix null counter stack description + stringBuilder.append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetCard)).append(" counter)"); if(i == targetCards.size() - 2) { stringBuilder.append(" and "); @@ -104,57 +116,66 @@ public class CountersPutEffect extends SpellAbilityEffect { } } stringBuilder.append("."); - return stringBuilder.toString(); } - @Override - public void resolve(SpellAbility sa) { + protected void resolvePerType(SpellAbility sa, final Player placer, CounterType counterType, int counterAmount, GameEntityCounterTable table) { final Card card = sa.getHostCard(); final Game game = card.getGame(); final Player activator = sa.getActivatingPlayer(); final PlayerController pc = activator.getController(); - - String strTyp = sa.getParam("CounterType"); - CounterType counterType = null; - boolean existingCounter = strTyp.equals("ExistingCounter"); - boolean eachExistingCounter = sa.hasParam("EachExistingCounter"); - String amount = sa.getParamOrDefault("CounterNum", "1"); - - if (!existingCounter) { - try { - counterType = AbilityUtils.getCounterType(strTyp, sa); - } catch (Exception e) { - System.out.println("Counter type doesn't match, nor does an SVar exist with the type name."); - return; - } - } - - Player placer = activator; - if (sa.hasParam("Placer")) { - final String pstr = sa.getParam("Placer"); - placer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), pstr, sa).get(0); - } - final boolean etbcounter = sa.hasParam("ETB"); - final boolean remember = sa.hasParam("RememberCounters"); final boolean rememberCards = sa.hasParam("RememberCards"); - int counterAmount = AbilityUtils.calculateAmount(sa.getHostCard(), amount, sa); final int max = sa.hasParam("MaxFromEffect") ? Integer.parseInt(sa.getParam("MaxFromEffect")) : -1; - CardCollection tgtCards = new CardCollection(); + boolean existingCounter = sa.hasParam("CounterType") && sa.getParam("CounterType").equals("ExistingCounter"); + boolean eachExistingCounter = sa.hasParam("EachExistingCounter"); + List tgtObjects = Lists.newArrayList(); if (sa.hasParam("Bolster")) { CardCollection creatsYouCtrl = CardLists.filter(activator.getCardsIn(ZoneType.Battlefield), Presets.CREATURES); CardCollection leastToughness = new CardCollection(Aggregates.listWithMin(creatsYouCtrl, CardPredicates.Accessors.fnGetDefense)); - tgtCards.addAll(pc.chooseCardsForEffect(leastToughness, sa, Localizer.getInstance().getMessage("lblChooseACreatureWithLeastToughness"), 1, 1, false)); - tgtObjects.addAll(tgtCards); + + Map params = Maps.newHashMap(); + params.put("CounterType", counterType); + + Iterables.addAll(tgtObjects, activator.getController().chooseCardsForEffect(leastToughness, sa, Localizer.getInstance().getMessage("lblChooseACreatureWithLeastToughness"), 1, 1, false, params)); + } else if (sa.hasParam("Choices")) { + ZoneType choiceZone = ZoneType.Battlefield; + if (sa.hasParam("ChoiceZone")) { + choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone")); + } + Player chooser = activator; + if (sa.hasParam("Chooser")) { + List choosers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Chooser"), sa); + if (choosers.isEmpty()) { + return; + } + chooser = choosers.get(0); + } + + CardCollection choices = new CardCollection(game.getCardsIn(choiceZone)); + + int n = sa.hasParam("ChoiceAmount") ? Integer.parseInt(sa.getParam("ChoiceAmount")) : 1; + + choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, card, sa); + + String title = Localizer.getInstance().getMessage("lblChooseaCard") + " "; + if (sa.hasParam("ChoiceTitle")) { + title = sa.getParam("ChoiceTitle"); + // TODO might use better message + if (counterType != null) { + title += " " + counterType.getName(); + } + } + + Map params = Maps.newHashMap(); + params.put("CounterType", counterType); + Iterables.addAll(tgtObjects, chooser.getController().chooseCardsForEffect(choices, sa, title, n, n, sa.hasParam("ChoiceOptional"), params)); } else { tgtObjects.addAll(getDefinedOrTargeted(sa, "Defined")); } - GameEntityCounterTable table = new GameEntityCounterTable(); - for (final GameObject obj : tgtObjects) { // check if the object is still in game or if it was moved Card gameCard = null; @@ -207,6 +228,17 @@ public class CountersPutEffect extends SpellAbilityEffect { } } + if (sa.hasParam("EachFromSource")) { + for (Card c : AbilityUtils.getDefinedCards(card, sa.getParam("EachFromSource"), sa)) { + for (Entry cti : c.getCounters().entrySet()) { + if (gameCard != null && gameCard.canReceiveCounters(cti.getKey())) { + gameCard.addCounter(cti.getKey(), cti.getValue(), placer, true, table); + } + } + } + continue; + } + if (obj instanceof Card) { boolean counterAdded = false; counterAmount = sa.usesTargeting() && sa.hasParam("DividedAsYouChoose") ? sa.getTargetRestrictions().getDividedValue(gameCard) : counterAmount; @@ -223,7 +255,7 @@ public class CountersPutEffect extends SpellAbilityEffect { // Adapt need extra logic if (sa.hasParam("Adapt")) { - if (!(gameCard.getCounters(CounterType.P1P1) == 0 + if (!(gameCard.getCounters(CounterEnumType.P1P1) == 0 || gameCard.hasKeyword("CARDNAME adapts as though it had no +1/+1 counters"))) { continue; } @@ -254,8 +286,13 @@ public class CountersPutEffect extends SpellAbilityEffect { continue; } + Map params = Maps.newHashMap(); + params.put("CounterType", counterType); + params.put("Amount", counterAmount); + params.put("Target", gameCard); + String message = Localizer.getInstance().getMessage("lblDoYouWantPutTargetP1P1CountersOnCard", String.valueOf(counterAmount), CardTranslation.getTranslatedName(gameCard.getName())); - Player chooser = pc.chooseSingleEntityForEffect(activator.getOpponents(), sa, Localizer.getInstance().getMessage("lblChooseAnOpponent")); + Player chooser = pc.chooseSingleEntityForEffect(activator.getOpponents(), sa, Localizer.getInstance().getMessage("lblChooseAnOpponent"), params); if (chooser.getController().confirmAction(sa, PlayerActionConfirmMode.Tribute, message)) { gameCard.setTributed(true); @@ -268,13 +305,14 @@ public class CountersPutEffect extends SpellAbilityEffect { if (etbcounter) { gameCard.addEtbCounter(counterType, counterAmount, placer); } else { - if (gameCard.addCounter(counterType, counterAmount, placer, true, table) > 0) { + int addedAmount = gameCard.addCounter(counterType, counterAmount, placer, true, table); + if (addedAmount > 0) { counterAdded = true; } - } - if (remember) { - final int value = gameCard.getTotalCountersToAdd(); - gameCard.addCountersAddedBy(card, counterType, value); + + if (sa.hasParam("RemovePhase")) { + addRemovePhaseTrigger(card, sa, sa.getParam("RemovePhase"), gameCard, counterType, addedAmount); + } } if (sa.hasParam("Evolve")) { @@ -317,7 +355,85 @@ public class CountersPutEffect extends SpellAbilityEffect { pl.addCounter(counterType, counterAmount, placer, true, table); } } + } + + @Override + public void resolve(SpellAbility sa) { + final Card card = sa.getHostCard(); + final Game game = card.getGame(); + final Player activator = sa.getActivatingPlayer(); + + CounterType counterType = null; + String amount = sa.getParamOrDefault("CounterNum", "1"); + boolean rememberAmount = sa.hasParam("RememberAmount"); + + if (!sa.hasParam("EachExistingCounter") && !sa.hasParam("EachFromSource") && !sa.hasParam("SharedKeywords")) { + try { + counterType = AbilityUtils.getCounterType(sa.getParam("CounterType"), sa); + } catch (Exception e) { + System.out.println("Counter type doesn't match, nor does an SVar exist with the type name."); + return; + } + } + + Player placer = activator; + if (sa.hasParam("Placer")) { + final String pstr = sa.getParam("Placer"); + placer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), pstr, sa).get(0); + } + + int counterAmount = AbilityUtils.calculateAmount(sa.getHostCard(), amount, sa); + + GameEntityCounterTable table = new GameEntityCounterTable(); + + if (sa.hasParam("SharedKeywords")) { + List keywords = Arrays.asList(sa.getParam("SharedKeywords").split(" & ")); + List zones = ZoneType.listValueOf(sa.getParam("SharedKeywordsZone")); + String[] restrictions = sa.hasParam("SharedRestrictions") ? sa.getParam("SharedRestrictions").split(",") : new String[]{"Card"}; + keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, sa.getHostCard()); + for (String k : keywords) { + resolvePerType(sa, placer, CounterType.getType(k), counterAmount, table); + } + } else { + resolvePerType(sa, placer, counterType, counterAmount, table); + } + + int totalAdded = 0; + for (Integer i : table.values()) { + totalAdded += i; + } + + if (totalAdded > 0 && rememberAmount) { + // TODO use SpellAbility Remember later + card.addRemembered(Integer.valueOf(totalAdded)); + } + table.triggerCountersPutAll(game); } + protected void addRemovePhaseTrigger(final Card host, final SpellAbility sa, String phase, Card tgt, CounterType ct, int added) { + boolean intrinsic = sa.isIntrinsic(); + + StringBuilder delTrig = new StringBuilder("Mode$ Phase | Phase$ "); + delTrig.append(phase); + delTrig.append(" | TriggerDescription$ For each ").append(ct.getName()).append(" counter you put on a creature this way, remove a ").append(ct.getName()).append(" counter from that creature at the beginning of the next"); + if ("Cleanup".equals(phase)) { + delTrig.append("cleanup step"); + } else if ("End of Turn".equals(phase)) { + delTrig.append("next end step"); + } + + String trigSA = new StringBuilder("DB$ RemoveCounter | Defined$ DelayTriggerRemembered | CounterNum$ 1 | CounterType$ ").append(ct).toString(); + + // these trigger are one per counter + for (int i = 0; i < added; i++) { + final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), sa.getHostCard(), intrinsic); + trig.addRemembered(tgt); + + final SpellAbility newSa = AbilityFactory.getAbility(trigSA, sa.getHostCard()); + newSa.setIntrinsic(intrinsic); + trig.setOverridingAbility(newSa); + sa.getActivatingPlayer().getGame().getTriggerHandler().registerDelayedTrigger(trig); + } + } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersPutOrRemoveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersPutOrRemoveEffect.java index 61e89d134ec..14298c46617 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersPutOrRemoveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersPutOrRemoveEffect.java @@ -32,7 +32,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect { sb.append(sa.getActivatingPlayer().getName()); if (sa.hasParam("CounterType")) { - CounterType ctype = CounterType.valueOf(sa.getParam("CounterType")); + CounterType ctype = CounterType.getType(sa.getParam("CounterType")); sb.append(" removes a ").append(ctype.getName()); sb.append(" counter from or put another ").append(ctype.getName()).append(" counter on "); } else { @@ -56,7 +56,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect { CounterType ctype = null; if (sa.hasParam("CounterType")) { - ctype = CounterType.valueOf(sa.getParam("CounterType")); + ctype = CounterType.getType(sa.getParam("CounterType")); } GameEntityCounterTable table = new GameEntityCounterTable(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveAllEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveAllEffect.java index f53a1b58264..74110e12e3e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveAllEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveAllEffect.java @@ -18,7 +18,7 @@ public class CountersRemoveAllEffect extends SpellAbilityEffect { protected String getStackDescription(SpellAbility sa) { final StringBuilder sb = new StringBuilder(); - final CounterType cType = CounterType.valueOf(sa.getParam("CounterType")); + final CounterType cType = CounterType.getType(sa.getParam("CounterType")); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa); final String zone = sa.hasParam("ValidZone") ? sa.getParam("ValidZone") : "Battlefield"; String amountString = Integer.toString(amount); @@ -69,11 +69,11 @@ public class CountersRemoveAllEffect extends SpellAbilityEffect { continue; } if (sa.hasParam("AllCounters")) { - counterAmount = tgtCard.getCounters(CounterType.valueOf(type)); + counterAmount = tgtCard.getCounters(CounterType.getType(type)); } if (counterAmount > 0) { - tgtCard.subtractCounter(CounterType.valueOf(type), counterAmount); + tgtCard.subtractCounter(CounterType.getType(type), counterAmount); game.updateLastStateForCard(tgtCard); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java index 5698fb50bb0..286cf8589a7 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java @@ -31,7 +31,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect { final String num = sa.getParam("CounterNum"); int amount = 0; - if (!num.equals("All") && !num.equals("Remembered")) { + if (!num.equals("All") && !num.equals("Any")) { amount = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa); } @@ -48,7 +48,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect { sb.append(amount).append(" ").append(" counter"); } } else { - sb.append(amount).append(" ").append(CounterType.valueOf(counterName).getName()).append(" counter"); + sb.append(amount).append(" ").append(CounterType.getType(counterName).getName()).append(" counter"); } if (amount != 1) { sb.append("s"); @@ -80,7 +80,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect { final String num = sa.getParam("CounterNum"); int cntToRemove = 0; - if (!num.equals("All") && !num.equals("Any") && !num.equals("Remembered")) { + if (!num.equals("All") && !num.equals("Any")) { cntToRemove = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa); } @@ -130,7 +130,10 @@ public class CountersRemoveEffect extends SpellAbilityEffect { srcCards = game.getCardsIn(ZoneType.Battlefield); srcCards = CardLists.getValidCards(srcCards, sa.getParam("ValidSource"), player, card, sa); if (num.equals("Any")) { - srcCards = player.getController().chooseCardsForEffect(srcCards, sa, Localizer.getInstance().getMessage("lblChooseCardsToTakeTargetCounters", counterType.getName()), 0, srcCards.size(), true); + String title = Localizer.getInstance().getMessage("lblChooseCardsToTakeTargetCounters", counterType.getName()); + Map params = Maps.newHashMap(); + params.put("CounterType", counterType); + srcCards = player.getController().chooseCardsForEffect(srcCards, sa, title, 0, srcCards.size(), true, params); } } else { srcCards = getTargetCards(sa); @@ -156,8 +159,6 @@ public class CountersRemoveEffect extends SpellAbilityEffect { continue; } else if (num.equals("All") || num.equals("Any")) { cntToRemove = gameCard.getCounters(counterType); - } else if (num.equals("Remembered")) { - cntToRemove = gameCard.getCountersAddedBy(card, counterType); } if (type.equals("Any")) { @@ -169,7 +170,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect { if (sa.hasParam("UpTo") || num.equals("Any")) { Map params = Maps.newHashMap(); params.put("Target", gameCard); - params.put("CounterType", type); + params.put("CounterType", counterType); String title = Localizer.getInstance().getMessage("lblSelectRemoveCountersNumberOfTarget", type); cntToRemove = pc.chooseNumber(sa, title, 0, cntToRemove, params); } @@ -179,6 +180,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect { gameCard.subtractCounter(counterType, cntToRemove); if (rememberRemoved) { for (int i = 0; i < cntToRemove; i++) { + // TODO might need to be more specific card.addRemembered(Pair.of(counterType, i)); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java index 9a80817d6e3..6e848865bc6 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DamageDealEffect.java @@ -1,8 +1,10 @@ package forge.game.ability.effects; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import forge.game.Game; +import forge.game.GameEntity; import forge.game.GameEntityCounterTable; import forge.game.GameObject; import forge.game.ability.AbilityUtils; @@ -11,6 +13,7 @@ import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardDamageMap; import forge.game.card.CardUtil; +import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.util.Lang; @@ -30,7 +33,12 @@ public class DamageDealEffect extends DamageBaseEffect { // when damageStackDescription is called, just build exactly what is happening final StringBuilder stringBuilder = new StringBuilder(); final String damage = spellAbility.getParam("NumDmg"); - final int dmg = AbilityUtils.calculateAmount(spellAbility.getHostCard(), damage, spellAbility); + int dmg; + try { // try-catch to fix Volcano Hellion Crash + dmg = AbilityUtils.calculateAmount(spellAbility.getHostCard(), damage, spellAbility); + } catch (NullPointerException e) { + dmg = 0; + } List targets = SpellAbilityEffect.getTargets(spellAbility); if (targets.isEmpty()) { @@ -53,7 +61,8 @@ public class DamageDealEffect extends DamageBaseEffect { stringBuilder.append("divided evenly (rounded down) to\n"); } else if (spellAbility.hasParam("DividedAsYouChoose")) { stringBuilder.append("divided to\n"); - } + } else + stringBuilder.append("to "); final List targetCards = SpellAbilityEffect.getTargetCards(spellAbility); final List players = SpellAbilityEffect.getTargetPlayers(spellAbility); @@ -63,7 +72,9 @@ public class DamageDealEffect extends DamageBaseEffect { // target cards for (int i = 0; i < targetCards.size(); i++) { Card targetCard = targetCards.get(i); - stringBuilder.append(targetCard).append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetCard)).append(" damage)"); + stringBuilder.append(targetCard); + if (spellAbility.getTargetRestrictions().getDividedMap().get(targetCard) != null) //fix null damage stack description + stringBuilder.append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetCard)).append(" damage)"); if (i == targetCount - 2) { stringBuilder.append(" and "); @@ -74,9 +85,10 @@ public class DamageDealEffect extends DamageBaseEffect { // target players for (int i = 0; i < players.size(); i++) { - Player targetPlayer = players.get(i); - stringBuilder.append(targetPlayer).append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetPlayer)).append(" damage)"); + stringBuilder.append(targetPlayer); + if (spellAbility.getTargetRestrictions().getDividedMap().get(targetPlayer) != null) //fix null damage stack description + stringBuilder.append(" (").append(spellAbility.getTargetRestrictions().getDividedMap().get(targetPlayer)).append(" damage)"); if (i == players.size() - 2) { stringBuilder.append(" and "); @@ -114,6 +126,7 @@ public class DamageDealEffect extends DamageBaseEffect { @Override public void resolve(SpellAbility sa) { final Card hostCard = sa.getHostCard(); + final Player activationPlayer = sa.getActivatingPlayer(); final Game game = hostCard.getGame(); final String damage = sa.getParam("NumDmg"); @@ -224,7 +237,12 @@ public class DamageDealEffect extends DamageBaseEffect { } for (final Object o : tgts) { - dmg = (sa.usesTargeting() && sa.hasParam("DividedAsYouChoose")) ? sa.getTargetRestrictions().getDividedValue(o) : dmg; + if (!removeDamage) { + dmg = (sa.usesTargeting() && sa.hasParam("DividedAsYouChoose")) ? sa.getTargetRestrictions().getDividedValue(o) : dmg; + if (dmg <= 0) { + continue; + } + } if (o instanceof Card) { final Card c = (Card) o; final Card gc = game.getCardState(c, null); @@ -238,7 +256,27 @@ public class DamageDealEffect extends DamageBaseEffect { c.setHasBeenDealtDeathtouchDamage(false); c.clearAssignedDamage(); } else { - c.addDamage(dmg, sourceLKI, false, noPrevention, damageMap, preventMap, counterTable, sa); + if (sa.hasParam("ExcessDamage") && (!sa.hasParam("ExcessDamageCondition") || + sourceLKI.isValid(sa.getParam("ExcessDamageCondition").split(","), activationPlayer, hostCard, sa))) { + // excess damage explicit says toughness, not lethal damage in the rules + int lethal = c.getLethalDamage(); + if (sourceLKI.hasKeyword(Keyword.DEATHTOUCH)) { + lethal = Math.min(lethal, 1); + } + int dmgToTarget = Math.min(lethal, dmg); + + c.addDamage(dmgToTarget, sourceLKI, false, noPrevention, damageMap, preventMap, counterTable, sa); + + List list = Lists.newArrayList(); + list.addAll(AbilityUtils.getDefinedCards(hostCard, sa.getParam("ExcessDamage"), sa)); + list.addAll(AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("ExcessDamage"), sa)); + + if (!list.isEmpty()) { + list.get(0).addDamage(dmg - dmgToTarget, sourceLKI, false, noPrevention, damageMap, preventMap, counterTable, sa); + } + } else { + c.addDamage(dmg, sourceLKI, false, noPrevention, damageMap, preventMap, counterTable, sa); + } } } } else if (o instanceof Player) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/DebuffEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DebuffEffect.java index 442fe3c5340..2595c792dec 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DebuffEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DebuffEffect.java @@ -70,7 +70,7 @@ public class DebuffEffect extends SpellAbilityEffect { for (final Card tgtC : getTargetCards(sa)) { final List addedKW = Lists.newArrayList(); final List removedKW = Lists.newArrayList(); - if (tgtC.isInPlay() && tgtC.canBeTargetedBy(sa)) { + if (tgtC.isInPlay() && (!sa.usesTargeting() || tgtC.canBeTargetedBy(sa))) { if (sa.hasParam("AllSuffixKeywords")) { String suffix = sa.getParam("AllSuffixKeywords"); for (final KeywordInterface kw : tgtC.getKeywords()) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/DelayedTriggerEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DelayedTriggerEffect.java index 33cd4a6aaf7..0a95d27b9e2 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DelayedTriggerEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DelayedTriggerEffect.java @@ -46,10 +46,6 @@ public class DelayedTriggerEffect extends SpellAbilityEffect { final Trigger delTrig = TriggerHandler.parseTrigger(mapParams, sa.getHostCard(), true); - if (sa.hasParam("CopyTriggeringObjects")) { - delTrig.setStoredTriggeredObjects(sa.getTriggeringObjects()); - } - if (triggerRemembered != null) { for (final String rem : triggerRemembered.split(",")) { for (final Object o : AbilityUtils.getDefinedObjects(sa.getHostCard(), rem, sa)) { @@ -78,6 +74,11 @@ public class DelayedTriggerEffect extends SpellAbilityEffect { if (ApiType.SetState == overridingSA.getApi()) { overridingSA.setSVar("StoredTransform", String.valueOf(sa.getHostCard().getTransformedTimestamp())); } + + if (sa.hasParam("CopyTriggeringObjects")) { + overridingSA.setTriggeringObjects(sa.getTriggeringObjects()); + } + delTrig.setOverridingAbility(overridingSA); } final TriggerHandler trigHandler = sa.getActivatingPlayer().getGame().getTriggerHandler(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/DigEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DigEffect.java index 457edcaff89..343bb983c8d 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DigEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DigEffect.java @@ -3,6 +3,7 @@ package forge.game.ability.effects; import forge.card.MagicColor; import forge.game.Game; import forge.game.GameActionUtil; +import forge.game.GameEntity; import forge.game.GameEntityCounterTable; import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; @@ -13,6 +14,8 @@ import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.card.CardZoneTable; import forge.game.card.CounterType; +import forge.game.combat.Combat; +import forge.game.event.GameEventCombatChanged; import forge.game.player.DelayedReveal; import forge.game.player.Player; import forge.game.player.PlayerView; @@ -163,7 +166,7 @@ public class DigEffect extends SpellAbilityEffect { if (sa.hasParam("Choser")) { final FCollectionView choosers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Choser"), sa); if (!choosers.isEmpty()) { - chooser = player.getController().chooseSingleEntityForEffect(choosers, sa, Localizer.getInstance().getMessage("lblChooser") + ":"); + chooser = player.getController().chooseSingleEntityForEffect(choosers, null, sa, Localizer.getInstance().getMessage("lblChooser") + ":", false, p, null); } if (sa.hasParam("SetChosenPlayer")) { host.setChosenPlayer(chooser); @@ -218,7 +221,7 @@ public class DigEffect extends SpellAbilityEffect { } for (final byte pair : MagicColor.COLORPAIR) { Card chosen = chooser.getController().chooseSingleEntityForEffect(CardLists.filter(valid, CardPredicates.isExactlyColor(pair)), - delayedReveal, sa, Localizer.getInstance().getMessage("lblChooseOne"), false, p); + delayedReveal, sa, Localizer.getInstance().getMessage("lblChooseOne"), false, p, null); if (chosen != null) { movedCards.add(chosen); } @@ -238,7 +241,7 @@ public class DigEffect extends SpellAbilityEffect { prompt = Localizer.getInstance().getMessage("lblChooseACardLeaveTarget", p.getName(), destZone2.getTranslatedName()); } - Card chosen = chooser.getController().chooseSingleEntityForEffect(valid, delayedReveal, sa, prompt, false, p); + Card chosen = chooser.getController().chooseSingleEntityForEffect(valid, delayedReveal, sa, prompt, false, p, null); movedCards.remove(chosen); if (sa.hasParam("RandomOrder")) { CardLists.shuffle(movedCards); @@ -271,7 +274,7 @@ public class DigEffect extends SpellAbilityEffect { int max = anyNumber ? valid.size() : Math.min(valid.size(),destZone1ChangeNum); int min = (anyNumber || optional) ? 0 : max; if ( max > 0 ) { // if max is 0 don't make a choice - chosen = chooser.getController().chooseEntitiesForEffect(valid, min, max, delayedReveal, sa, prompt, p); + chosen = chooser.getController().chooseEntitiesForEffect(valid, min, max, delayedReveal, sa, prompt, p, null); } chooser.getController().endTempShowCards(); @@ -307,6 +310,21 @@ public class DigEffect extends SpellAbilityEffect { if (sa.hasParam("Tapped")) { c.setTapped(true); } + if (sa.hasParam("Attacking")) { + final Combat combat = game.getCombat(); + if ( null != combat ) { + final FCollectionView e = combat.getDefenders(); + + final GameEntity defender = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(e, sa, + Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())), null); + + if (defender != null) { + combat.addAttacker(c, defender); + game.getCombat().getBandOfAttacker(c).setBlocked(false); + game.fireEvent(new GameEventCombatChanged()); + } + } + } } else if (destZone1.equals(ZoneType.Exile)) { c.setExiledWith(effectHost); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/DigMultipleEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DigMultipleEffect.java index dfeccc06ade..9fc679605fd 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DigMultipleEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DigMultipleEffect.java @@ -10,6 +10,7 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollection; +import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardZoneTable; import forge.game.player.Player; @@ -22,7 +23,6 @@ public class DigMultipleEffect extends SpellAbilityEffect { @Override public void resolve(SpellAbility sa) { - // TODO Auto-generated method stub final Card host = sa.getHostCard(); final Player player = sa.getActivatingPlayer(); final Game game = player.getGame(); @@ -37,6 +37,7 @@ public class DigMultipleEffect extends SpellAbilityEffect { final int libraryPosition2 = sa.hasParam("LibraryPosition2") ? Integer.parseInt(sa.getParam("LibraryPosition2")) : -1; String changeValid = sa.hasParam("ChangeValid") ? sa.getParam("ChangeValid") : ""; + boolean chooseOptional = sa.hasParam("Optional"); CardZoneTable table = new CardZoneTable(); for (final Player p : getTargetPlayers(sa)) { @@ -79,12 +80,33 @@ public class DigMultipleEffect extends SpellAbilityEffect { continue; } - CardCollection chosen = chooser.getController().chooseCardsForEffectMultiple(validMap, sa, Localizer.getInstance().getMessage("lblChooseCards")); + CardCollection chosen = chooser.getController().chooseCardsForEffectMultiple(validMap, sa, Localizer.getInstance().getMessage("lblChooseCards"), chooseOptional); if (!chosen.isEmpty()) { game.getAction().reveal(chosen, chooser, true, Localizer.getInstance().getMessage("lblPlayerPickedCardFrom", chooser.getName())); } + if (sa.hasParam("ChooseAmount") || sa.hasParam("ChosenZone")) { + int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ChooseAmount", "1"), sa); + final ZoneType chosenZone = sa.hasParam("ChosenZone") ? ZoneType.smartValueOf(sa.getParam("ChosenZone")) : ZoneType.Battlefield; + + CardCollectionView extraChosen = chooser.getController().chooseCardsForEffect(chosen, sa, Localizer.getInstance().getMessage("lblChooseCards"), amount, amount, false, null); + if (!extraChosen.isEmpty()) { + game.getAction().reveal(extraChosen, chooser, true, Localizer.getInstance().getMessage("lblPlayerPickedCardFrom", chooser.getName())); + } + + for (Card c : extraChosen) { + final ZoneType origin = c.getZone().getZoneType(); + final PlayerZone zone = c.getOwner().getZone(chosenZone); + chosen.remove(c); + rest.remove(c); + c = game.getAction().moveTo(zone, c, sa); + if (!origin.equals(c.getZone().getZoneType())) { + table.put(origin, c.getZone().getZoneType(), c); + } + } + } + for (Card c : chosen) { final ZoneType origin = c.getZone().getZoneType(); final PlayerZone zone = c.getOwner().getZone(destZone1); diff --git a/forge-game/src/main/java/forge/game/ability/effects/DigUntilEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DigUntilEffect.java index 58b9a835902..eff16192f72 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DigUntilEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DigUntilEffect.java @@ -1,20 +1,27 @@ package forge.game.ability.effects; import forge.game.Game; +import forge.game.GameEntity; import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardZoneTable; +import forge.game.combat.Combat; +import forge.game.event.GameEventCombatChanged; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.PlayerZone; import forge.game.zone.ZoneType; +import forge.util.CardTranslation; import forge.util.MyRandom; import forge.util.Localizer; +import forge.util.collect.FCollectionView; import java.util.*; +import com.google.common.collect.Maps; + public class DigUntilEffect extends SpellAbilityEffect { /* (non-Javadoc) @@ -148,7 +155,7 @@ public class DigUntilEffect extends SpellAbilityEffect { if (revealed.size() > 0) { game.getAction().reveal(revealed, p, false); } - + if (foundDest != null) { // Allow ordering of found cards @@ -168,6 +175,27 @@ public class DigUntilEffect extends SpellAbilityEffect { if (sa.hasParam("GainControl") && foundDest.equals(ZoneType.Battlefield)) { c.setController(sa.getActivatingPlayer(), game.getNextTimestamp()); m = game.getAction().moveTo(c.getController().getZone(foundDest), c, sa); + if (sa.hasParam("Tapped")) { + c.setTapped(true); + } + if (sa.hasParam("Attacking")) { + final Combat combat = game.getCombat(); + if (null != combat) { + final FCollectionView e = combat.getDefenders(); + + Map params = Maps.newHashMap(); + params.put("Attacker", c); + + final GameEntity defender = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(e, sa, + Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())), params); + + if (defender != null) { + combat.addAttacker(c, defender); + combat.getBandOfAttacker(c).setBlocked(false); + game.fireEvent(new GameEventCombatChanged()); + } + } + } } else if (sa.hasParam("NoMoveFound") && foundDest.equals(ZoneType.Library)) { //Don't do anything } else { diff --git a/forge-game/src/main/java/forge/game/ability/effects/DiscardEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DiscardEffect.java index cff66313247..dcc351e65fc 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DiscardEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DiscardEffect.java @@ -2,6 +2,7 @@ package forge.game.ability.effects; import forge.game.Game; import forge.game.GameActionUtil; +import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.*; @@ -10,6 +11,7 @@ import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerPredicates; import forge.game.spellability.SpellAbility; +import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.Lang; @@ -22,8 +24,8 @@ import org.apache.commons.lang3.StringUtils; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; -import java.util.ArrayList; import java.util.List; +import java.util.Map; public class DiscardEffect extends SpellAbilityEffect { @@ -109,7 +111,7 @@ public class DiscardEffect extends SpellAbilityEffect { final Game game = source.getGame(); //final boolean anyNumber = sa.hasParam("AnyNumber"); - final List discarded = new ArrayList<>(); + final List discarded = Lists.newArrayList(); final List targets = getTargetPlayers(sa), discarders; Player firstTarget = null; @@ -127,6 +129,8 @@ public class DiscardEffect extends SpellAbilityEffect { final CardZoneTable table = new CardZoneTable(); for (final Player p : discarders) { + boolean firstDiscard = p.getNumDiscardedThisTurn() == 0; + final CardCollection discardedByPlayer = new CardCollection(); if ((mode.equals("RevealTgtChoose") && firstTarget != null) || !sa.usesTargeting() || p.canBeTargetedBy(sa)) { if (sa.hasParam("RememberDiscarder") && p.canDiscardBy(sa)) { source.addRemembered(p); @@ -149,35 +153,28 @@ public class DiscardEffect extends SpellAbilityEffect { for (final Card c : toDiscard) { if (p.discard(c, sa, table) != null) { discarded.add(c); - } - } - - if (sa.hasParam("RememberDiscarded")) { - for (final Card c : discarded) { - source.addRemembered(c); + discardedByPlayer.add(c); } } } - continue; } if (mode.equals("Hand")) { if (!p.canDiscardBy(sa)) { continue; } - boolean shouldRemember = sa.hasParam("RememberDiscarded"); - CardCollectionView toDiscard = new CardCollection(Lists.newArrayList(p.getCardsIn(ZoneType.Hand))); + CardCollectionView toDiscard = p.getCardsIn(ZoneType.Hand); if (toDiscard.size() > 1) { toDiscard = GameActionUtil.orderCardsByTheirOwners(game, toDiscard, ZoneType.Graveyard); } - for(Card c : toDiscard) { // without copying will get concurrent modification exception - boolean hasDiscarded = p.discard(c, sa, table) != null; - if( hasDiscarded && shouldRemember ) - source.addRemembered(c); + for(Card c : Lists.newArrayList(toDiscard)) { // without copying will get concurrent modification exception + if (p.discard(c, sa, table) != null) { + discarded.add(c); + discardedByPlayer.add(c); + } } - continue; } if (mode.equals("NotRemembered")) { @@ -192,6 +189,7 @@ public class DiscardEffect extends SpellAbilityEffect { for (final Card c : dPHand) { if (p.discard(c, sa, table) != null) { discarded.add(c); + discardedByPlayer.add(c); } } } @@ -231,6 +229,7 @@ public class DiscardEffect extends SpellAbilityEffect { for (Card c : toDiscardView) { if (p.discard(c, sa, table) != null) { discarded.add(c); + discardedByPlayer.add(c); } } } @@ -249,7 +248,10 @@ public class DiscardEffect extends SpellAbilityEffect { } for (Card c : toDiscard) { - c.getController().discard(c, sa, table); + if (c.getController().discard(c, sa, table) != null) { + discarded.add(c); + discardedByPlayer.add(c); + } } } } @@ -282,6 +284,7 @@ public class DiscardEffect extends SpellAbilityEffect { for (final Card c : dPChHand) { if (p.discard(c, sa, table) != null) { discarded.add(c); + discardedByPlayer.add(c); } } } else if (mode.equals("RevealYouChoose") || mode.equals("RevealTgtChoose") || mode.equals("TgtChoose")) { @@ -332,11 +335,21 @@ public class DiscardEffect extends SpellAbilityEffect { if (card == null) { continue; } if (p.discard(card, sa, table) != null) { discarded.add(card); + discardedByPlayer.add(card); } } } } } + + if (!discardedByPlayer.isEmpty()) { + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Player, p); + runParams.put(AbilityKey.Cards, discardedByPlayer); + runParams.put(AbilityKey.Cause, sa); + runParams.put(AbilityKey.FirstTime, firstDiscard); + game.getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false); + } } if (sa.hasParam("RememberDiscarded")) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/DrawEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DrawEffect.java index c1912191dad..e68dbd5ca75 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DrawEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DrawEffect.java @@ -58,7 +58,7 @@ public class DrawEffect extends SpellAbilityEffect { actualNum = p.getController().chooseNumber(sa, "lblHowMayCardDoYouWantDraw", 0, numCards); } - final CardCollectionView drawn = p.drawCards(actualNum); + final CardCollectionView drawn = p.drawCards(actualNum, sa); if (sa.hasParam("Reveal")) { p.getGame().getAction().reveal(drawn, p); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java index 7e59dde1f78..326d56bf27d 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java @@ -86,7 +86,7 @@ public class EffectEffect extends SpellAbilityEffect { } if (sa.hasParam("ForgetCounter")) { - CounterType cType = CounterType.valueOf(sa.getParam("ForgetCounter")); + CounterType cType = CounterType.getType(sa.getParam("ForgetCounter")); rememberList = new FCollection(CardLists.filter(Iterables.filter(rememberList, Card.class), CardPredicates.hasCounter(cType))); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/EncodeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/EncodeEffect.java index b2f88def518..fce3d683d35 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/EncodeEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/EncodeEffect.java @@ -54,7 +54,7 @@ public class EncodeEffect extends SpellAbilityEffect { Card movedCard = game.getAction().moveTo(ZoneType.Exile, host, sa); // choose a creature - Card choice = player.getController().chooseSingleEntityForEffect(choices, sa, Localizer.getInstance().getMessage("lblChooseACreatureYouControlToEncode") + " ", true); + Card choice = player.getController().chooseSingleEntityForEffect(choices, sa, Localizer.getInstance().getMessage("lblChooseACreatureYouControlToEncode") + " ", true, null); if (choice == null) { return; diff --git a/forge-game/src/main/java/forge/game/ability/effects/ExploreEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ExploreEffect.java index c31e4db297a..212e6ad83fc 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ExploreEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ExploreEffect.java @@ -7,7 +7,7 @@ import forge.game.ability.AbilityKey; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollection; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.player.Player; import forge.game.player.PlayerController; import forge.game.spellability.SpellAbility; @@ -78,7 +78,7 @@ public class ExploreEffect extends SpellAbilityEffect { // if the card is not more in the game anymore // this might still return true but its no problem if (game.getZoneOf(gamec).is(ZoneType.Battlefield) && gamec.equalsWithTimestamp(c)) { - c.addCounter(CounterType.P1P1, 1, pl, true, table); + c.addCounter(CounterEnumType.P1P1, 1, pl, true, table); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ImmediateTriggerEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ImmediateTriggerEffect.java index 7b0e66c1c42..4868397540c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ImmediateTriggerEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ImmediateTriggerEffect.java @@ -3,6 +3,7 @@ package forge.game.ability.effects; import com.google.common.collect.Maps; import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; +import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerHandler; @@ -47,10 +48,6 @@ public class ImmediateTriggerEffect extends SpellAbilityEffect { final Trigger immediateTrig = TriggerHandler.parseTrigger(mapParams, sa.getHostCard(), sa.isIntrinsic()); - if (sa.hasParam("CopyTriggeringObjects")) { - immediateTrig.setStoredTriggeredObjects(sa.getTriggeringObjects()); - } - // Need to copy paid costs if (triggerRemembered != null) { @@ -71,8 +68,15 @@ public class ImmediateTriggerEffect extends SpellAbilityEffect { } if (mapParams.containsKey("Execute") || sa.hasAdditionalAbility("Execute")) { - SpellAbility overridingSA = sa.getAdditionalAbility("Execute"); + AbilitySub overridingSA = sa.getAdditionalAbility("Execute"); overridingSA.setActivatingPlayer(sa.getActivatingPlayer()); + // need to set Parent to null, otherwise it might have wrong root ability + overridingSA.setParent(null); + + if (sa.hasParam("CopyTriggeringObjects")) { + overridingSA.setTriggeringObjects(sa.getTriggeringObjects()); + } + immediateTrig.setOverridingAbility(overridingSA); } final TriggerHandler trigHandler = sa.getActivatingPlayer().getGame().getTriggerHandler(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/InvestigateEffect.java b/forge-game/src/main/java/forge/game/ability/effects/InvestigateEffect.java new file mode 100644 index 00000000000..c8688c4d114 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/InvestigateEffect.java @@ -0,0 +1,61 @@ +package forge.game.ability.effects; + +import org.apache.commons.lang3.mutable.MutableBoolean; + +import forge.game.Game; +import forge.game.ability.AbilityUtils; +import forge.game.card.Card; +import forge.game.card.CardZoneTable; +import forge.game.card.token.TokenInfo; +import forge.game.event.GameEventCombatChanged; +import forge.game.event.GameEventTokenCreated; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.util.Lang; + +public class InvestigateEffect extends TokenEffectBase { + + @Override + protected String getStackDescription(SpellAbility sa) { + final Card card = sa.getHostCard(); + final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("Num", "1"), sa); + + StringBuilder sb = new StringBuilder("Investigate"); + if (amount > 1) { + sb.append(" ").append(Lang.getNumeral(amount)).append(" times"); + } + sb.append(". (Create a colorless Clue artifact token with \"{2}, Sacrifice this artifact: Draw a card.\")"); + + return sb.toString(); + } + + @Override + public void resolve(SpellAbility sa) { + final Card card = sa.getHostCard(); + final Game game = card.getGame(); + + final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("Num", "1"), sa); + + final String tokenScript = "c_a_clue_draw"; + final Card prototype = TokenInfo.getProtoType(tokenScript, sa, false); + + for (final Player p : getTargetPlayers(sa)) { + for (int i = 0; i < amount; i++) { + CardZoneTable triggerList = new CardZoneTable(); + MutableBoolean combatChanged = new MutableBoolean(false); + makeTokens(prototype, p, sa, 1, true, false, triggerList, combatChanged); + + triggerList.triggerChangesZoneAll(game); + p.addInvestigatedThisTurn(); + + game.fireEvent(new GameEventTokenCreated()); + + if (combatChanged.isTrue()) { + game.updateCombatForView(); + game.fireEvent(new GameEventCombatChanged()); + } + } + } + } + +} diff --git a/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java index b55bcc90385..8558f46272b 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java @@ -195,6 +195,26 @@ public class ManaEffect extends SpellAbilityEffect { } if (colors == 0) return; abMana.setExpressChoice(ColorSet.fromMask(colors)); + } else if (type.startsWith("EachColoredManaSymbol")) { + final String res = type.split("_")[1]; + final CardCollection list = AbilityUtils.getDefinedCards(card, res, sa); + StringBuilder sb = new StringBuilder(); + for (Card c : list) { + String mana = c.getManaCost().toString(); + for (int i = 0; i < mana.length(); i++) { + char symbol = mana.charAt(i); + switch (symbol) { + case 'W': + case 'U': + case 'B': + case 'R': + case 'G': + sb.append(symbol).append(' '); + break; + } + } + } + abMana.setExpressChoice(sb.toString().trim()); } if (abMana.getExpressChoice().isEmpty()) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ManaReflectedEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ManaReflectedEffect.java index 3d19c777592..c5867722a2c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ManaReflectedEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ManaReflectedEffect.java @@ -13,6 +13,8 @@ import forge.util.Localizer; import java.util.Collection; import java.util.List; +import org.apache.commons.lang3.StringUtils; + public class ManaReflectedEffect extends SpellAbilityEffect { /* (non-Javadoc) @@ -98,12 +100,12 @@ public class ManaReflectedEffect extends SpellAbilityEffect { if (amount == 0) { sb.append("0"); } else { - try { + if (StringUtils.isNumeric(baseMana)) { // if baseMana is an integer(colorless), just multiply amount // and baseMana final int base = Integer.parseInt(baseMana); sb.append(base * amount); - } catch (final NumberFormatException e) { + } else { for (int i = 0; i < amount; i++) { if (i != 0) { sb.append(" "); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ManifestEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ManifestEffect.java index fed406415c2..e4c0b312ee0 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ManifestEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ManifestEffect.java @@ -40,7 +40,8 @@ public class ManifestEffect extends SpellAbilityEffect { } String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChooseCardToManifest") + " "; - tgtCards = new CardCollection(activator.getController().chooseEntitiesForEffect(choices, amount, amount, null, sa, title, p)); + + tgtCards = new CardCollection(activator.getController().chooseCardsForEffect(choices, sa, title, amount, amount, false, null)); } else if ("TopOfLibrary".equals(defined)) { tgtCards = p.getTopXCardsFromLibrary(amount); } else { diff --git a/forge-game/src/main/java/forge/game/ability/effects/MeldEffect.java b/forge-game/src/main/java/forge/game/ability/effects/MeldEffect.java index b89a17b08e0..50443ae04b1 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/MeldEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/MeldEffect.java @@ -36,7 +36,7 @@ public class MeldEffect extends SpellAbilityEffect { return; } - Card secondary = controller.getController().chooseSingleEntityForEffect(field, sa, Localizer.getInstance().getMessage("lblChooseCardToMeld")); + Card secondary = controller.getController().chooseSingleEntityForEffect(field, sa, Localizer.getInstance().getMessage("lblChooseCardToMeld"), null); secondary = game.getAction().exile(secondary, sa); diff --git a/forge-game/src/main/java/forge/game/ability/effects/MultiplePilesEffect.java b/forge-game/src/main/java/forge/game/ability/effects/MultiplePilesEffect.java index ec3de6b43eb..57b74d34c9b 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/MultiplePilesEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/MultiplePilesEffect.java @@ -86,7 +86,7 @@ public class MultiplePilesEffect extends SpellAbilityEffect { for (int i = 1; i < piles; i++) { int size = pool.size(); - CardCollectionView pile = p.getController().chooseCardsForEffect(pool, sa, Localizer.getInstance().getMessage("lblChooseCardsInTargetPile", String.valueOf(i)), 0, size, false); + CardCollectionView pile = p.getController().chooseCardsForEffect(pool, sa, Localizer.getInstance().getMessage("lblChooseCardsInTargetPile", String.valueOf(i)), 0, size, false, null); pileList.add(pile); pool.removeAll(pile); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/MustBlockEffect.java b/forge-game/src/main/java/forge/game/ability/effects/MustBlockEffect.java index f83f720d8ad..1a950048965 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/MustBlockEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/MustBlockEffect.java @@ -12,8 +12,10 @@ import forge.game.zone.ZoneType; import forge.util.Localizer; import java.util.List; +import java.util.Map; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; public class MustBlockEffect extends SpellAbilityEffect { @@ -23,6 +25,13 @@ public class MustBlockEffect extends SpellAbilityEffect { final Player activator = sa.getActivatingPlayer(); final Game game = activator.getGame(); + List cards; + if (sa.hasParam("DefinedAttacker")) { + cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DefinedAttacker"), sa); + } else { + cards = Lists.newArrayList(host); + } + List tgtCards = Lists.newArrayList(); if (sa.hasParam("Choices")) { Player chooser = activator; @@ -35,8 +44,9 @@ public class MustBlockEffect extends SpellAbilityEffect { choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host); if (!choices.isEmpty()) { String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChooseaCard") +" "; - - Card choosen = chooser.getController().chooseSingleEntityForEffect(choices, sa, title, false); + Map params = Maps.newHashMap(); + params.put("Attackers", cards); + Card choosen = chooser.getController().chooseSingleEntityForEffect(choices, sa, title, false, params); if (choosen != null) { tgtCards.add(choosen); @@ -48,13 +58,6 @@ public class MustBlockEffect extends SpellAbilityEffect { final boolean mustBlockAll = sa.hasParam("BlockAllDefined"); - List cards; - if (sa.hasParam("DefinedAttacker")) { - cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DefinedAttacker"), sa); - } else { - cards = Lists.newArrayList(host); - } - for (final Card c : tgtCards) { if ((!sa.usesTargeting()) || c.canBeTargetedBy(sa)) { if (mustBlockAll) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java index c5b07d427b6..17a633afa6a 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java @@ -118,7 +118,7 @@ public class PlayEffect extends SpellAbilityEffect { final int choicenum = AbilityUtils.calculateAmount(source, sa.getParam("ChoiceNum"), sa); tgtCards = new CardCollection( activator.getController().chooseCardsForEffect(choice, sa, - source + " - " + Localizer.getInstance().getMessage("lblChooseUpTo") + " " + Lang.nounWithNumeral(choicenum, "card"), 0, choicenum, true + source + " - " + Localizer.getInstance().getMessage("lblChooseUpTo") + " " + Lang.nounWithNumeral(choicenum, "card"), 0, choicenum, true, null ) ); } @@ -145,7 +145,7 @@ public class PlayEffect extends SpellAbilityEffect { final CardCollection saidNoTo = new CardCollection(); while (tgtCards.size() > saidNoTo.size() && saidNoTo.size() < amount && amount > 0) { activator.getController().tempShowCards(showCards); - Card tgtCard = controller.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblSelectCardToPlay")); + Card tgtCard = controller.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblSelectCardToPlay"), null); activator.getController().endTempShowCards(); if (tgtCard == null) { return; diff --git a/forge-game/src/main/java/forge/game/ability/effects/PoisonEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PoisonEffect.java index 21d6f14715a..a5baf906e78 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PoisonEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PoisonEffect.java @@ -5,7 +5,7 @@ import forge.game.GameEntityCounterTable; import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.util.Lang; @@ -59,7 +59,7 @@ import java.util.List; sb.append("s"); } - String type = CounterType.POISON.getName() + " counter"; + String type = CounterEnumType.POISON.getName() + " counter"; sb.append(" ").append(Lang.nounWithAmount(amount, type)).append("."); diff --git a/forge-game/src/main/java/forge/game/ability/effects/PumpAllEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PumpAllEffect.java index 790eed68849..9bda62c3a75 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PumpAllEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PumpAllEffect.java @@ -96,6 +96,12 @@ public class PumpAllEffect extends SpellAbilityEffect { } else if (sa.hasParam("UntilLoseControl")) { tgtC.addLeavesPlayCommand(untilEOT); tgtC.addChangeControllerCommand(untilEOT); + } else if (sa.hasParam("UntilTheEndOfYourNextTurn")) { + if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) { + game.getEndOfTurn().registerUntilEnd(sa.getActivatingPlayer(), untilEOT); + } else { + game.getEndOfTurn().addUntilEnd(sa.getActivatingPlayer(), untilEOT); + } } else { game.getEndOfTurn().addUntil(untilEOT); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/PumpEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PumpEffect.java index 8cd637577ac..abc11856243 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PumpEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PumpEffect.java @@ -11,6 +11,7 @@ import forge.game.card.CardUtil; import forge.game.event.GameEventCardStatsChanged; import forge.game.keyword.KeywordInterface; import forge.game.player.Player; +import forge.game.player.PlayerCollection; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.Aggregates; @@ -290,6 +291,10 @@ public class PumpEffect extends SpellAbilityEffect { replaced = "CardUID_" + host.getId(); } else if (defined.equals("ActivatorName")) { replaced = sa.getActivatingPlayer().getName(); + } else if (defined.endsWith("Player")) { + PlayerCollection players = AbilityUtils.getDefinedPlayers(host, defined, sa); + if (players.isEmpty()) return; + replaced = "PlayerUID_" + players.get(0).getId(); } for (int i = 0; i < keywords.size(); i++) { keywords.set(i, TextUtil.fastReplace(keywords.get(i), defined, replaced)); diff --git a/forge-game/src/main/java/forge/game/ability/effects/RepeatEachEffect.java b/forge-game/src/main/java/forge/game/ability/effects/RepeatEachEffect.java index 54e32d8e6d6..6efb4c3776c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/RepeatEachEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/RepeatEachEffect.java @@ -1,8 +1,6 @@ package forge.game.ability.effects; import com.google.common.collect.Lists; -import com.google.common.collect.Maps; - import forge.GameCommand; import forge.game.Game; import forge.game.ability.AbilityUtils; @@ -13,13 +11,9 @@ import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityStackInstance; import forge.game.zone.ZoneType; -import forge.util.Aggregates; import forge.util.collect.FCollection; -import forge.util.Localizer; - +import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; public class RepeatEachEffect extends SpellAbilityEffect { @@ -46,7 +40,6 @@ public class RepeatEachEffect extends SpellAbilityEffect { boolean useImprinted = sa.hasParam("UseImprinted"); boolean loopOverCards = false; - boolean recordChoice = sa.hasParam("RecordChoice"); CardCollectionView repeatCards = null; List repeatSas = null; @@ -59,7 +52,6 @@ public class RepeatEachEffect extends SpellAbilityEffect { } repeatCards = CardLists.getValidCards(game.getCardsIn(zone), sa.getParam("RepeatCards"), source.getController(), source); - loopOverCards = !recordChoice; } else if (sa.hasParam(("RepeatSpellAbilities"))) { repeatSas = Lists.newArrayList(); @@ -138,10 +130,9 @@ public class RepeatEachEffect extends SpellAbilityEffect { boolean optional = sa.hasParam("RepeatOptionalForEachPlayer"); boolean nextTurn = sa.hasParam("NextTurnForEachPlayer"); if (sa.hasParam("StartingWithActivator")) { - int size = repeatPlayers.size(); - Player activator = sa.getActivatingPlayer(); - while (!activator.equals(repeatPlayers.getFirst())) { - repeatPlayers.add(size - 1, repeatPlayers.remove(0)); + int aidx = repeatPlayers.indexOf(player); + if (aidx != -1) { + Collections.rotate(repeatPlayers, -aidx); } } for (final Player p : repeatPlayers) { @@ -164,73 +155,6 @@ public class RepeatEachEffect extends SpellAbilityEffect { } } } - - if (sa.hasParam("RepeatCounters")) { - Card target = sa.getTargetCard(); - if (target == null) { - target = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).get(0); - } - for (CounterType type : target.getCounters().keySet()) { - StringBuilder sb = new StringBuilder(); - sb.append("Number$").append(target.getCounters(type)); - source.setSVar("RepeatSVarCounter", type.getName().toUpperCase()); - source.setSVar("RepeatCounterAmount", sb.toString()); - AbilityUtils.resolve(repeat); - } - } - if (recordChoice) { - boolean random = sa.hasParam("Random"); - Map> recordMap = Maps.newHashMap(); - if (sa.hasParam("ChoosePlayer")) { - for (Card card : repeatCards) { - Player p; - if (random) { - p = Aggregates.random(game.getPlayers()); - } else { - p = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(game.getPlayers(), sa, Localizer.getInstance().getMessage("lblChoosePlayer")); - } - if (recordMap.containsKey(p)) { - recordMap.get(p).add(0, card); - } else { - recordMap.put(p, Lists.newArrayList(card)); - } - } - } - else if (sa.hasParam("ChooseCard")) { - List list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), - sa.getParam("ChooseCard"), source.getController(), source); - String filterController = sa.getParam("FilterControlledBy"); - // default: Starting with you and proceeding in the chosen direction - Player p = sa.getActivatingPlayer(); - do { - CardCollection valid = new CardCollection(list); - if ("NextPlayerInChosenDirection".equals(filterController)) { - valid = CardLists.filterControlledBy(valid, - game.getNextPlayerAfter(p, source.getChosenDirection())); - } - Card card = p.getController().chooseSingleEntityForEffect(valid, sa, Localizer.getInstance().getMessage("lblChooseaCard")); - if (recordMap.containsKey(p)) { - recordMap.get(p).add(0, card); - } else { - recordMap.put(p, Lists.newArrayList(card)); - } - if (source.getChosenDirection() != null) { - p = game.getNextPlayerAfter(p, source.getChosenDirection()); - } else { - p = game.getNextPlayerAfter(p); - } - } while (!p.equals(sa.getActivatingPlayer())); - } - - for (Entry> entry : recordMap.entrySet()) { - // Remember the player and imprint the cards - source.addRemembered(entry.getKey()); - source.addImprintedCards(entry.getValue()); - AbilityUtils.resolve(repeat); - source.removeRemembered(entry.getKey()); - source.removeImprintedCards(entry.getValue()); - } - } if(sa.hasParam("DamageMap")) { sa.getPreventMap().triggerPreventDamage(false); diff --git a/forge-game/src/main/java/forge/game/ability/effects/RepeatEffect.java b/forge-game/src/main/java/forge/game/ability/effects/RepeatEffect.java index e22f817af6e..eaed68ffe73 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/RepeatEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/RepeatEffect.java @@ -5,7 +5,6 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollectionView; -import forge.game.card.CardFactoryUtil; import forge.game.card.CardLists; import forge.game.player.Player; import forge.game.spellability.AbilitySub; @@ -94,15 +93,8 @@ public class RepeatEffect extends SpellAbilityEffect { } list = CardLists.getValidCards(list, repeatPresent.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa); - int right; final String rightString = repeatCompare.substring(2); - try { // If this is an Integer, just parse it - right = Integer.parseInt(rightString); - } catch (final NumberFormatException e) { // Otherwise, grab it from - // the - // SVar - right = CardFactoryUtil.xCount(sa.getHostCard(), sa.getHostCard().getSVar(rightString)); - } + int right = AbilityUtils.calculateAmount(sa.getHostCard(), rightString, sa); final int left = list.size(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java index f4a84e42a43..7e05cec8c9f 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java @@ -45,13 +45,13 @@ public class SacrificeEffect extends SpellAbilityEffect { } } else if (sa.hasParam("CumulativeUpkeep")) { GameEntityCounterTable table = new GameEntityCounterTable(); - card.addCounter(CounterType.AGE, 1, activator, true, table); + card.addCounter(CounterEnumType.AGE, 1, activator, true, table); table.triggerCountersPutAll(game); Cost cumCost = new Cost(sa.getParam("CumulativeUpkeep"), true); Cost payCost = new Cost(ManaCost.ZERO, true); - int n = card.getCounters(CounterType.AGE); + int n = card.getCounters(CounterEnumType.AGE); // multiply cost for (int i = 0; i < n; ++i) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/SetStateEffect.java b/forge-game/src/main/java/forge/game/ability/effects/SetStateEffect.java index e25e9cc2ef3..56d064457b8 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/SetStateEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/SetStateEffect.java @@ -8,7 +8,7 @@ import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardUtil; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.event.GameEventCardStatsChanged; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; @@ -125,7 +125,7 @@ public class SetStateEffect extends SpellAbilityEffect { } game.fireEvent(new GameEventCardStatsChanged(tgt)); if (sa.hasParam("Mega")) { - tgt.addCounter(CounterType.P1P1, 1, p, true, table); + tgt.addCounter(CounterEnumType.P1P1, 1, p, true, table); } if (remChanged) { host.addRemembered(tgt); diff --git a/forge-game/src/main/java/forge/game/ability/effects/TokenEffect.java b/forge-game/src/main/java/forge/game/ability/effects/TokenEffect.java index 5f27c905323..afe5751103c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/TokenEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/TokenEffect.java @@ -17,189 +17,25 @@ */ package forge.game.ability.effects; -import java.util.Arrays; -import java.util.List; +import org.apache.commons.lang3.mutable.MutableBoolean; -import forge.card.MagicColor; import forge.game.ability.AbilityKey; import forge.game.card.token.TokenInfo; -import org.apache.commons.lang3.StringUtils; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; - -import forge.card.CardType; import forge.game.Game; -import forge.game.GameEntity; -import forge.game.GameObject; -import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; -import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; -import forge.game.card.CardCollection; -import forge.game.card.CardUtil; import forge.game.card.CardZoneTable; -import forge.game.combat.Combat; import forge.game.event.GameEventCombatChanged; import forge.game.event.GameEventTokenCreated; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.trigger.Trigger; -import forge.game.trigger.TriggerHandler; -import forge.game.zone.ZoneType; -import forge.util.Localizer; -import forge.util.CardTranslation; -import forge.item.PaperToken; -import forge.util.collect.FCollectionView; -import forge.util.MyRandom; -public class TokenEffect extends SpellAbilityEffect { - - private String tokenOwner; - private String[] tokenColors; - private String[] tokenOriginalColors; - private String tokenImage; - private String[] tokenAltImages; - private String[] tokenAbilities; - private String[] tokenTriggers; - private String[] tokenSVars; - private String[] tokenStaticAbilities; - private boolean tokenTapped; - private boolean tokenAttacking; - private String tokenBlocking; - private String tokenAmount; - private String tokenToughness; - private String tokenPower; - private String[] tokenTypes; - private String[] tokenOriginalTypes; - private String tokenName; - private String tokenOriginalName; - private String[] tokenKeywords; - private String[] tokenHiddenKeywords; - - private void readMetaParams(final SpellAbility mapParams) { - this.tokenTapped = mapParams.getParamOrDefault("TokenTapped", "False").equalsIgnoreCase("True"); - this.tokenAttacking = mapParams.getParamOrDefault("TokenAttacking", "False").equalsIgnoreCase("True"); - this.tokenBlocking = mapParams.getParam("TokenBlocking"); - - this.tokenAmount = mapParams.getParamOrDefault("TokenAmount", "1"); - this.tokenOwner = mapParams.getParamOrDefault("TokenOwner", "You"); - } - - private void readParameters(final SpellAbility mapParams, Card prototype) { - readMetaParams(mapParams); - if (prototype == null) { - readTokenParams(mapParams); - } - } - - private void readTokenParams(final SpellAbility mapParams) { - String image; - String[] keywords; - - if (mapParams.hasParam("TokenKeywords")) { - // TODO: Change this Split to a semicolon or something else - keywords = mapParams.getParam("TokenKeywords").split("<>"); - } else { - keywords = new String[0]; - } - - if (mapParams.hasParam("TokenHiddenKeywords")) { - this.tokenHiddenKeywords = mapParams.getParam("TokenHiddenKeywords").split("&"); - } - - if (mapParams.hasParam("TokenImage")) { - image = PaperToken.makeTokenFileName(mapParams.getParam("TokenImage")); - } else { - image = ""; - } - - if (mapParams.hasParam("TokenAltImages")) { - this.tokenAltImages = mapParams.getParam("TokenAltImages").split(","); - for (int i = 0; i < tokenAltImages.length; i++) { - this.tokenAltImages[i] = PaperToken.makeTokenFileName(this.tokenAltImages[i].trim()); - } - } else { - this.tokenAltImages = null; - } - - if (mapParams.hasParam("TokenAbilities")) { - this.tokenAbilities = mapParams.getParam("TokenAbilities").split(","); - } else { - this.tokenAbilities = null; - } - if (mapParams.hasParam("TokenTriggers")) { - this.tokenTriggers = mapParams.getParam("TokenTriggers").split(","); - } else { - this.tokenTriggers = null; - } - if (mapParams.hasParam("TokenSVars")) { - this.tokenSVars = mapParams.getParam("TokenSVars").split(","); - } else { - this.tokenSVars = null; - } - if (mapParams.hasParam("TokenStaticAbilities")) { - this.tokenStaticAbilities = mapParams.getParam("TokenStaticAbilities").split(","); - } else { - this.tokenStaticAbilities = null; - } - - this.tokenPower = mapParams.getParam("TokenPower"); - this.tokenToughness = mapParams.getParam("TokenToughness"); - - this.tokenOriginalTypes = mapParams.getOriginalMapParams().get("TokenTypes").split(","); - this.tokenTypes = mapParams.getParam("TokenTypes").split(","); - - if (mapParams.hasParam("TokenName")) { - this.tokenOriginalName = mapParams.getOriginalMapParams().get("TokenName"); - this.tokenName = mapParams.getParam("TokenName"); - } else { - this.tokenOriginalName = StringUtils.join(new CardType(Lists.newArrayList(this.tokenOriginalTypes)).getSubtypes(), " "); - this.tokenName = StringUtils.join(new CardType(Lists.newArrayList(this.tokenTypes)).getSubtypes(), " "); - } - - this.tokenOriginalColors = mapParams.getOriginalMapParams().get("TokenColors").split(","); - this.tokenColors = mapParams.getParam("TokenColors").split(","); - - this.tokenKeywords = keywords; - this.tokenImage = image; - } +public class TokenEffect extends TokenEffectBase { @Override protected String getStackDescription(SpellAbility sa) { - final StringBuilder sb = new StringBuilder(); - final Card host = sa.getHostCard(); - Card prototype = loadTokenPrototype(sa); - - if (prototype != null) { - return sa.getDescription(); - } - - readParameters(sa, prototype); - - final int finalPower = AbilityUtils.calculateAmount(host, this.tokenPower, sa); - final int finalToughness = AbilityUtils.calculateAmount(host, this.tokenToughness, sa); - final int finalAmount = AbilityUtils.calculateAmount(host, this.tokenAmount, sa); - - final String substitutedName = this.tokenName.equals("ChosenType") ? host.getChosenType() : this.tokenName; - - sb.append("Create (").append(finalAmount).append(") "); - if (Arrays.asList(this.tokenTypes).contains("Creature")) { - sb.append(finalPower).append("/").append(finalToughness).append(" "); - } - sb.append(substitutedName).append(" token"); - if (finalAmount != 1) { - sb.append("s"); - } - - if (this.tokenOwner.equals("Opponent")) { - sb.append(" under your opponent's control."); - } else { - sb.append("."); - } - - return sb.toString(); + return sa.getDescription(); } public Card loadTokenPrototype(SpellAbility sa) { @@ -209,9 +45,7 @@ public class TokenEffect extends SpellAbilityEffect { final Card result = TokenInfo.getProtoType(sa.getParam("TokenScript"), sa); - if (result != null) { - tokenName = result.getName(); - } else { + if (result == null) { throw new RuntimeException("don't find Token for TokenScript: " + sa.getParam("TokenScript")); } @@ -224,6 +58,18 @@ public class TokenEffect extends SpellAbilityEffect { final Game game = host.getGame(); final SpellAbility root = sa.getRootAbility(); + // linked Abilities, if it needs chosen values, but nothing is chosen, no token can be created + if (sa.hasParam("TokenTypes")) { + if (sa.getParam("TokenTypes").contains("ChosenType") && !host.hasChosenType()) { + return; + } + } + if (sa.hasParam("TokenColors")) { + if (sa.getParam("TokenColors").contains("ChosenColor") && !host.hasChosenColor()) { + return; + } + } + // Cause of the Token Effect, in general it should be this // but if its a Replacement Effect, it might be something else or null SpellAbility cause = sa; @@ -234,55 +80,9 @@ public class TokenEffect extends SpellAbilityEffect { } } - final boolean remember = sa.hasParam("RememberTokens"); - final boolean imprint = sa.hasParam("ImprintTokens"); - final List allTokens = Lists.newArrayList(); - - boolean combatChanged = false; - boolean inCombat = game.getPhaseHandler().inCombat(); - Card prototype = loadTokenPrototype(sa); - readParameters(sa, prototype); - final int finalAmount = AbilityUtils.calculateAmount(host, this.tokenAmount, sa); - - TokenInfo tokenInfo; - - if (prototype == null) { - String originalColorDesc = parseColorForImage(); - - final List imageNames = Lists.newArrayListWithCapacity(1); - if (this.tokenImage.equals("")) { - imageNames.add(PaperToken.makeTokenFileName(originalColorDesc, tokenPower, tokenToughness, tokenOriginalName)); - } else { - imageNames.add(0, this.tokenImage); - } - if (this.tokenAltImages != null) { - imageNames.addAll(Arrays.asList(this.tokenAltImages)); - } - - String cost = determineTokenColor(host); - - final int finalPower = AbilityUtils.calculateAmount(host, this.tokenPower, sa); - final int finalToughness = AbilityUtils.calculateAmount(host, this.tokenToughness, sa); - - final String[] substitutedTypes = Arrays.copyOf(this.tokenTypes, this.tokenTypes.length); - for (int i = 0; i < substitutedTypes.length; i++) { - if (substitutedTypes[i].equals("ChosenType")) { - substitutedTypes[i] = host.getChosenType(); - } - } - final String substitutedName = this.tokenName.equals("ChosenType") ? host.getChosenType() : this.tokenName; - - final String imageName = imageNames.get(MyRandom.getRandom().nextInt(imageNames.size())); - tokenInfo = new TokenInfo(substitutedName, imageName, - cost, substitutedTypes, this.tokenKeywords, finalPower, finalToughness); - } else { - // TODO: Substitute type name for Chosen tokens - // TODO: If host has has it's color/type altered make sure that's appropriately applied - // TODO: Lock down final power and toughness if it's actually X values - tokenInfo = new TokenInfo(prototype, host); - } + final int finalAmount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("TokenAmount", "1"), sa); boolean useZoneTable = true; CardZoneTable triggerList = sa.getChangeZoneTable(); @@ -295,66 +95,9 @@ public class TokenEffect extends SpellAbilityEffect { useZoneTable = true; } - for (final Player controller : AbilityUtils.getDefinedPlayers(host, this.tokenOwner, sa)) { - List tokens; - - if (prototype == null) { - tokens = tokenInfo.makeTokenWithMultiplier(controller, finalAmount, cause != null); - grantHiddenKeywords(tokens); - grantSvars(tokens, sa); - grantAbilities(tokens, sa); - grantTriggers(tokens, sa); - grantStatics(tokens, sa); - } else { - tokens = TokenInfo.makeTokensFromPrototype(prototype, controller, finalAmount, cause != null); - } - - for (Card tok : tokens) { - if (this.tokenTapped) { - tok.setTapped(true); - } - - if (sa.hasParam("AttachedTo") && !attachTokenTo(tok, sa)) { - continue; - } - - // Should this be catching the Card that's returned? - Card c = game.getAction().moveToPlay(tok, sa); - if (c.getZone() != null) { - triggerList.put(ZoneType.None, c.getZone().getZoneType(), c); - } - - if (sa.hasParam("AtEOTTrig")) { - addSelfTrigger(sa, sa.getParam("AtEOTTrig"), c); - } - - if (inCombat) { - combatChanged = addTokenToCombat(game, c, tok.getController(), sa, host) || combatChanged; - } - - c.updateStateForView(); - - if (remember) { - game.getCardState(sa.getHostCard()).addRemembered(c); - } - if (imprint) { - game.getCardState(sa.getHostCard()).addImprintedCard(c); - } - if (sa.hasParam("RememberSource")) { - game.getCardState(c).addRemembered(host); - } - if (sa.hasParam("TokenRemembered")) { - final Card token = game.getCardState(c); - final String remembered = sa.getParam("TokenRemembered"); - for (final Object o : AbilityUtils.getDefinedObjects(host, remembered, sa)) { - token.addRemembered(o); - } - } - allTokens.add(c); - } - if ("Clue".equals(tokenName)) { // investigate trigger - controller.addInvestigatedThisTurn(); - } + MutableBoolean combatChanged = new MutableBoolean(false); + for (final Player owner : AbilityUtils.getDefinedPlayers(host, sa.getParamOrDefault("TokenOwner", "You"), sa)) { + makeTokens(prototype, owner, sa, finalAmount, cause != null, false, triggerList, combatChanged); } if (!useZoneTable) { @@ -364,190 +107,9 @@ public class TokenEffect extends SpellAbilityEffect { game.fireEvent(new GameEventTokenCreated()); - if (combatChanged) { + if (combatChanged.isTrue()) { game.updateCombatForView(); game.fireEvent(new GameEventCombatChanged()); } - if (sa.hasParam("AtEOT")) { - registerDelayedTrigger(sa, sa.getParam("AtEOT"), allTokens); - } - } - - private String parseColorForImage() { - StringBuilder originalColorDesc = new StringBuilder(); - for (final String col : this.tokenOriginalColors) { - originalColorDesc.append(MagicColor.toShortString(col)); - if (originalColorDesc.toString().equals("C")) { - return originalColorDesc.toString(); - } - } - return originalColorDesc.toString(); - } - - private String determineTokenColor(Card host) { - final String[] substitutedColors = Arrays.copyOf(this.tokenColors, this.tokenColors.length); - for (int i = 0; i < substitutedColors.length; i++) { - if (substitutedColors[i].equals("ChosenColor")) { - // this currently only supports 1 chosen color - substitutedColors[i] = host.getChosenColor(); - } - } - - StringBuilder sb = new StringBuilder(); - for (final String col : substitutedColors) { - String str = MagicColor.toShortString(col); - if (str.equals("C")) { - return "1"; - } - - sb.append(str).append(" "); - } - - return sb.toString().trim(); - } - - private void grantHiddenKeywords(List tokens) { - // Grant rule changes - if (this.tokenHiddenKeywords != null) { - for (final String s : this.tokenHiddenKeywords) { - for (final Card c : tokens) { - c.addHiddenExtrinsicKeyword(s); - } - } - } - } - - private void grantAbilities(List tokens, SpellAbility root) { - if (this.tokenAbilities != null) { - for (final String s : this.tokenAbilities) { - final String actualAbility = AbilityUtils.getSVar(root, s); - for (final Card c : tokens) { - final SpellAbility grantedAbility = AbilityFactory.getAbility(actualAbility, c); - // Set intrinsic, so that effects like Clone will copy these. - grantedAbility.setIntrinsic(true); - c.addSpellAbility(grantedAbility); - } - } - } - } - - private void grantTriggers(List tokens, SpellAbility root) { - if (this.tokenTriggers != null) { - for (final String s : this.tokenTriggers) { - final String actualTrigger = AbilityUtils.getSVar(root, s); - for (final Card c : tokens) { - final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, c, true); - final String ability = AbilityUtils.getSVar(root, parsedTrigger.getParam("Execute")); - parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(ability, c)); - c.addTrigger(parsedTrigger); - } - } - } - } - - private void grantSvars(List tokens, SpellAbility root) { - if (this.tokenSVars != null) { - for (final String s : this.tokenSVars) { - String actualSVar = AbilityUtils.getSVar(root, s); - String name = s; - if (actualSVar.startsWith("SVar")) { - actualSVar = actualSVar.split("SVar:")[1]; - name = actualSVar.split(":")[0]; - actualSVar = actualSVar.split(":")[1]; - } - for (final Card c : tokens) { - c.setSVar(name, actualSVar); - } - } - } - } - - private void grantStatics(List tokens, SpellAbility root) { - if (this.tokenStaticAbilities != null) { - for (final String s : this.tokenStaticAbilities) { - final String actualAbility = AbilityUtils.getSVar(root, s); - for (final Card c : tokens) { - c.addStaticAbility(actualAbility); - } - } - } - } - - private boolean addTokenToCombat(Game game, Card c, Player controller, SpellAbility sa, Card host) { - boolean combatChanged = false; - if (this.tokenAttacking) { - final Combat combat = game.getCombat(); - - // into battlefield attacking only should work if you are the attacking player - if (combat.getAttackingPlayer().equals(controller)) { - final FCollectionView defs = combat.getDefenders(); - final GameEntity defender = controller.getController().chooseSingleEntityForEffect(defs, sa, Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())), false); - combat.addAttacker(c, defender); - combatChanged = true; - } - } - if (this.tokenBlocking != null) { - final Combat combat = game.getPhaseHandler().getCombat(); - final Card attacker = Iterables.getFirst(AbilityUtils.getDefinedCards(host, this.tokenBlocking, sa), null); - if (attacker != null) { - final boolean wasBlocked = combat.isBlocked(attacker); - combat.addBlocker(attacker, c); - combat.orderAttackersForDamageAssignment(c); - - // Run triggers for new blocker and add it to damage assignment order - if (!wasBlocked) { - combat.setBlocked(attacker, true); - combat.addBlockerToDamageAssignmentOrder(attacker, c); - } - combatChanged = true; - } - } - return combatChanged; - } - - private boolean attachTokenTo(Card tok, SpellAbility sa) { - final Card host = sa.getHostCard(); - final Game game = host.getGame(); - - GameObject aTo = Iterables.getFirst( - AbilityUtils.getDefinedObjects(host, sa.getParam("AttachedTo"), sa), null); - - if (aTo instanceof GameEntity) { - GameEntity ge = (GameEntity)aTo; - // check what the token would be on the battlefield - Card lki = CardUtil.getLKICopy(tok); - - lki.setLastKnownZone(tok.getController().getZone(ZoneType.Battlefield)); - - // double freeze tracker, so it doesn't update view - game.getTracker().freeze(); - CardCollection preList = new CardCollection(lki); - game.getAction().checkStaticAbilities(false, Sets.newHashSet(lki), preList); - - // TODO update when doing Attach Update - boolean canAttach = lki.isAttachment(); - - if (canAttach && !ge.canBeAttached(lki)) { - canAttach = false; - } - - // reset static abilities - game.getAction().checkStaticAbilities(false); - // clear delayed changes, this check should not have updated the view - game.getTracker().clearDelayed(); - // need to unfreeze tracker - game.getTracker().unfreeze(); - - if (!canAttach) { - // Token can't attach to it - return false; - } - - tok.attachToEntity(ge); - return true; - } else { - // not a GameEntity, cant be attach - return false; - } } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/TokenEffectBase.java b/forge-game/src/main/java/forge/game/ability/effects/TokenEffectBase.java new file mode 100644 index 00000000000..6768eb14849 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/TokenEffectBase.java @@ -0,0 +1,231 @@ +package forge.game.ability.effects; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.mutable.MutableBoolean; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import forge.GameCommand; +import forge.game.Game; +import forge.game.GameEntity; +import forge.game.GameObject; +import forge.game.ability.AbilityUtils; +import forge.game.ability.SpellAbilityEffect; +import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.card.CardUtil; +import forge.game.card.CardZoneTable; +import forge.game.card.token.TokenInfo; +import forge.game.combat.Combat; +import forge.game.event.GameEventCardStatsChanged; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; +import forge.util.CardTranslation; +import forge.util.Localizer; +import forge.util.collect.FCollectionView; + +public abstract class TokenEffectBase extends SpellAbilityEffect { + + protected List makeTokens(final Card prototype, final Player creator, final SpellAbility sa, int finalAmount, + boolean applyMultiplier, boolean clone, CardZoneTable triggerList, MutableBoolean combatChanged) { + final Card host = sa.getHostCard(); + final Game game = host.getGame(); + final long timestamp = game.getNextTimestamp(); + + final List pumpKeywords = Lists.newArrayList(); + if (sa.hasParam("PumpKeywords")) { + pumpKeywords.addAll(Arrays.asList(sa.getParam("PumpKeywords").split(" & "))); + } + + List allTokens = Lists.newArrayList(); + for (Card tok : TokenInfo.makeTokensFromPrototype(prototype, creator, finalAmount, applyMultiplier)) { + if (sa.hasParam("TokenTapped")) { + tok.setTapped(true); + } + + if (!sa.hasParam("AttachAfter") && sa.hasParam("AttachedTo") && !attachTokenTo(tok, sa)) { + continue; + } + + if (clone) { + tok.setCopiedPermanent(prototype); + } + + // Should this be catching the Card that's returned? + Card c = game.getAction().moveToPlay(tok, sa); + if (c == null || c.getZone() == null) { + // in case token can't enter the battlefield, it isn't created + continue; + } + triggerList.put(ZoneType.None, c.getZone().getZoneType(), c); + + creator.addTokensCreatedThisTurn(); + + if (clone) { + c.setCloneOrigin(host); + } + if (!pumpKeywords.isEmpty()) { + c.addChangedCardKeywords(pumpKeywords, Lists.newArrayList(), false, false, timestamp); + addPumpUntil(sa, c, timestamp); + } + + if (sa.hasParam("AtEOTTrig")) { + addSelfTrigger(sa, sa.getParam("AtEOTTrig"), c); + } + + if (addTokenToCombat(game, c, tok.getController(), sa, host)) { + combatChanged.setTrue(); + } + + if (sa.hasParam("AttachAfter") && sa.hasParam("AttachedTo")) { + attachTokenTo(tok, sa); + } + + c.updateStateForView(); + + if (sa.hasParam("RememberTokens")) { + game.getCardState(sa.getHostCard()).addRemembered(c); + } + if (sa.hasParam("ImprintTokens")) { + game.getCardState(sa.getHostCard()).addImprintedCard(c); + } + if (sa.hasParam("RememberSource")) { + game.getCardState(c).addRemembered(host); + } + if (sa.hasParam("TokenRemembered")) { + final Card token = game.getCardState(c); + final String remembered = sa.getParam("TokenRemembered"); + for (final Object o : AbilityUtils.getDefinedObjects(host, remembered, sa)) { + token.addRemembered(o); + } + } + allTokens.add(c); + } + + if (sa.hasParam("AtEOT")) { + registerDelayedTrigger(sa, sa.getParam("AtEOT"), allTokens); + } + return allTokens; + } + + private boolean addTokenToCombat(Game game, Card c, Player controller, SpellAbility sa, Card host) { + if (!game.getPhaseHandler().inCombat()) { + return false; + } + boolean combatChanged = false; + final Combat combat = game.getCombat(); + + if (sa.hasParam("TokenAttacking") && combat.getAttackingPlayer().equals(controller)) { + String attacking = sa.getParam("TokenAttacking"); + GameEntity defender = null; + if ("True".equalsIgnoreCase(attacking)) { + // into battlefield attacking only should work if you are the attacking player + if (combat.getAttackingPlayer().equals(controller)) { + final FCollectionView defs = combat.getDefenders(); + Map params = Maps.newHashMap(); + params.put("Attacker", c); + defender = controller.getController().chooseSingleEntityForEffect(defs, sa, + Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())), false, params); + } + } else { + defender = Iterables.getFirst(AbilityUtils.getDefinedEntities(host, attacking, sa), null); + } + if (defender != null) { + combat.addAttacker(c, defender); + combatChanged = true; + } + } + if (sa.hasParam("TokenBlocking")) { + final Card attacker = Iterables.getFirst(AbilityUtils.getDefinedCards(host, sa.getParam("TokenBlocking"), sa), null); + if (attacker != null) { + final boolean wasBlocked = combat.isBlocked(attacker); + combat.addBlocker(attacker, c); + combat.orderAttackersForDamageAssignment(c); + + // Run triggers for new blocker and add it to damage assignment order + if (!wasBlocked) { + combat.setBlocked(attacker, true); + combat.addBlockerToDamageAssignmentOrder(attacker, c); + } + combatChanged = true; + } + } + return combatChanged; + } + + private boolean attachTokenTo(Card tok, SpellAbility sa) { + final Card host = sa.getHostCard(); + final Game game = host.getGame(); + + GameObject aTo = Iterables.getFirst( + AbilityUtils.getDefinedObjects(host, sa.getParam("AttachedTo"), sa), null); + + if (aTo instanceof GameEntity) { + GameEntity ge = (GameEntity)aTo; + // check what the token would be on the battlefield + Card lki = CardUtil.getLKICopy(tok); + + lki.setLastKnownZone(tok.getController().getZone(ZoneType.Battlefield)); + + // double freeze tracker, so it doesn't update view + game.getTracker().freeze(); + CardCollection preList = new CardCollection(lki); + game.getAction().checkStaticAbilities(false, Sets.newHashSet(lki), preList); + + boolean canAttach = lki.isAttachment(); + + if (canAttach && !ge.canBeAttached(lki)) { + canAttach = false; + } + + // reset static abilities + game.getAction().checkStaticAbilities(false); + // clear delayed changes, this check should not have updated the view + game.getTracker().clearDelayed(); + // need to unfreeze tracker + game.getTracker().unfreeze(); + + if (!canAttach) { + // Token can't attach to it + return false; + } + + tok.attachToEntity(ge); + return true; + } else { + // not a GameEntity, cant be attach + return false; + } + } + + protected void addPumpUntil(SpellAbility sa, final Card c, long timestamp) { + if (!sa.hasParam("PumpDuration")) { + return; + } + final String duration = sa.getParam("PumpDuration"); + final Card host = sa.getHostCard(); + final Game game = host.getGame(); + final GameCommand untilEOT = new GameCommand() { + private static final long serialVersionUID = -42244224L; + + @Override + public void run() { + c.removeChangedCardKeywords(timestamp); + game.fireEvent(new GameEventCardStatsChanged(c)); + } + }; + + if ("UntilYourNextTurn".equals(duration)) { + game.getCleanup().addUntil(sa.getActivatingPlayer(), untilEOT); + } else { + game.getEndOfTurn().addUntil(untilEOT); + } + } +} diff --git a/forge-game/src/main/java/forge/game/ability/effects/TwoPilesEffect.java b/forge-game/src/main/java/forge/game/ability/effects/TwoPilesEffect.java index 9c4d2d881a3..d5b03eb83eb 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/TwoPilesEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/TwoPilesEffect.java @@ -95,7 +95,7 @@ public class TwoPilesEffect extends SpellAbilityEffect { card.clearRemembered(); // first, separate the cards into piles - final CardCollectionView pile1 = separator.getController().chooseCardsForEffect(pool, sa, title, 0, size, false); + final CardCollectionView pile1 = separator.getController().chooseCardsForEffect(pool, sa, title, 0, size, false, null); final CardCollection pile2 = new CardCollection(pool); pile2.removeAll(pile1); diff --git a/forge-game/src/main/java/forge/game/ability/effects/UntapEffect.java b/forge-game/src/main/java/forge/game/ability/effects/UntapEffect.java index 584a1739ffb..d2c1a94dbd5 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/UntapEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/UntapEffect.java @@ -81,7 +81,7 @@ public class UntapEffect extends SpellAbilityEffect { valid, sa.getActivatingPlayer(), sa.getHostCard()); list = CardLists.filter(list, Presets.TAPPED); - final CardCollectionView selected = p.getController().chooseCardsForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectCardToUntap"), mandatory ? num : 0, num, !mandatory); + final CardCollectionView selected = p.getController().chooseCardsForEffect(list, sa, Localizer.getInstance().getMessage("lblSelectCardToUntap"), mandatory ? num : 0, num, !mandatory, null); if (selected != null) { for (final Card c : selected) { c.untap(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/VoteEffect.java b/forge-game/src/main/java/forge/game/ability/effects/VoteEffect.java index 769e758388c..cc572a9c5fd 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/VoteEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/VoteEffect.java @@ -1,6 +1,7 @@ package forge.game.ability.effects; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -9,9 +10,9 @@ import forge.game.ability.AbilityKey; import org.apache.commons.lang3.StringUtils; import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import forge.game.Game; import forge.game.ability.AbilityFactory; @@ -19,10 +20,7 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardLists; -import forge.game.card.CardPredicates; import forge.game.player.Player; -import forge.game.player.PlayerCollection; -import forge.game.player.PlayerPredicates; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; import forge.game.trigger.TriggerType; @@ -67,33 +65,29 @@ public class VoteEffect extends SpellAbilityEffect { return; } - // starting with the activator - int pSize = tgtPlayers.size(); Player activator = sa.getActivatingPlayer(); - while (tgtPlayers.contains(activator) && !activator.equals(Iterables.getFirst(tgtPlayers, null))) { - tgtPlayers.add(pSize - 1, tgtPlayers.remove(0)); + + // starting with the activator + int aidx = tgtPlayers.indexOf(activator); + if (aidx != -1) { + Collections.rotate(tgtPlayers, -aidx); } + ListMultimap votes = ArrayListMultimap.create(); - Player voter = null; - - PlayerCollection voters = game.getPlayers().filter(PlayerPredicates.hasKeyword("You choose how each player votes this turn.")); - - if (voters.size() > 1) { - List illusions = CardLists.filter(voters.getCardsIn(ZoneType.Command), CardPredicates.nameEquals("Illusion of Choice Effect")); - voter = Collections.max(illusions, CardPredicates.compareByTimestamp()).getController(); - } else if (voters.size() == 1) { - voter = voters.get(0); - } + Player voter = game.getControlVote(); for (final Player p : tgtPlayers) { - int voteAmount = p.getKeywords().getAmount("You get an additional vote.") + 1; - int optionalVotes = p.getKeywords().getAmount("You may vote an additional time."); - voteAmount += p.getController().chooseNumber(sa, Localizer.getInstance().getMessage("lblHowManyAdditionalVotesDoYouWant"), 0, optionalVotes); + int voteAmount = p.getAdditionalVotesAmount() + 1; + int optionalVotes = p.getAdditionalOptionalVotesAmount(); Player realVoter = voter == null ? p : voter; + Map params = Maps.newHashMap(); + params.put("Voter", realVoter); + voteAmount += p.getController().chooseNumber(sa, Localizer.getInstance().getMessage("lblHowManyAdditionalVotesDoYouWant"), 0, optionalVotes, params); + for (int i = 0; i < voteAmount; i++) { - Object result = realVoter.getController().vote(sa, host + Localizer.getInstance().getMessage("lblVote") + ":", voteType, votes); + Object result = realVoter.getController().vote(sa, host + Localizer.getInstance().getMessage("lblVote") + ":", voteType, votes, p); votes.put(result, p); host.getGame().getAction().nofityOfValue(sa, p, result + "\r\n" + Localizer.getInstance().getMessage("lblCurrentVote") + ":" + votes, p); @@ -104,34 +98,49 @@ public class VoteEffect extends SpellAbilityEffect { runParams.put(AbilityKey.AllVotes, votes); game.getTriggerHandler().runTrigger(TriggerType.Vote, runParams, false); - List subAbs = Lists.newArrayList(); - final List mostVotes = getMostVotes(votes); - if (sa.hasParam("Tied") && mostVotes.size() > 1) { - subAbs.add(sa.getParam("Tied")); - } else if (sa.hasParam("VoteSubAbility")) { - for (final Object o : mostVotes) { - host.addRemembered(o); - } - subAbs.add(sa.getParam("VoteSubAbility")); - } else { - for (Object type : mostVotes) { - subAbs.add(sa.getParam("Vote" + type.toString())); - } - } - if (sa.hasParam("StoreVoteNum")) { - for (final Object type : voteType) { - host.setSVar("VoteNum" + type, "Number$" + votes.get(type).size()); - } - } else { - for (final String subAb : subAbs) { - final SpellAbility action = AbilityFactory.getAbility(host.getSVar(subAb), host); + if (sa.hasParam("EachVote")) { + for (Map.Entry> e : votes.asMap().entrySet()) { + final SpellAbility action = AbilityFactory.getAbility(host, sa.getParam("Vote" + e.getKey().toString())); + action.setActivatingPlayer(sa.getActivatingPlayer()); ((AbilitySub) action).setParent(sa); - AbilityUtils.resolve(action); + + for (Player p : e.getValue()) { + host.addRemembered(p); + AbilityUtils.resolve(action); + host.removeRemembered(p); + } + } + } else { + List subAbs = Lists.newArrayList(); + final List mostVotes = getMostVotes(votes); + if (sa.hasParam("Tied") && mostVotes.size() > 1) { + subAbs.add(sa.getParam("Tied")); + } else if (sa.hasParam("VoteSubAbility")) { + for (final Object o : mostVotes) { + host.addRemembered(o); + } + subAbs.add(sa.getParam("VoteSubAbility")); + } else { + for (Object type : mostVotes) { + subAbs.add(sa.getParam("Vote" + type.toString())); + } + } + if (sa.hasParam("StoreVoteNum")) { + for (final Object type : voteType) { + host.setSVar("VoteNum" + type, "Number$" + votes.get(type).size()); + } + } else { + for (final String subAb : subAbs) { + final SpellAbility action = AbilityFactory.getAbility(host, subAb); + action.setActivatingPlayer(sa.getActivatingPlayer()); + ((AbilitySub) action).setParent(sa); + AbilityUtils.resolve(action); + } + } + if (sa.hasParam("VoteSubAbility")) { + host.clearRemembered(); } - } - if (sa.hasParam("VoteSubAbility")) { - host.clearRemembered(); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ZoneExchangeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ZoneExchangeEffect.java index 1353090efd6..b2a41fed62d 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ZoneExchangeEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ZoneExchangeEffect.java @@ -70,7 +70,7 @@ public class ZoneExchangeEffect extends SpellAbilityEffect { return; } - Card object2 = p.getController().chooseSingleEntityForEffect(list, sa, Localizer.getInstance().getMessage("lblChooseaCard"), !sa.hasParam("Mandatory")); + Card object2 = p.getController().chooseSingleEntityForEffect(list, sa, Localizer.getInstance().getMessage("lblChooseaCard"), !sa.hasParam("Mandatory"), null); if (object2 == null || !object2.isInZone(zone2) || (type != null && !object2.getType().hasStringType(type))) { return; } 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 0be3279d3e4..753b99333cf 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -40,10 +40,7 @@ import forge.game.cost.Cost; import forge.game.cost.CostSacrifice; import forge.game.event.*; import forge.game.event.GameEventCardDamaged.DamageType; -import forge.game.keyword.Keyword; -import forge.game.keyword.KeywordCollection; -import forge.game.keyword.KeywordInterface; -import forge.game.keyword.KeywordsChange; +import forge.game.keyword.*; import forge.game.player.Player; import forge.game.player.PlayerCollection; import forge.game.replacement.ReplaceMoved; @@ -59,6 +56,7 @@ import forge.game.zone.ZoneType; import forge.item.IPaperCard; import forge.item.PaperCard; import forge.trackable.TrackableProperty; +import forge.trackable.Tracker; import forge.util.*; import forge.util.collect.FCollection; import forge.util.collect.FCollectionView; @@ -96,7 +94,6 @@ public class Card extends GameEntity implements Comparable { private SpellAbility castSA = null; private final CardDamageHistory damageHistory = new CardDamageHistory(); - private Map> countersAddedBy = Maps.newTreeMap(); // Hidden keywords won't be displayed on the card private final KeywordCollection hiddenExtrinsicKeyword = new KeywordCollection(); @@ -129,6 +126,8 @@ public class Card extends GameEntity implements Comparable { private final Multimap cantHaveKeywords = MultimapBuilder.hashKeys().enumSetValues(Keyword.class).build(); + private final Map counterTypeTimestamps = Maps.newHashMap(); + private final Map canBlockAdditional = Maps.newTreeMap(); private final Set canBlockAny = Sets.newHashSet(); @@ -220,7 +219,6 @@ public class Card extends GameEntity implements Comparable { private int turnInZone; - private int xManaCostPaid = 0; private Map xManaCostPaidByColor; private int sunburstValue = 0; @@ -237,6 +235,7 @@ public class Card extends GameEntity implements Comparable { private String chosenName = ""; private Integer chosenNumber; private Player chosenPlayer; + private EvenOdd chosenEvenOdd = null; private Direction chosenDirection = null; private String chosenMode = ""; @@ -265,8 +264,6 @@ public class Card extends GameEntity implements Comparable { // breaking when the LKI object is changed to a different card state. private int lkiCMC = -1; - private int countersAdded = 0; - private CardRules cardRules; private final CardView view; @@ -296,7 +293,7 @@ public class Card extends GameEntity implements Comparable { * @param id0 the unique id of the new card. */ public Card(final int id0, final Game game0) { - this(id0, null, true, game0); + this(id0, null, game0); } /** @@ -308,17 +305,14 @@ public class Card extends GameEntity implements Comparable { * @see IPaperCard */ public Card(final int id0, final IPaperCard paperCard0, final Game game0) { - this(id0, paperCard0, true, game0); + this(id0, paperCard0, game0, game0 == null ? null : game0.getTracker()); } - public Card(final int id0, final IPaperCard paperCard0, final boolean allowCache, final Game game0) { + public Card(final int id0, final IPaperCard paperCard0, final Game game0, final Tracker tracker0) { super(id0); game = game0; - if (id0 >= 0 && allowCache && game != null) { - game.addCard(id0, this); - } paperCard = paperCard0; - view = new CardView(id0, game == null ? null : game.getTracker()); + view = new CardView(id0, tracker0); currentState = new CardState(view.getCurrentState(), this); states.put(CardStateName.Original, currentState); view.updateChangedColorWords(this); @@ -477,7 +471,7 @@ public class Card extends GameEntity implements Comparable { currentState.getView().updateColors(this); } if (!changedCardKeywords.isEmpty()) { - currentState.getView().updateKeywords(this, currentState); + updateKeywords(); } if (state == CardStateName.FaceDown) { @@ -543,7 +537,7 @@ public class Card extends GameEntity implements Comparable { // The following methods are used to selectively update certain view components (text, // P/T, card types) in order to avoid card flickering due to aggressive full update public void updateAbilityTextForView() { - view.getCurrentState().updateKeywords(this, getCurrentState()); + updateKeywords(); view.getCurrentState().updateAbilityText(this, getCurrentState()); } @@ -1055,23 +1049,6 @@ public class Card extends GameEntity implements Comparable { } } - public final Object getTriggeringObject(final AbilityKey typeIn) { - Object triggered = null; - if (!currentState.getTriggers().isEmpty()) { - for (final Trigger t : currentState.getTriggers()) { - final SpellAbility sa = t.getTriggeredSA(); - if (sa == null) { - continue; - } - triggered = sa.hasTriggeringObject(typeIn) ? sa.getTriggeringObject(typeIn) : null; - if (triggered != null) { - break; - } - } - } - return triggered; - } - public final int getSunburstValue() { return sunburstValue; } @@ -1087,10 +1064,11 @@ public class Card extends GameEntity implements Comparable { } public final int getXManaCostPaid() { - return xManaCostPaid; - } - public final void setXManaCostPaid(final int n) { - xManaCostPaid = n; + if (getCastSA() != null) { + Integer paid = getCastSA().getXManaCostPaid(); + return paid == null ? 0 : paid; + } + return 0; } public final Map getXManaCostPaidByColor() { @@ -1194,7 +1172,6 @@ public class Card extends GameEntity implements Comparable { @Override public final boolean canReceiveCounters(final CounterType type) { - // CantPutCounter static abilities for (final Card ca : getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { for (final StaticAbility stAb : ca.getStaticAbilities()) { @@ -1203,34 +1180,27 @@ public class Card extends GameEntity implements Comparable { } } } - - if (type == CounterType.DREAM) { - // need to be done extra because it is also a state based action - return !hasKeyword("CARDNAME can't have more than seven dream counters on it.") || getCounters(CounterType.DREAM) <= 6; - } return true; } - public final int getTotalCountersToAdd() { - return countersAdded; - } - - public final void setTotalCountersToAdd(int value) { - countersAdded = value; - } - public final int addCounter(final CounterType counterType, final int n, final Player source, final boolean applyMultiplier, GameEntityCounterTable table) { return addCounter(counterType, n, source, applyMultiplier, true, table); } public final int addCounterFireNoEvents(final CounterType counterType, final int n, final Player source, final boolean applyMultiplier, GameEntityCounterTable table) { return addCounter(counterType, n, source, applyMultiplier, false, table); } + public final int addCounter(final CounterEnumType counterType, final int n, final Player source, final boolean applyMultiplier, GameEntityCounterTable table) { + return addCounter(counterType, n, source, applyMultiplier, true, table); + } + public final int addCounterFireNoEvents(final CounterEnumType counterType, final int n, final Player source, final boolean applyMultiplier, GameEntityCounterTable table) { + return addCounter(counterType, n, source, applyMultiplier, false, table); + } @Override public int addCounter(final CounterType counterType, final int n, final Player source, final boolean applyMultiplier, final boolean fireEvents, GameEntityCounterTable table) { int addAmount = n; - if(addAmount <= 0) { - addAmount = 0; // As per rule 107.1b + if(addAmount <= 0 || !canReceiveCounters(counterType)) { + // As per rule 107.1b return 0; } final Map repParams = AbilityKey.mapFromAffected(this); @@ -1250,19 +1220,9 @@ public class Card extends GameEntity implements Comparable { return 0; } - if (canReceiveCounters(counterType)) { - if (counterType == CounterType.DREAM && hasKeyword("CARDNAME can't have more than seven dream counters on it.")) { - addAmount = Math.min(7 - getCounters(CounterType.DREAM), addAmount); - } - } - else { - addAmount = 0; - } - if (addAmount <= 0) { return 0; } - setTotalCountersToAdd(addAmount); final Integer oldValue = getCounters(counterType); final Integer newValue = addAmount + (oldValue == null ? 0 : oldValue); @@ -1305,42 +1265,42 @@ public class Card extends GameEntity implements Comparable { getController().addCounterToPermThisTurn(counterType, addAmount); view.updateCounters(this); } + if (newValue <= 0) { + removeCounterTimestamp(counterType); + } else { + addCounterTimestamp(counterType); + } if (table != null) { table.put(this, counterType, addAmount); } return addAmount; } - /** - *

- * addCountersAddedBy. - *

- * @param source - the card adding the counters to this card - * @param counterType - the counter type added - * @param counterAmount - the amount of counters added - */ - public final void addCountersAddedBy(final Card source, final CounterType counterType, final int counterAmount) { - final Map counterMap = Maps.newTreeMap(); - counterMap.put(counterType, counterAmount); - countersAddedBy.put(source, counterMap); + public boolean addCounterTimestamp(CounterType counterType) { + return addCounterTimestamp(counterType, true); + } + public boolean addCounterTimestamp(CounterType counterType, boolean updateView) { + if (!counterType.isKeywordCounter()) { + return false; + } + removeCounterTimestamp(counterType); + + long timestamp = game.getNextTimestamp(); + counterTypeTimestamps.put(counterType, timestamp); + addChangedCardKeywords(ImmutableList.of(counterType.toString()), null, false, false, timestamp, updateView); + return true; } - /** - *

- * getCountersAddedBy. - *

- * @param source - the card the counters were added by - * @param counterType - the counter type added - * @return the amount of counters added. - */ - public final int getCountersAddedBy(final Card source, final CounterType counterType) { - int counterAmount = 0; - if (countersAddedBy.containsKey(source)) { - final Map counterMap = countersAddedBy.get(source); - counterAmount = counterMap.containsKey(counterType) ? counterMap.get(counterType) : 0; - countersAddedBy.remove(source); + public boolean removeCounterTimestamp(CounterType counterType) { + return removeCounterTimestamp(counterType, true); + } + + public boolean removeCounterTimestamp(CounterType counterType, boolean updateView) { + Long old = counterTypeTimestamps.remove(counterType); + if (old != null) { + removeChangedCardKeywords(old, updateView); } - return counterAmount; + return old != null; } @Override @@ -1358,6 +1318,10 @@ public class Card extends GameEntity implements Comparable { setCounters(counterName, newValue); view.updateCounters(this); + if (newValue <= 0) { + this.removeCounterTimestamp(counterName); + } + //fire card stats changed event if p/t bonuses or loyalty changed from subtracted counters if (powerBonusBefore != getPowerBonusFromCounters() || toughnessBonusBefore != getToughnessBonusFromCounters() || loyaltyBefore != getCurrentLoyalty()) { getGame().fireEvent(new GameEventCardStatsChanged(this)); @@ -1380,8 +1344,23 @@ public class Card extends GameEntity implements Comparable { @Override public final void setCounters(final Map allCounters) { + boolean changed = false; + for (CounterType ct : counters.keySet()) { + if (removeCounterTimestamp(ct, false)) { + changed = true; + } + } counters = allCounters; view.updateCounters(this); + + for (CounterType ct : counters.keySet()) { + if (addCounterTimestamp(ct, false)) { + changed = true; + } + } + if (changed) { + updateKeywords(); + } } @Override @@ -1389,6 +1368,16 @@ public class Card extends GameEntity implements Comparable { if (counters.isEmpty()) { return; } counters.clear(); view.updateCounters(this); + + boolean changed = false; + for (CounterType ct : Lists.newArrayList(counterTypeTimestamps.keySet())) { + if (removeCounterTimestamp(ct, false)) { + changed = true; + } + } + if (changed) { + updateKeywords(); + } } public final String getSVar(final String var) { @@ -1551,6 +1540,19 @@ public class Card extends GameEntity implements Comparable { view.updateNamedCard(this); } + public boolean hasChosenEvenOdd() { + return chosenEvenOdd != null; + } + + public EvenOdd getChosenEvenOdd() { + return chosenEvenOdd; + } + public void setChosenEvenOdd(EvenOdd chosenEvenOdd0) { + if (chosenEvenOdd == chosenEvenOdd0) { return; } + chosenEvenOdd = chosenEvenOdd0; + view.updateChosenEvenOdd(this); + } + // used for cards like Meddling Mage... public final String getNamedCard() { return getChosenName(); @@ -1627,7 +1629,7 @@ public class Card extends GameEntity implements Comparable { } } if (keyword.startsWith("CantBeCounteredBy") || keyword.startsWith("Panharmonicon") - || keyword.startsWith("Dieharmonicon")) { + || keyword.startsWith("Dieharmonicon") || keyword.startsWith("Shrineharmonicon")) { final String[] p = keyword.split(":"); sbLong.append(p[2]).append("\r\n"); } else if (keyword.startsWith("etbCounter")) { @@ -1640,7 +1642,7 @@ public class Card extends GameEntity implements Comparable { } else { s.append(getName()); s.append(" enters the battlefield with "); - s.append(Lang.nounWithNumeral(p[2], CounterType.valueOf(p[1]).getName() + " counter")); + s.append(Lang.nounWithNumeral(p[2], CounterType.getType(p[1]).getName() + " counter")); s.append(" on it."); } sbLong.append(s).append("\r\n"); @@ -1844,6 +1846,9 @@ public class Card extends GameEntity implements Comparable { String desc = "(As this Saga enters and after your draw step, " + " add a lore counter. Sacrifice after " + Strings.repeat("I", Integer.valueOf(k[1])) + ".)"; sbLong.append(desc); + } else if (inst.getKeyword().equals(Keyword.COMPANION)) { + sbLong.append("Companion - "); + sbLong.append(((Companion)inst).getDescription()); } else { if ((i != 0) && (sb.length() != 0)) { @@ -1934,7 +1939,7 @@ public class Card extends GameEntity implements Comparable { StringBuilder replacementEffects = new StringBuilder(); for (final ReplacementEffect replacementEffect : state.getReplacementEffects()) { if (!replacementEffect.isSecondary()) { - String text = replacementEffect.toString(); + String text = replacementEffect.getDescription(); if (text.contains("enters the battlefield")) { sb.append(text).append("\r\n"); } else { @@ -2023,9 +2028,7 @@ public class Card extends GameEntity implements Comparable { if (sa.isAdventure() && state.getView().getState().equals(CardStateName.Original)) { StringBuilder sbSA = new StringBuilder(); sbSA.append("Adventure — ").append(getState(CardStateName.Adventure).getName()); - if (sa.getPayCosts() != null) { - sbSA.append(" ").append(sa.getPayCosts().toSimpleString()); - } + sbSA.append(" ").append(sa.getPayCosts().toSimpleString()); sbSA.append(": "); sbSA.append(sAbility); sAbility = sbSA.toString(); @@ -2297,7 +2300,7 @@ public class Card extends GameEntity implements Comparable { // Replacement effects for (final ReplacementEffect replacementEffect : state.getReplacementEffects()) { if (!replacementEffect.isSecondary()) { - sb.append(replacementEffect.toString()).append("\r\n"); + sb.append(replacementEffect.getDescription()).append("\r\n"); } } @@ -2463,7 +2466,11 @@ public class Card extends GameEntity implements Comparable { list.clear(); } list.removeAll(ck.getRemovedAbilities()); - list.addAll(ck.getAbilities()); + for (SpellAbility sa : ck.getAbilities()) { + if (mana == null || mana == sa.isManaAbility()) { + list.add(sa); + } + } } // add Facedown abilities from Original state but only if this state is face down @@ -2686,11 +2693,17 @@ public class Card extends GameEntity implements Comparable { public final void addFacedownCommand(final GameCommand c) { facedownCommandList.add(c); } - + public final void runUntapCommands() { + for (final GameCommand c : untapCommandList) { + c.run(); + } + untapCommandList.clear(); + } public final void runUnattachCommands() { for (final GameCommand c : unattachCommandList) { c.run(); } + unattachCommandList.clear(); } public final void runFaceupCommands() { @@ -2875,7 +2888,7 @@ public class Card extends GameEntity implements Comparable { return false; } - return CardLists.count(attachedCards, CardPredicates.Presets.EQUIPMENT) > 0; + return Iterables.any(attachedCards, CardPredicates.Presets.EQUIPMENT); } public final boolean isEquippedBy(Card c) { return this.hasCardAttachment(c); @@ -2897,7 +2910,7 @@ public class Card extends GameEntity implements Comparable { return false; } - return CardLists.count(attachedCards, CardPredicates.Presets.FORTIFICATION) > 0; + return Iterables.any(attachedCards, CardPredicates.Presets.FORTIFICATION); } public final boolean isFortifiedBy(Card c) { // 301.5e + 301.6 @@ -3006,6 +3019,11 @@ public class Card extends GameEntity implements Comparable { // Play the Equip sound getGame().fireEvent(new GameEventCardAttachment(this, oldTarget, entity)); + // Run replacement effects + final Map repParams = AbilityKey.mapFromAffected(this); + repParams.put(AbilityKey.AttachTarget, entity); + getGame().getReplacementHandler().run(ReplacementType.Attached, repParams); + // run trigger final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.AttachSource, this); @@ -3202,7 +3220,7 @@ public class Card extends GameEntity implements Comparable { } public final int getCurrentLoyalty() { - return getCounters(CounterType.LOYALTY); + return getCounters(CounterEnumType.LOYALTY); } // values that are printed on card @@ -3418,6 +3436,7 @@ public class Card extends GameEntity implements Comparable { } else { newPT.put(timestamp, Pair.of(power, toughness)); } + getView().updateLethalDamage(this); currentState.getView().updatePower(this); currentState.getView().updateToughness(this); } @@ -3429,6 +3448,7 @@ public class Card extends GameEntity implements Comparable { removed |= newPTCharacterDefining.remove(timestamp) != null; if (removed) { + getView().updateLethalDamage(this); currentState.getView().updatePower(this); currentState.getView().updateToughness(this); } @@ -3451,9 +3471,9 @@ public class Card extends GameEntity implements Comparable { } public final int getPowerBonusFromCounters() { - return getCounters(CounterType.P1P1) + getCounters(CounterType.P1P2) + getCounters(CounterType.P1P0) - - getCounters(CounterType.M1M1) + 2 * getCounters(CounterType.P2P2) - 2 * getCounters(CounterType.M2M1) - - 2 * getCounters(CounterType.M2M2) - getCounters(CounterType.M1M0) + 2 * getCounters(CounterType.P2P0); + return getCounters(CounterEnumType.P1P1) + getCounters(CounterEnumType.P1P2) + getCounters(CounterEnumType.P1P0) + - getCounters(CounterEnumType.M1M1) + 2 * getCounters(CounterEnumType.P2P2) - 2 * getCounters(CounterEnumType.M2M1) + - 2 * getCounters(CounterEnumType.M2M2) - getCounters(CounterEnumType.M1M0) + 2 * getCounters(CounterEnumType.P2P0); } public final StatBreakdown getNetPowerBreakdown() { @@ -3509,10 +3529,10 @@ public class Card extends GameEntity implements Comparable { } public final int getToughnessBonusFromCounters() { - return getCounters(CounterType.P1P1) + 2 * getCounters(CounterType.P1P2) - getCounters(CounterType.M1M1) - + getCounters(CounterType.P0P1) - 2 * getCounters(CounterType.M0M2) + 2 * getCounters(CounterType.P2P2) - - getCounters(CounterType.M0M1) - getCounters(CounterType.M2M1) - 2 * getCounters(CounterType.M2M2) - + 2 * getCounters(CounterType.P0P2); + return getCounters(CounterEnumType.P1P1) + 2 * getCounters(CounterEnumType.P1P2) - getCounters(CounterEnumType.M1M1) + + getCounters(CounterEnumType.P0P1) - 2 * getCounters(CounterEnumType.M0M2) + 2 * getCounters(CounterEnumType.P2P2) + - getCounters(CounterEnumType.M0M1) - getCounters(CounterEnumType.M2M1) - 2 * getCounters(CounterEnumType.M2M2) + + 2 * getCounters(CounterEnumType.P0P2); } public final StatBreakdown getNetToughnessBreakdown() { @@ -3638,9 +3658,7 @@ public class Card extends GameEntity implements Comparable { // Run triggers getGame().getTriggerHandler().runTrigger(TriggerType.Untaps, AbilityKey.mapFromCard(this), false); - for (final GameCommand var : untapCommandList) { - var.run(); - } + runUntapCommands(); setTapped(false); getGame().fireEvent(new GameEventCardTapped(this, false)); } @@ -3648,9 +3666,16 @@ public class Card extends GameEntity implements Comparable { public final void addChangedCardTraits(Collection spells, Collection removedAbilities, Collection trigger, Collection replacements, Collection statics, boolean removeAll, boolean removeNonMana, boolean removeIntrinsic, long timestamp) { - changedCardTraits.put(timestamp, new CardTraitChanges( - spells, removedAbilities, trigger, replacements, statics, removeAll, removeNonMana, removeIntrinsic - )); + // in case two static abilities has the same timestamp + if (changedCardTraits.containsKey(timestamp)) { + changedCardTraits.get(timestamp).merge( + spells, removedAbilities, trigger, replacements, statics, removeAll, removeNonMana, removeIntrinsic + ); + } else { + changedCardTraits.put(timestamp, new CardTraitChanges( + spells, removedAbilities, trigger, replacements, statics, removeAll, removeNonMana, removeIntrinsic + )); + } // update view updateAbilityTextForView(); } @@ -3719,7 +3744,8 @@ public class Card extends GameEntity implements Comparable { } public final void updateKeywords() { - currentState.getView().updateKeywords(this, currentState); + getCurrentState().getView().updateKeywords(this, getCurrentState()); + getView().updateLethalDamage(this); } public final void addChangedCardKeywords(final List keywords, final List removeKeywords, @@ -3938,7 +3964,9 @@ public class Card extends GameEntity implements Comparable { keywordsGrantedByTextChanges.add(newKw); } } - addChangedCardKeywordsInternal(addKeywords, removeKeywords, false, false, timestamp, true); + if (!addKeywords.isEmpty() || !removeKeywords.isEmpty()) { + addChangedCardKeywordsInternal(addKeywords, removeKeywords, false, false, timestamp, true); + } } private void updateKeywordsOnRemoveChangedText(final KeywordsChange k) { @@ -3999,7 +4027,7 @@ public class Card extends GameEntity implements Comparable { public final KeywordInterface addIntrinsicKeyword(final String s) { KeywordInterface inst = currentState.addIntrinsicKeyword(s, true); if (inst != null) { - currentState.getView().updateKeywords(this, currentState); + updateKeywords(); } return inst; } @@ -4009,19 +4037,19 @@ public class Card extends GameEntity implements Comparable { } public final void addIntrinsicKeywords(final Iterable s, boolean initTraits) { if (currentState.addIntrinsicKeywords(s, initTraits)) { - currentState.getView().updateKeywords(this, currentState); + updateKeywords(); } } public final void removeIntrinsicKeyword(final String s) { if (currentState.removeIntrinsicKeyword(s)) { - currentState.getView().updateKeywords(this, currentState); + updateKeywords(); } } public final void removeIntrinsicKeyword(final KeywordInterface s) { if (currentState.removeIntrinsicKeyword(s)) { - currentState.getView().updateKeywords(this, currentState); + updateKeywords(); } } @@ -4045,14 +4073,14 @@ public class Card extends GameEntity implements Comparable { } if (hiddenExtrinsicKeyword.add(s) != null) { view.updateNonAbilityText(this); - currentState.getView().updateKeywords(this, currentState); + updateKeywords(); } } public final void addHiddenExtrinsicKeyword(KeywordInterface k) { if (hiddenExtrinsicKeyword.insert(k)) { view.updateNonAbilityText(this); - currentState.getView().updateKeywords(this, currentState); + updateKeywords(); } } @@ -4062,7 +4090,7 @@ public class Card extends GameEntity implements Comparable { } if (hiddenExtrinsicKeyword.remove(s)) { view.updateNonAbilityText(this); - currentState.getView().updateKeywords(this, currentState); + updateKeywords(); } } @@ -4138,7 +4166,7 @@ public class Card extends GameEntity implements Comparable { } public final boolean isPermanent() { - return !isImmutable && getType().isPermanent(); + return !isImmutable && (isInZone(ZoneType.Battlefield) || getType().isPermanent()); } public final boolean isSpell() { @@ -4164,6 +4192,7 @@ public class Card extends GameEntity implements Comparable { public final boolean isFortification() { return getType().hasSubtype("Fortification"); } public final boolean isCurse() { return getType().hasSubtype("Curse"); } public final boolean isAura() { return getType().hasSubtype("Aura"); } + public final boolean isShrine() { return getType().hasSubtype("Shrine"); } public final boolean isAttachment() { return isAura() || isEquipment() || isFortification(); } public final boolean isHistoric() {return getType().isLegendary() || isArtifact() || getType().hasSubtype("Saga");} @@ -4205,7 +4234,7 @@ public class Card extends GameEntity implements Comparable { public final boolean hasSuspend() { return hasKeyword(Keyword.SUSPEND) && getLastKnownZone().is(ZoneType.Exile) - && getCounters(CounterType.TIME) >= 1; + && getCounters(CounterEnumType.TIME) >= 1; } public final boolean isPhasedOut() { @@ -4244,6 +4273,11 @@ public class Card extends GameEntity implements Comparable { } private boolean switchPhaseState() { + + if (phasedOut && hasKeyword("CARDNAME can't phase in.")) { + return false; + } + if (!phasedOut && hasKeyword("CARDNAME can't phase out.")) { return false; } @@ -4394,7 +4428,7 @@ public class Card extends GameEntity implements Comparable { if (incR[0].equals("Spell") && !isSpell()) { return testFailed; } - if (incR[0].equals("Permanent") && (isInstant() || isSorcery())) { + if (incR[0].equals("Permanent") && !isPermanent()) { return testFailed; } if (!incR[0].equals("card") && !incR[0].equals("Card") && !incR[0].equals("Spell") @@ -4729,9 +4763,17 @@ public class Card extends GameEntity implements Comparable { return false; } + // this is the amount of damage a creature needs to receive before it dies + public final int getLethal() { + if (hasKeyword("Lethal damage dealt to CARDNAME is determined by its power rather than its toughness.")) { + return getNetPower(); } + else { + return getNetToughness(); } + } + // this is the minimal damage a trampling creature has to assign to a blocker public final int getLethalDamage() { - return getNetToughness() - getDamage() - getTotalAssignedDamage(); + return getLethal() - getDamage() - getTotalAssignedDamage(); } public final int getDamage() { @@ -5074,7 +5116,7 @@ public class Card extends GameEntity implements Comparable { GameEventCardDamaged.DamageType damageType = DamageType.Normal; if (isPlaneswalker()) { - subtractCounter(CounterType.LOYALTY, damageIn); + subtractCounter(CounterType.get(CounterEnumType.LOYALTY), damageIn); } if (isCreature()) { final Game game = source.getGame(); @@ -5084,7 +5126,7 @@ public class Card extends GameEntity implements Comparable { if (isInPlay()) { if (wither) { - addCounter(CounterType.M1M1, damageIn, source.getController(), true, counterTable); + addCounter(CounterType.get(CounterEnumType.M1M1), damageIn, source.getController(), true, counterTable); damageType = DamageType.M1M1Counters; } else { @@ -6271,12 +6313,21 @@ public class Card extends GameEntity implements Comparable { getGame().getTriggerHandler().clearSuppression(TriggerType.ChangesZone); } + public void forceTurnFaceUp() { + getGame().getTriggerHandler().suppressMode(TriggerType.TurnFaceUp); + turnFaceUp(false, false); + getGame().getTriggerHandler().clearSuppression(TriggerType.TurnFaceUp); + } + public final void addGoad(Long timestamp, final Player p) { goad.put(timestamp, p); + updateAbilityTextForView(); } public final void removeGoad(Long timestamp) { - goad.remove(timestamp); + if (goad.remove(timestamp) != null) { + updateAbilityTextForView(); + } } public final boolean isGoaded() { @@ -6353,6 +6404,9 @@ public class Card extends GameEntity implements Comparable { removeSVar("PayX"); // Temporary AI X announcement variable removeSVar("IsCastFromPlayEffect"); // Temporary SVar indicating that the spell is cast indirectly via AF Play setSunburstValue(0); // Sunburst + setXManaCostPaidByColor(null); + setKickerMagnitude(0); + setPseudoMultiKickerMagnitude(0); } public final int getFinalChapterNr() { diff --git a/forge-game/src/main/java/forge/game/card/CardDamageMap.java b/forge-game/src/main/java/forge/game/card/CardDamageMap.java index b5cf899f352..a3359c43a83 100644 --- a/forge-game/src/main/java/forge/game/card/CardDamageMap.java +++ b/forge-game/src/main/java/forge/game/card/CardDamageMap.java @@ -9,6 +9,7 @@ import java.util.Set; import com.google.common.collect.ForwardingTable; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.collect.Table; @@ -59,8 +60,7 @@ public class CardDamageMap extends ForwardingTable { if (sum > 0) { final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.DamageSource, sourceLKI); - runParams.put(AbilityKey.DamageTargets, Sets.newHashSet(e.getValue().keySet())); - runParams.put(AbilityKey.DamageAmount, sum); + runParams.put(AbilityKey.DamageMap, Maps.newHashMap(e.getValue())); runParams.put(AbilityKey.IsCombatDamage, isCombat); game.getTriggerHandler().runTrigger(TriggerType.DamageDealtOnce, runParams, false); @@ -80,8 +80,7 @@ public class CardDamageMap extends ForwardingTable { final GameEntity ge = e.getKey(); final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.DamageTarget, ge); - runParams.put(AbilityKey.DamageSources, Sets.newHashSet(e.getValue().keySet())); - runParams.put(AbilityKey.DamageAmount, sum); + runParams.put(AbilityKey.DamageMap, Maps.newHashMap(e.getValue())); runParams.put(AbilityKey.IsCombatDamage, isCombat); game.getTriggerHandler().runTrigger(TriggerType.DamageDoneOnce, runParams, false); 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 5fa4868727e..dd9f68bd571 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -20,7 +20,6 @@ package forge.game.card; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; - import forge.ImageKeys; import forge.StaticData; import forge.card.*; @@ -28,7 +27,6 @@ import forge.card.mana.ManaCost; import forge.game.CardTraitBase; import forge.game.Game; import forge.game.ability.AbilityFactory; -import forge.game.ability.AbilityUtils; import forge.game.cost.Cost; import forge.game.player.Player; import forge.game.replacement.ReplacementEffect; @@ -119,39 +117,6 @@ public class CardFactory { } - /** - *

- * copyCardWithChangedStats - *

- * - * This method copies the card together with certain temporarily changed stats of the card - * (namely, changed color, changed types, changed keywords). - * - * copyCardWithChangedStats must NOT be used for ordinary card copy operations because - * according to MTG rules the changed text (including keywords, types) is not copied over - * to cards cloned by another card. However, this method is useful, for example, for certain - * triggers that demand the latest information about the changes to the card which is lost - * when the card changes its zone after GameAction::changeZone is called. - * - * @param in - * a {@link forge.game.card.Card} object. - * @param assignNewId - * a boolean - * @return a {@link forge.game.card.Card} object. - */ - public static final Card copyCardWithChangedStats(final Card in, boolean assignNewId) { - Card out = copyCard(in, assignNewId); - - // Copy changed color, type, keyword arrays (useful for some triggers that require - // information about the latest state of the card as it left the battlefield) - out.setChangedCardColors(in.getChangedCardColors()); - out.setChangedCardKeywords(in.getChangedCardKeywords()); - out.setChangedCardTypes(in.getChangedCardTypesMap()); - out.setChangedCardNames(in.getChangedCardNames()); - - return out; - } - /** *

* copySpellHost. @@ -185,18 +150,12 @@ public class CardFactory { c.setCopiedSpell(true); if (bCopyDetails) { - c.setXManaCostPaid(original.getXManaCostPaid()); c.setXManaCostPaidByColor(original.getXManaCostPaidByColor()); c.setKickerMagnitude(original.getKickerMagnitude()); // Rule 706.10 : Madness is copied if (original.isInZone(ZoneType.Stack)) { c.setMadness(original.isMadness()); - - final SpellAbilityStackInstance si = controller.getGame().getStack().getInstanceFromSpellAbility(sa); - if (si != null) { - c.setXManaCostPaid(si.getXManaPaid()); - } } for (OptionalCost cost : original.getOptionalCostsPaid()) { @@ -234,8 +193,8 @@ public class CardFactory { } final SpellAbility copySA; - if (sa.isTrigger()) { - copySA = getCopiedTriggeredAbility(sa); + if (sa.isTrigger() && sa.isWrapper()) { + copySA = getCopiedTriggeredAbility((WrappedAbility)sa, c); } else { copySA = sa.copy(c, false); } @@ -254,10 +213,6 @@ public class CardFactory { if (!copySA.isTrigger()) { copySA.setPayCosts(new Cost("", sa.isAbility())); } - if (sa.getTargetRestrictions() != null) { - TargetRestrictions target = new TargetRestrictions(sa.getTargetRestrictions()); - copySA.setTargetRestrictions(target); - } copySA.setActivatingPlayer(controller); if (bCopyDetails) { @@ -374,7 +329,7 @@ public class CardFactory { triggerSB.append("that planar deck and turn it face up"); StringBuilder saSB = new StringBuilder(); - saSB.append("AB$ RollPlanarDice | Cost$ X | SorcerySpeed$ True | AnyPlayer$ True | ActivationZone$ Command | "); + saSB.append("AB$ RollPlanarDice | Cost$ X | SorcerySpeed$ True | Activator$ Player | ActivationZone$ Command | "); saSB.append("SpellDescription$ Roll the planar dice. X is equal to the amount of times the planar die has been rolled this turn."); card.setSVar("RolledWalk", "DB$ Planeswalk | Cost$ 0"); @@ -478,23 +433,6 @@ public class CardFactory { CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities()); } - /** - * Create a copy of a card, including its copiable characteristics (but not - * abilities). - * @param from - * @param newOwner - * @return - */ - public static Card copyCopiableCharacteristics(final Card from, final Player newOwner) { - int id = newOwner == null ? 0 : newOwner.getGame().nextCardId(); - final Card c = new Card(id, from.getPaperCard(), from.getGame()); - c.setOwner(newOwner); - c.setSetCode(from.getSetCode()); - - copyCopiableCharacteristics(from, c); - return c; - } - /** * Copy the copiable characteristics of one card to another, taking the * states of both cards into account. @@ -644,50 +582,12 @@ public class CardFactory { * * return a wrapped ability */ - public static SpellAbility getCopiedTriggeredAbility(final SpellAbility sa) { + public static SpellAbility getCopiedTriggeredAbility(final WrappedAbility sa, final Card newHost) { if (!sa.isTrigger()) { return null; } - // Find trigger - Trigger t = null; - if (sa.isWrapper()) { - // copy trigger? - t = sa.getTrigger(); - } else { // some keyword ability, e.g. Exalted, Annihilator - return sa.copy(); - } - // set up copied wrapped ability - SpellAbility trig = t.getOverridingAbility(); - if (trig == null) { - trig = AbilityFactory.getAbility(sa.getHostCard().getSVar(t.getParam("Execute")), sa.getHostCard()); - } - trig.setHostCard(sa.getHostCard()); - trig.setTrigger(true); - trig.setSourceTrigger(t.getId()); - sa.setTriggeringObjects(sa.getTriggeringObjects()); - trig.setTriggerRemembered(t.getTriggerRemembered()); - if (t.getStoredTriggeredObjects() != null) { - trig.setTriggeringObjects(t.getStoredTriggeredObjects()); - } - trig.setActivatingPlayer(sa.getActivatingPlayer()); - if (t.hasParam("TriggerController")) { - Player p = AbilityUtils.getDefinedPlayers(t.getHostCard(), t.getParam("TriggerController"), trig).get(0); - trig.setActivatingPlayer(p); - } - - if (t.hasParam("RememberController")) { - sa.getHostCard().addRemembered(sa.getActivatingPlayer()); - } - - trig.setStackDescription(trig.toString()); - - WrappedAbility wrapperAbility = new WrappedAbility(t, trig, ((WrappedAbility) sa).getDecider()); - wrapperAbility.setTrigger(true); - wrapperAbility.setMandatory(sa.isMandatory()); - wrapperAbility.setDescription(wrapperAbility.getStackDescription()); - t.setTriggeredSA(wrapperAbility); - return wrapperAbility; + return new WrappedAbility(sa.getTrigger(), sa.getWrappedAbility().copy(newHost, false), sa.getDecider()); } public static CardCloneStates getCloneStates(final Card in, final Card out, final CardTraitBase sa) { @@ -875,6 +775,13 @@ public class CardFactory { } } + for (final Trigger trigger : state.getTriggers()) { + final SpellAbility newSa = trigger.getOverridingAbility(); + if (newSa != null && newSa.getOriginalHost() == null) { + newSa.setOriginalHost(in); + } + } + if (sa.hasParam("GainTextOf") && originalState != null) { state.setSetCode(originalState.getSetCode()); state.setRarity(originalState.getRarity()); diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index e60f3e16bd1..f2c7ea82d86 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -20,7 +20,6 @@ package forge.game.card; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; -import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -35,7 +34,6 @@ import forge.game.GameEntity; import forge.game.GameEntityCounterTable; import forge.game.GameLogEntryType; import forge.game.ability.AbilityFactory; -import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.CardPredicates.Presets; @@ -640,6 +638,10 @@ public class CardFactoryUtil { return doXMath(oppDmg, m, source); } + if (value.contains("NonCombatDamageDealtThisTurn")) { + return doXMath(player.getAssignedDamage() - player.getAssignedCombatDamage(), m, source); + } + if (value.equals("OpponentsAttackedThisTurn")) { return doXMath(player.getAttackedOpponentsThisTurn().size(), m, source); } @@ -721,10 +723,13 @@ public class CardFactoryUtil { return c.getImprintedCards().get(0).getCMC(); } - if (l[0].startsWith("GreatestPower_")) { - final String restriction = l[0].substring(14); - final String[] rest = restriction.split(","); - CardCollection list = CardLists.getValidCards(cc.getGame().getCardsIn(ZoneType.Battlefield), rest, cc, c, null); + if (l[0].startsWith("GreatestPower")) { + final String[] lparts = l[0].split("_", 2); + final String[] rest = lparts[1].split(","); + final CardCollectionView cardsInZones = lparts[0].length() > 13 + ? game.getCardsIn(ZoneType.listValueOf(lparts[0].substring(13))) + : game.getCardsIn(ZoneType.Battlefield); + CardCollection list = CardLists.getValidCards(cardsInZones, rest, cc, c, null); int highest = 0; for (final Card crd : list) { if (crd.getNetPower() > highest) { @@ -838,26 +843,11 @@ public class CardFactoryUtil { // Count$CountersAddedToPermYouCtrl if (l[0].startsWith("CountersAddedToPermYouCtrl")) { final String[] components = l[0].split(" ", 2); - final CounterType counterType = CounterType.valueOf(components[1]); + final CounterType counterType = CounterType.getType(components[1]); int n = cc.getCounterToPermThisTurn(counterType); return doXMath(n, m, c); } - // Count$CountersAdded - if (l[0].startsWith("CountersAdded")) { - final String[] components = l[0].split(" ", 3); - final CounterType counterType = CounterType.valueOf(components[1]); - String restrictions = components[2]; - final String[] rest = restrictions.split(","); - CardCollection candidates = CardLists.getValidCards(game.getCardsInGame(), rest, cc, c, null); - - int added = 0; - for (final Card counterSource : candidates) { - added += c.getCountersAddedBy(counterSource, counterType); - } - return doXMath(added, m, c); - } - if (l[0].startsWith("CommanderCastFromCommandZone")) { // only used by Opal Palace, and it does add the trigger to the card return doXMath(cc.getCommanderCast(c), m, c); @@ -910,6 +900,9 @@ public class CardFactoryUtil { return doXMath(c.getXManaCostPaidCount(colors.toString()), m, c); } + if (sq[0].equals("YouCycledThisTurn")) { + return doXMath(cc.getCycledThisTurn(), m, c); + } if (sq[0].equals("YouDrewThisTurn")) { return doXMath(cc.getNumDrawnThisTurn(), m, c); @@ -952,11 +945,6 @@ public class CardFactoryUtil { return doXMath(c.getRegeneratedThisTurn(), m, c); } - // TriggeringObjects - if (sq[0].startsWith("Triggered")) { - return doXMath(xCount((Card) c.getTriggeringObject(AbilityKey.Card), sq[0].substring(9)), m, c); - } - if (sq[0].contains("YourStartingLife")) { return doXMath(cc.getStartingLife(), m, c); } @@ -1248,7 +1236,8 @@ public class CardFactoryUtil { return doXMath(CardUtil.getColors(c).countColors(), m, c); } if (sq[0].contains("ChosenNumber")) { - return doXMath(c.getChosenNumber(), m, c); + Integer i = c.getChosenNumber(); + return doXMath(i == null ? 0 : i, m, c); } if (sq[0].contains("CardCounters")) { // CardCounters.ALL to be used for Kinsbaile Borderguard and anything that cares about all counters @@ -2153,7 +2142,7 @@ public class CardFactoryUtil { String[] splitkw = parse.split(":"); String desc = "CARDNAME enters the battlefield with "; - desc += Lang.nounWithNumeral(splitkw[2], CounterType.valueOf(splitkw[1]).getName() + " counter"); + desc += Lang.nounWithNumeral(splitkw[2], CounterType.getType(splitkw[1]).getName() + " counter"); desc += " on it."; String extraparams = ""; @@ -2688,7 +2677,7 @@ public class CardFactoryUtil { + "| Secondary$ True | TriggerZones$ Battlefield | TriggerDescription$ Ingest (" + inst.getReminderText() + ")"; - final String abStr = "DB$ Mill | NumCards$ 1 | Destination$ Exile | Defined$ TriggeredTarget"; + final String abStr = "DB$ Dig | DigNum$ 1 | ChangeNum$ All | DestinationZone$ Exile | Defined$ TriggeredTarget"; final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic); @@ -3010,24 +2999,43 @@ public class CardFactoryUtil { inst.addTrigger(parsedTrigger); } else if (keyword.startsWith("Saga")) { - // Saga there doesn't need Max value anymore? final String[] k = keyword.split(":"); - final String[] abs = k[2].split(","); + final List abs = Arrays.asList(k[2].split(",")); + if (abs.size() != Integer.valueOf(k[1])) { + throw new RuntimeException("Saga max differ from Ability amount"); + } - int i = 1; - for (String ab : abs) { - SpellAbility sa = AbilityFactory.getAbility(card, ab); - sa.setChapter(i); + int idx = 0; + int skipId = 0; + for(String ab : abs) { + idx += 1; + if (idx <= skipId) { + continue; + } - // TODO better logic for Roman numbers - // In the Description try to merge Chapter trigger with the Same Effect - String trigStr = "Mode$ CounterAdded | ValidCard$ Card.Self | TriggerZones$ Battlefield" - + "| CounterType$ LORE | CounterAmount$ EQ" + i - + "| TriggerDescription$ " + Strings.repeat("I", i) + " - " + sa.getDescription(); - final Trigger t = TriggerHandler.parseTrigger(trigStr, card, intrinsic); - t.setOverridingAbility(sa); - inst.addTrigger(t); - ++i; + skipId = idx + abs.subList(idx - 1, abs.size()).lastIndexOf(ab); + StringBuilder desc = new StringBuilder(); + for (int i = idx; i <= skipId; i++) { + if (i != idx) { + desc.append(", "); + } + desc.append(TextUtil.toRoman(i)); + } + + for (int i = idx; i <= skipId; i++) { + SpellAbility sa = AbilityFactory.getAbility(card, ab); + sa.setChapter(i); + + StringBuilder trigStr = new StringBuilder("Mode$ CounterAdded | ValidCard$ Card.Self | TriggerZones$ Battlefield"); + trigStr.append("| CounterType$ LORE | CounterAmount$ EQ").append(i); + if (i != idx) { + trigStr.append(" | Secondary$ True"); + } + trigStr.append("| TriggerDescription$ ").append(desc).append(" — ").append(sa.getDescription()); + final Trigger t = TriggerHandler.parseTrigger(trigStr.toString(), card, intrinsic); + t.setOverridingAbility(sa); + inst.addTrigger(t); + } } } else if (keyword.equals("Soulbond")) { // Setup ETB trigger for card with Soulbond keyword @@ -3575,7 +3583,7 @@ public class CardFactoryUtil { } else if (keyword.equals("Sunburst")) { // Rule 702.43a If this object is entering the battlefield as a creature, // ignoring any type-changing effects that would affect it - CounterType t = card.isCreature() ? CounterType.P1P1 : CounterType.CHARGE; + CounterType t = CounterType.get(card.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE); StringBuilder sb = new StringBuilder("etbCounter:"); sb.append(t).append(":Sunburst:no Condition:"); @@ -4286,7 +4294,7 @@ public class CardFactoryUtil { int counters = AbilityUtils.calculateAmount(c, k[1], this); GameEntityCounterTable table = new GameEntityCounterTable(); - c.addCounter(CounterType.TIME, counters, getActivatingPlayer(), true, table); + c.addCounter(CounterEnumType.TIME, counters, getActivatingPlayer(), true, table); table.triggerCountersPutAll(game); String sb = TextUtil.concatWithSpace(getActivatingPlayer().toString(),"has suspended", c.getName(), "with", String.valueOf(counters),"time counters on it."); diff --git a/forge-game/src/main/java/forge/game/card/CardPredicates.java b/forge-game/src/main/java/forge/game/card/CardPredicates.java index 4f4d00b2010..6104f053fd2 100644 --- a/forge-game/src/main/java/forge/game/card/CardPredicates.java +++ b/forge-game/src/main/java/forge/game/card/CardPredicates.java @@ -299,6 +299,24 @@ public final class CardPredicates { }; } + public static final Predicate evenCMC() { + return new Predicate() { + @Override + public boolean apply(final Card c) { + return c.getCMC() % 2 == 0; + } + }; + } + + public static final Predicate oddCMC() { + return new Predicate() { + @Override + public boolean apply(final Card c) { + return c.getCMC() % 2 == 1; + } + }; + } + public static final Predicate hasCounters() { return new Predicate() { @Override @@ -311,6 +329,9 @@ public final class CardPredicates { public static final Predicate hasCounter(final CounterType type) { return hasCounter(type, 1); } + public static final Predicate hasCounter(final CounterEnumType type) { + return hasCounter(type, 1); + } public static final Predicate hasCounter(final CounterType type, final int n) { return new Predicate() { @@ -320,6 +341,9 @@ public final class CardPredicates { } }; } + public static final Predicate hasCounter(final CounterEnumType type, final int n) { + return hasCounter(CounterType.get(type), n); + } public static final Predicate hasLessCounter(final CounterType type, final int n) { return new Predicate() { @@ -330,6 +354,9 @@ public final class CardPredicates { } }; } + public static final Predicate hasLessCounter(final CounterEnumType type, final int n) { + return hasLessCounter(CounterType.get(type), n); + } public static Predicate canReceiveCounters(final CounterType counter) { return new Predicate() { @@ -339,6 +366,9 @@ public final class CardPredicates { } }; } + public static Predicate canReceiveCounters(final CounterEnumType counter) { + return canReceiveCounters(CounterType.get(counter)); + } public static final Predicate hasGreaterPowerThan(final int minPower) { return new Predicate() { @@ -358,6 +388,9 @@ public final class CardPredicates { } }; } + public static final Comparator compareByCounterType(final CounterEnumType type) { + return compareByCounterType(CounterType.get(type)); + } public static final Predicate hasSVar(final String name) { return new Predicate() { diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index 53e52c0a69c..04000e8f769 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -4,8 +4,10 @@ import com.google.common.collect.Iterables; import forge.card.ColorSet; import forge.card.MagicColor; import forge.game.Direction; +import forge.game.EvenOdd; import forge.game.Game; import forge.game.GameEntity; +import forge.game.GameObject; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.card.CardPredicates.Presets; @@ -15,7 +17,6 @@ import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.spellability.OptionalCost; import forge.game.spellability.SpellAbility; -import forge.game.trigger.Trigger; import forge.game.zone.Zone; import forge.game.zone.ZoneType; import forge.util.Expressions; @@ -72,7 +73,7 @@ public class CardProperty { } } } else if (property.equals("Permanent")) { - if (card.isInstant() || card.isSorcery()) { + if (!card.isPermanent()) { return false; } } else if (property.equals("Historic")) { @@ -392,30 +393,20 @@ public class CardProperty { } } else if (property.startsWith("AttachedTo")) { final String restriction = property.split("AttachedTo ")[1]; - if (restriction.equals("Targeted")) { - if (!source.getCurrentState().getTriggers().isEmpty()) { - for (final Trigger t : source.getCurrentState().getTriggers()) { - final SpellAbility sa = t.getTriggeredSA(); - final CardCollectionView cards = AbilityUtils.getDefinedCards(source, "Targeted", sa); - for (final Card c : cards) { - if (card.getEquipping() != c && !c.equals(card.getEntityAttachedTo())) { - return false; - } - } + + if (card.getEntityAttachedTo() == null) { + return false; + } + + if (!card.getEntityAttachedTo().isValid(restriction, sourceController, source, spellAbility)) { + boolean found = false; + for (final GameObject o : AbilityUtils.getDefinedObjects(source, restriction, spellAbility)) { + if (o.equals(card.getEntityAttachedTo())) { + found = true; + break; } - } else { - for (final SpellAbility sa : source.getCurrentState().getNonManaAbilities()) { - final CardCollectionView cards = AbilityUtils.getDefinedCards(source, "Targeted", sa); - for (final Card c : cards) { - if (card.getEquipping() == c || c.equals(card.getEntityAttachedTo())) { // handle multiple targets - return true; - } - } - } - return false; } - } else { - if ((card.getEntityAttachedTo() == null || !card.getEntityAttachedTo().isValid(restriction, sourceController, source, spellAbility))) { + if (!found) { return false; } } @@ -856,15 +847,6 @@ public class CardProperty { } } return false; - case "TriggeredCard": - final Object triggeringObject = source.getTriggeringObject(AbilityKey.fromString(restriction.substring("Triggered".length()))); - if (!(triggeringObject instanceof Card)) { - return false; - } - if (card.sharesCardTypeWith((Card) triggeringObject)) { - return true; - } - return false; case "EachTopLibrary": final CardCollection cards = new CardCollection(); for (Player p : game.getPlayers()) { @@ -877,6 +859,16 @@ public class CardProperty { } } return false; + default: + final CardCollection cards1 = AbilityUtils.getDefinedCards(card, restriction, spellAbility); + if (cards1.isEmpty()) { + return false; + } + for (Card c : cards1) { + if (!card.sharesCardTypeWith(c)) { + return false; + } + } } } } else if (property.equals("sharesPermanentTypeWith")) { @@ -1381,6 +1373,27 @@ public class CardProperty { if (card.getNetPower() >= card.getNetToughness()) { return false; } + } else if (property.equals("cmcEven")) { + if (card.getCMC() % 2 != 0) { + return false; + } + } else if (property.equals("cmcOdd")) { + if (card.getCMC() % 2 != 1) { + return false; + } + } else if (property.equals("cmcChosenEvenOdd")) { + if (!source.hasChosenEvenOdd()) { + return false; + } + if ((card.getCMC() % 2 == 0) != (source.getChosenEvenOdd() == EvenOdd.Even)) { + return false; + } + } else if (property.equals("cmcNotChosenEvenOdd")) { + if (source.hasChosenEvenOdd()) { + if ((card.getCMC() % 2 == 0) == (source.getChosenEvenOdd() == EvenOdd.Even)) { + return false; + } + } } else if (property.startsWith("power") || property.startsWith("toughness") || property.startsWith("cmc") || property.startsWith("totalPT")) { int x; @@ -1400,11 +1413,7 @@ public class CardProperty { rhs = property.substring(10); y = card.getNetPower() + card.getNetToughness(); } - try { - x = Integer.parseInt(rhs); - } catch (final NumberFormatException e) { - x = AbilityUtils.calculateAmount(source, rhs, spellAbility); - } + x = AbilityUtils.calculateAmount(source, rhs, spellAbility); if (!Expressions.compare(y, property, x)) { return false; @@ -1430,16 +1439,11 @@ public class CardProperty { // TODO get a working regex out of this pattern so the amount of // digits doesn't matter - int number; final String[] splitProperty = property.split("_"); final String strNum = splitProperty[1].substring(2); final String comparator = splitProperty[1].substring(0, 2); String counterType; - try { - number = Integer.parseInt(strNum); - } catch (final NumberFormatException e) { - number = CardFactoryUtil.xCount(source, source.getSVar(strNum)); - } + int number = AbilityUtils.calculateAmount(source, strNum, spellAbility); counterType = splitProperty[2]; final int actualnumber = card.getCounters(CounterType.getType(counterType)); @@ -1454,12 +1458,18 @@ public class CardProperty { if (property.equals("attacking")) return combat.isAttacking(card); if (property.equals("attackingLKI")) return combat.isLKIAttacking(card); if (property.equals("attackingYou")) return combat.isAttacking(card, sourceController); - if (property.equals("attackingYouOrYourPW")) { + if (property.equals("attackingYouOrYourPW")) { Player defender = combat.getDefenderPlayerByAttacker(card); if (!sourceController.equals(defender)) { return false; } } + if (property.equals("attackingOpponent")) { + Player defender = combat.getDefenderPlayerByAttacker(card); + if (!sourceController.isOpponentOf(defender)) { + return false; + } + } } else if (property.startsWith("notattacking")) { return null == combat || !combat.isAttacking(card); } else if (property.equals("attackedThisCombat")) { @@ -1487,13 +1497,12 @@ public class CardProperty { if (combat.isBlocking(card, c)) return true; return false; - } - if (what.startsWith("Remembered")) { - for (final Object o : source.getRemembered()) { - if (o instanceof Card && combat.isBlocking(card, (Card) o)) { + } else { + for(Card c : AbilityUtils.getDefinedCards(source, what, spellAbility)) { + if (combat.isBlocking(card, c)) { return true; } - } + }; return false; } } else if (property.startsWith("sharesBlockingAssignmentWith")) { @@ -1518,27 +1527,38 @@ public class CardProperty { } else if (property.startsWith("blockedByThisTurn")) { return !card.getBlockedByThisTurn().isEmpty(); } else if (property.startsWith("blockedValidThisTurn ")) { - if (card.getBlockedThisTurn() == null) { + CardCollectionView blocked = card.getBlockedThisTurn(); + if (blocked == null) { return false; } - String valid = property.split(" ")[1]; - for(Card c : card.getBlockedThisTurn()) { + for(Card c : blocked) { if (c.isValid(valid, card.getController(), source, spellAbility)) { return true; } } + for(Card c : AbilityUtils.getDefinedCards(source, valid, spellAbility)) { + if (blocked.contains(c)) { + return true; + } + }; return false; } else if (property.startsWith("blockedByValidThisTurn ")) { - if (card.getBlockedByThisTurn() == null) { + CardCollectionView blocked = card.getBlockedByThisTurn(); + if (blocked == null) { return false; } String valid = property.split(" ")[1]; - for(Card c : card.getBlockedByThisTurn()) { + for(Card c : blocked) { if (c.isValid(valid, card.getController(), source, spellAbility)) { return true; } } + for(Card c : AbilityUtils.getDefinedCards(source, valid, spellAbility)) { + if (blocked.contains(c)) { + return true; + } + }; return false; } else if (property.startsWith("blockedBySourceThisTurn")) { return source.getBlockedByThisTurn().contains(card); @@ -1779,4 +1799,4 @@ public class CardProperty { return true; } -} \ No newline at end of file +} diff --git a/forge-game/src/main/java/forge/game/card/CardTraitChanges.java b/forge-game/src/main/java/forge/game/card/CardTraitChanges.java index cb656beaaff..48074e86f17 100644 --- a/forge-game/src/main/java/forge/game/card/CardTraitChanges.java +++ b/forge-game/src/main/java/forge/game/card/CardTraitChanges.java @@ -19,13 +19,19 @@ public class CardTraitChanges implements Cloneable { private List removedAbilities = Lists.newArrayList(); - private boolean removeAll; - private boolean removeNonMana; - private boolean removeIntrinsic; + private boolean removeAll = false; + private boolean removeNonMana = false; + private boolean removeIntrinsic = false; public CardTraitChanges(Collection spells, Collection removedAbilities, Collection trigger, Collection res, Collection st, boolean removeAll, boolean removeNonMana, boolean removeIntrinsic) { + merge(spells, removedAbilities, trigger, res, st, removeAll, removeNonMana, removeIntrinsic); + } + + public void merge(Collection spells, Collection removedAbilities, + Collection trigger, Collection res, Collection st, + boolean removeAll, boolean removeNonMana, boolean removeIntrinsic) { if (spells != null) { this.abilities.addAll(spells); } @@ -41,10 +47,10 @@ public class CardTraitChanges implements Cloneable { if (st != null) { this.staticAbilities.addAll(st); } - - this.removeAll = removeAll; - this.removeNonMana = removeNonMana; - this.removeIntrinsic = removeIntrinsic; + + this.removeAll |= removeAll; + this.removeNonMana |= removeNonMana; + this.removeIntrinsic |= removeIntrinsic; } /** 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 d3173a2ad1b..9646a727f51 100644 --- a/forge-game/src/main/java/forge/game/card/CardUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardUtil.java @@ -206,7 +206,7 @@ public final class CardUtil { .build() ); - final Card newCopy = new Card(in.getId(), in.getPaperCard(), false, in.getGame()); + final Card newCopy = new Card(in.getId(), in.getPaperCard(), in.getGame(), null); newCopy.setSetCode(in.getSetCode()); newCopy.setOwner(in.getOwner()); newCopy.setController(in.getController(), 0); @@ -237,12 +237,14 @@ public final class CardUtil { newCopy.setType(new CardType(in.getType())); newCopy.setToken(in.isToken()); + newCopy.setCopiedSpell(in.isCopiedSpell()); + newCopy.setImmutable(in.isImmutable()); // lock in the current P/T without bonus from counters newCopy.setBasePower(in.getCurrentPower() + in.getTempPowerBoost()); newCopy.setBaseToughness(in.getCurrentToughness() + in.getTempToughnessBoost()); - newCopy.setCounters(Maps.newEnumMap(in.getCounters())); + newCopy.setCounters(Maps.newHashMap(in.getCounters())); newCopy.setColor(in.determineColor().getColor()); newCopy.setReceivedDamageFromThisTurn(in.getReceivedDamageFromThisTurn()); @@ -293,7 +295,12 @@ public final class CardUtil { newCopy.addOptionalCostPaid(ocost); } - newCopy.setCastSA(in.getCastSA()); + if (in.getCastSA() != null) { + SpellAbility castSA = in.getCastSA().copy(newCopy, true); + castSA.setLastStateBattlefield(CardCollection.EMPTY); + castSA.setLastStateGraveyard(CardCollection.EMPTY); + newCopy.setCastSA(castSA); + } newCopy.setCastFrom(in.getCastFrom()); newCopy.setExiledWith(in.getExiledWith()); diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index 9e89b7ffa07..9a1939bd67d 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -8,6 +8,7 @@ import forge.ImageKeys; import forge.card.*; import forge.card.mana.ManaCost; import forge.game.Direction; +import forge.game.EvenOdd; import forge.game.GameEntityView; import forge.game.GameType; import forge.game.combat.Combat; @@ -242,6 +243,7 @@ public class CardView extends GameEntityView { } void updateCounters(Card c) { set(TrackableProperty.Counters, c.getCounters()); + updateLethalDamage(c); CardStateView state = getCurrentState(); state.updatePower(c); state.updateToughness(c); @@ -253,6 +255,7 @@ public class CardView extends GameEntityView { } void updateDamage(Card c) { set(TrackableProperty.Damage, c.getDamage()); + updateLethalDamage(c); } public int getAssignedDamage() { @@ -260,10 +263,14 @@ public class CardView extends GameEntityView { } void updateAssignedDamage(Card c) { set(TrackableProperty.AssignedDamage, c.getTotalAssignedDamage()); + updateLethalDamage(c); } public int getLethalDamage() { - return getCurrentState().getToughness() - getDamage() - getAssignedDamage(); + return get(TrackableProperty.LethalDamage); + } + void updateLethalDamage(Card c) { + set(TrackableProperty.LethalDamage, c.getLethalDamage()); } public int getShieldCount() { @@ -304,6 +311,12 @@ public class CardView extends GameEntityView { void updateChosenDirection(Card c) { set(TrackableProperty.ChosenDirection, c.getChosenDirection()); } + public EvenOdd getChosenEvenOdd() { + return get(TrackableProperty.ChosenEvenOdd); + } + void updateChosenEvenOdd(Card c) { + set(TrackableProperty.ChosenEvenOdd, c.getChosenEvenOdd()); + } public String getChosenMode() { return get(TrackableProperty.ChosenMode); @@ -627,6 +640,13 @@ public class CardView extends GameEntityView { sb.append("]\r\n"); } + EvenOdd chosenEvenOdd = getChosenEvenOdd(); + if (chosenEvenOdd != null) { + sb.append("\r\n[Chosen value: "); + sb.append(chosenEvenOdd); + sb.append("]\r\n"); + } + CardView pairedWith = getPairedWith(); if (pairedWith != null) { sb.append("\r\n \r\nPaired With: ").append(pairedWith); @@ -687,6 +707,7 @@ public class CardView extends GameEntityView { } void updateState(Card c) { updateName(c); + updateDamage(c); boolean isSplitCard = c.isSplitCard(); set(TrackableProperty.Cloned, c.isCloned()); @@ -886,12 +907,6 @@ public class CardView extends GameEntityView { } public String getImageKey(Iterable viewers) { if (canBeShownToAny(viewers)) { - // Morph cards can only be present on the battlefield and on stack, otherwise show a standard card back - if (getZone() != ZoneType.Battlefield && getZone() != ZoneType.Stack) { - if (isFaceDown() && get(TrackableProperty.ImageKey).equals(ImageKeys.getTokenKey(ImageKeys.MORPH_IMAGE))) { - return ImageKeys.getTokenKey(ImageKeys.HIDDEN_CARD); - } - } return get(TrackableProperty.ImageKey); } return ImageKeys.getTokenKey(ImageKeys.HIDDEN_CARD); diff --git a/forge-game/src/main/java/forge/game/card/CardZoneTable.java b/forge-game/src/main/java/forge/game/card/CardZoneTable.java index 53e95605713..177cd669ba8 100644 --- a/forge-game/src/main/java/forge/game/card/CardZoneTable.java +++ b/forge-game/src/main/java/forge/game/card/CardZoneTable.java @@ -42,7 +42,7 @@ public class CardZoneTable extends ForwardingTable. + */ + +package forge.game.card; + +import com.google.common.collect.ImmutableList; + +/** + * The class Counters. + * + * @author Clemens Koza + * @version V0.0 17.02.2010 + */ +public enum CounterEnumType { + + M1M1("-1/-1", "-1/-1", 255, 110, 106), + P1P1("+1/+1", "+1/+1", 96, 226, 23), + + LOYALTY("LOYAL", 198, 198, 198), + + AGE("AGE", 255, 137, 57), + + AIM("AIM", 255, 180, 0), + + ARROW("ARROW", 237, 195, 0), + + ARROWHEAD("ARWHD", 230, 191, 167), + + AWAKENING("AWAKE", 0, 231, 79), + + BLAZE("BLAZE", 255, 124, 82), + + BLOOD("BLOOD", 255, 108, 111), + + BOUNTY("BOUNT", 255, 158, 0), + + BRIBERY("BRIBE", 172, 201, 235), + + BRICK("BRICK", 226, 192, 164), + + CAGE("CAGE", 155, 155, 155), + + CARRION("CRRON", 255, 163, 222), + + CHARGE("CHARG", 246, 192, 0), + + COIN("COIN",255,215,0), + + CORPSE("CRPSE", 230, 186, 209), + + CREDIT("CRDIT", 188, 197, 234), + + CRYSTAL("CRYST", 255, 85, 206), + + CUBE("CUBE", 148, 219, 0), + + CURRENCY("CURR", 223, 200, 0), + + DEATH("DEATH", 255, 108, 110), + + DELAY("DELAY", 102, 206, 255), + + DEPLETION("DPLT", 185, 201, 208), + + DESPAIR("DESPR", 238, 186, 187), + + DEVOTION("DEVOT", 255, 111, 255), + + DIVINITY("DVNTY", 0, 233, 255), + + DOOM("DOOM", 255, 104, 118), + + DREAM("DREAM", 190, 189, 255), + + ECHO("ECHO", 225, 180, 255), + + EGG("EGG", 255, 245, 195), + + ELIXIR("ELIXR", 81, 221, 175), + + EON("EON", 23, 194, 255), + + EYEBALL("EYE", 184, 202, 201), + + FADE("FADE", 159, 209, 192), + + FATE("FATE", 255, 164, 226), + + FEATHER("FTHR", 195, 202, 165), + + FILIBUSTER("FLBTR", 255, 179, 119), + + FLAME("FLAME", 255, 143, 43), + + FLOOD("FLOOD", 0, 203, 255), + + FORESHADOW("FRSHD",144,99, 207), + + FUNGUS("FNGUS", 121, 219, 151), + + FURY("FURY", 255, 120, 89), + + FUSE("FUSE", 255, 122, 85), + + GEM("GEM", 255, 99, 251), + + GLYPH("GLYPH", 184, 202, 199), + + GOLD("GOLD", 248, 191, 0), + + GROWTH("GRWTH", 87, 226, 32), + + HATCHLING("HATCH", 201, 199, 186), + + HEALING("HEAL", 255, 166, 236), + + HIT("HIT", 255, 245, 195), + + HOOFPRINT("HOOF", 233, 189, 170), + + HOUR("HOUR", 198, 197, 210), + + HOURGLASS("HRGLS", 0, 215, 255), + + HUNGER("HUNGR", 255, 91, 149), + + ICE("ICE", 0, 239, 255), + + INCARNATION("INCARNATION", 247, 206, 64), + + INFECTION("INFCT", 0, 230, 66), + + INTERVENTION("INTRV", 205, 203, 105), + + ISOLATION("ISOLT", 250, 190, 0), + + JAVELIN("JAVLN", 180, 206, 172), + + KI("KI", 190, 189, 255), + + KNOWLEDGE("KNOWLEDGE", 0, 115, 255), + + LANDMARK("LNMRK", 186, 28, 28), + + LEVEL("LEVEL", 60, 222, 185), + + LORE("LORE", 209, 198, 161), + + LUCK("LUCK", 185, 174, 255), + + M0M1("-0/-1", "-0/-1", 255, 110, 106), + + M0M2("-0/-2", "-0/-2", 255, 110, 106), + + M1M0("-1/-0", "-1/-0", 255, 110, 106), + + M2M1("-2/-1", "-2/-1", 255, 110, 106), + + M2M2("-2/-2", "-2/-2", 255, 110, 106), + + MAGNET("MAGNT", 198, 197, 210), + + MANA("MANA", 0, 237, 152), + + MANIFESTATION("MNFST", 104, 225, 8), + + MANNEQUIN("MANQN", 206, 199, 162), + + MATRIX("MATRX", 183, 174, 255), + + MINE("MINE", 255, 100, 127), + + MINING("MINNG", 184, 201, 207), + + MIRE("MIRE", 153, 209, 199), + + MUSIC("MUSIC", 255, 138, 255), + + MUSTER("MUSTR", 235, 196, 0), + + NET("NET", 0, 221, 251), + + OMEN("OMEN", 255, 178, 120), + + ORE("ORE", 200, 201, 163), + + PAGE("PAGE", 218, 195, 162), + + PAIN("PAIN", 255, 108, 111), + + PARALYZATION("PRLYZ", 220, 201, 0), + + PETAL("PETAL", 255, 162, 216), + + PETRIFICATION("PETRI", 185, 201, 208), + + PIN("PIN", 194, 196, 233), + + PLAGUE("PLGUE", 94, 226, 25), + + PLOT("PLOT", 255, 172, 133), + + PRESSURE("PRESS", 255, 164, 159), + + PHYLACTERY("PHYLA", 117, 219, 153), + + POLYP("POLYP", 236, 185, 198), + + PREY("PREY", 240, 0, 0), + + PUPA("PUPA", 0, 223, 203), + + P0P1("+0/+1", "+0/+1", 96, 226, 23), + + P0P2("+0/+2", "+0/+2", 96, 226, 23), + + P1P0("+1/+0", "+1/+0", 96, 226, 23), + + P1P2("+1/+2", "+1/+2", 96, 226, 23), + + P2P0("+2/+0", "+2/+0", 96, 226, 23), + + P2P2("+2/+2", "+2/+2", 96, 226, 23), + + QUEST("QUEST", 251, 189, 0), + + RUST("RUST", 255, 181, 116), + + SCREAM("SCREM", 0, 220, 255), + + SCROLL("SCRLL", 206, 199, 162), + + SHELL("SHELL", 190, 207, 111), + + SHIELD("SHLD", 202, 198, 186), + + SHRED("SHRED", 255, 165, 152), + + SILVER("SILVER", 192, 192, 192), + + SLEEP("SLEEP", 178, 192, 255), + + SLUMBER("SLMBR", 178, 205, 255), + + SLEIGHT("SLGHT", 185, 174, 255), + + SLIME("SLIME", 101, 220, 163), + + SOUL("SOUL", 243, 190, 247), + + SOOT("SOOT", 211, 194, 198), + + SPITE("SPITE", 0, 218, 255), + + SPORE("SPORE", 122, 218, 150), + + STORAGE("STORG", 255, 177, 121), + + STRIFE("STRFE", 255, 89, 223), + + STUDY("STUDY", 226, 192, 165), + + TASK("TASK", 191, 63, 49), + + THEFT("THEFT", 255, 176, 125), + + TIDE("TIDE", 0, 212, 187), + + TIME("TIME", 255, 121, 255), + + TOWER("tower", "TOWER", 0, 239, 255), + + TRAINING("TRAIN", 220, 201, 0), + + TRAP("TRAP", 255, 121, 86), + + TREASURE("TRSUR", 255, 184, 0), + + UNITY("UNITY", 242, 156, 255), + + VELOCITY("VELO", 255, 95, 138), + + VERSE("VERSE", 0, 237, 155), + + VITALITY("VITAL", 255, 94, 142), + + VORTEX("VORTX", 142, 200, 255), + + WAGE("WAGE", 242, 190, 106), + + WINCH("WINCH", 208, 195, 203), + + WIND("WIND", 0, 236, 255), + + WISH("WISH", 255, 85, 206), + + // Player Counters + + ENERGY("ENRGY"), + + EXPERIENCE("EXP"), + + POISON("POISN"), + + // Keyword Counters +/* + FLYING("Flying"), + FIRSTSTRIKE("First Strike"), + DOUBLESTRIKE("Double Strike"), + DEATHTOUCH("Deathtouch"), + HEXPROOF("Hexproof"), + INDESTRUCTIBLE("Indestructible"), + LIFELINK("Lifelink"), + MENACE("Menace"), + REACH("Reach"), + TRAMPLE("Trample"), + VIGILANCE("Vigilance") +//*/ + ; + + private String name, counterOnCardDisplayName; + private int red, green, blue; + + CounterEnumType() { + this.name = this.name().substring(0, 1).toUpperCase() + this.name().substring(1).toLowerCase(); + if (red == 0 && green == 0 && blue == 0) { + red = 255; + green = 255; + blue = 255; + } + } + + CounterEnumType(final String counterOnCardDisplayName) { + this(); + this.counterOnCardDisplayName = counterOnCardDisplayName; + } + + CounterEnumType(final String counterOnCardDisplayName, final int red, final int green, final int blue) { + this(counterOnCardDisplayName); + this.red = red; + this.green = green; + this.blue = blue; + } + + CounterEnumType(final String name, final String counterOnCardDisplayName, final int red, final int green, final int blue) { + this(counterOnCardDisplayName, red, green, blue); + this.name = name; + } + + public String getName() { + return this.name; + } + + public int getRed() { + return red; + } + + public int getGreen() { + return green; + } + + public int getBlue() { + return blue; + } + + public String getCounterOnCardDisplayName() { + return counterOnCardDisplayName; + } + + public static CounterEnumType getType(final String name) { + final String replacedName = name.replace("/", "").replaceAll("\\+", "p").replaceAll("\\-", "m").toUpperCase(); + return Enum.valueOf(CounterEnumType.class, replacedName); + } + + public static final ImmutableList values = ImmutableList.copyOf(values()); + +} diff --git a/forge-game/src/main/java/forge/game/card/CounterType.java b/forge-game/src/main/java/forge/game/card/CounterType.java index 5d9fd5970d6..0cf41244e4f 100644 --- a/forge-game/src/main/java/forge/game/card/CounterType.java +++ b/forge-game/src/main/java/forge/game/card/CounterType.java @@ -1,370 +1,133 @@ -/* - * Forge: Play Magic: the Gathering. - * Copyright (C) 2011 Forge Team - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - package forge.game.card; +import java.io.Serializable; +import java.util.Map; +import java.util.Objects; + +import org.apache.commons.lang3.builder.EqualsBuilder; + +import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; - -/** - * The class Counters. - * - * @author Clemens Koza - * @version V0.0 17.02.2010 - */ -public enum CounterType { - - M1M1("-1/-1", "-1/-1", 255, 110, 106), - P1P1("+1/+1", "+1/+1", 96, 226, 23), - - LOYALTY("LOYAL", 198, 198, 198), - - AGE("AGE", 255, 137, 57), - - AIM("AIM", 255, 180, 0), - - ARROW("ARROW", 237, 195, 0), - - ARROWHEAD("ARWHD", 230, 191, 167), - - AWAKENING("AWAKE", 0, 231, 79), - - BLAZE("BLAZE", 255, 124, 82), - - BLOOD("BLOOD", 255, 108, 111), - - BOUNTY("BOUNT", 255, 158, 0), - - BRIBERY("BRIBE", 172, 201, 235), - - BRICK("BRICK", 226, 192, 164), - - CAGE("CAGE", 155, 155, 155), - - CARRION("CRRON", 255, 163, 222), - - CHARGE("CHARG", 246, 192, 0), - - COIN("COIN",255,215,0), - - CORPSE("CRPSE", 230, 186, 209), - - CREDIT("CRDIT", 188, 197, 234), - - CRYSTAL("CRYST", 255, 85, 206), - - CUBE("CUBE", 148, 219, 0), - - CURRENCY("CURR", 223, 200, 0), - - DEATH("DEATH", 255, 108, 110), - - DELAY("DELAY", 102, 206, 255), - - DEPLETION("DPLT", 185, 201, 208), - - DESPAIR("DESPR", 238, 186, 187), - - DEVOTION("DEVOT", 255, 111, 255), - - DIVINITY("DVNTY", 0, 233, 255), - - DOOM("DOOM", 255, 104, 118), - - DREAM("DREAM", 190, 189, 255), - - ECHO("ECHO", 225, 180, 255), - - EGG("EGG", 255, 245, 195), - - ELIXIR("ELIXR", 81, 221, 175), - - EON("EON", 23, 194, 255), - - EYEBALL("EYE", 184, 202, 201), - - FADE("FADE", 159, 209, 192), - - FATE("FATE", 255, 164, 226), - - FEATHER("FTHR", 195, 202, 165), - - FILIBUSTER("FLBTR", 255, 179, 119), - - FLAME("FLAME", 255, 143, 43), - - FLOOD("FLOOD", 0, 203, 255), - - FUNGUS("FNGUS", 121, 219, 151), - - FURY("FURY", 255, 120, 89), - - FUSE("FUSE", 255, 122, 85), - - GEM("GEM", 255, 99, 251), - - GLYPH("GLYPH", 184, 202, 199), - - GOLD("GOLD", 248, 191, 0), - - GROWTH("GRWTH", 87, 226, 32), - - HATCHLING("HATCH", 201, 199, 186), - - HEALING("HEAL", 255, 166, 236), - - HIT("HIT", 255, 245, 195), - - HOOFPRINT("HOOF", 233, 189, 170), - - HOUR("HOUR", 198, 197, 210), - - HOURGLASS("HRGLS", 0, 215, 255), - - HUNGER("HUNGR", 255, 91, 149), - - ICE("ICE", 0, 239, 255), - - INFECTION("INFCT", 0, 230, 66), - - INTERVENTION("INTRV", 205, 203, 105), - - ISOLATION("ISOLT", 250, 190, 0), - - JAVELIN("JAVLN", 180, 206, 172), - - KI("KI", 190, 189, 255), - - KNOWLEDGE("KNOWLEDGE", 0, 115, 255), - - LANDMARK("LNMRK", 186, 28, 28), - - LEVEL("LEVEL", 60, 222, 185), - - LORE("LORE", 209, 198, 161), - - LUCK("LUCK", 185, 174, 255), - - M0M1("-0/-1", "-0/-1", 255, 110, 106), - - M0M2("-0/-2", "-0/-2", 255, 110, 106), - - M1M0("-1/-0", "-1/-0", 255, 110, 106), - - M2M1("-2/-1", "-2/-1", 255, 110, 106), - - M2M2("-2/-2", "-2/-2", 255, 110, 106), - - MAGNET("MAGNT", 198, 197, 210), - - MANA("MANA", 0, 237, 152), - - MANIFESTATION("MNFST", 104, 225, 8), - - MANNEQUIN("MANQN", 206, 199, 162), - - MATRIX("MATRX", 183, 174, 255), - - MINE("MINE", 255, 100, 127), - - MINING("MINNG", 184, 201, 207), - - MIRE("MIRE", 153, 209, 199), - - MUSIC("MUSIC", 255, 138, 255), - - MUSTER("MUSTR", 235, 196, 0), - - NET("NET", 0, 221, 251), - - OMEN("OMEN", 255, 178, 120), - - ORE("ORE", 200, 201, 163), - - PAGE("PAGE", 218, 195, 162), - - PAIN("PAIN", 255, 108, 111), - - PARALYZATION("PRLYZ", 220, 201, 0), - - PETAL("PETAL", 255, 162, 216), - - PETRIFICATION("PETRI", 185, 201, 208), - - PIN("PIN", 194, 196, 233), - - PLAGUE("PLGUE", 94, 226, 25), - - PLOT("PLOT", 255, 172, 133), - - PRESSURE("PRESS", 255, 164, 159), - - PHYLACTERY("PHYLA", 117, 219, 153), - - POLYP("POLYP", 236, 185, 198), - - PREY("PREY", 240, 0, 0), - - PUPA("PUPA", 0, 223, 203), - - P0P1("+0/+1", "+0/+1", 96, 226, 23), - - P0P2("+0/+2", "+0/+2", 96, 226, 23), - - P1P0("+1/+0", "+1/+0", 96, 226, 23), - - P1P2("+1/+2", "+1/+2", 96, 226, 23), - - P2P0("+2/+0", "+2/+0", 96, 226, 23), - - P2P2("+2/+2", "+2/+2", 96, 226, 23), - - QUEST("QUEST", 251, 189, 0), - - RUST("RUST", 255, 181, 116), - - SCREAM("SCREM", 0, 220, 255), - - SCROLL("SCRLL", 206, 199, 162), - - SHELL("SHELL", 190, 207, 111), - - SHIELD("SHLD", 202, 198, 186), - - SHRED("SHRED", 255, 165, 152), - - SILVER("SILVER", 192, 192, 192), - - SLEEP("SLEEP", 178, 192, 255), - - SLUMBER("SLMBR", 178, 205, 255), - - SLEIGHT("SLGHT", 185, 174, 255), - - SLIME("SLIME", 101, 220, 163 ), - - SOOT("SOOT", 211, 194, 198), - - SPITE("SPITE", 0, 218, 255), - - SPORE("SPORE", 122, 218, 150), - - STORAGE("STORG", 255, 177, 121), - - STRIFE("STRFE", 255, 89, 223), - - STUDY("STUDY", 226, 192, 165), - - TASK("TASK", 191, 63, 49), - - THEFT("THEFT", 255, 176, 125), - - TIDE("TIDE", 0, 212, 187), - - TIME("TIME", 255, 121, 255), - - TOWER("tower", "TOWER", 0, 239, 255), - - TRAINING("TRAIN", 220, 201, 0), - - TRAP("TRAP", 255, 121, 86), - - TREASURE("TRSUR", 255, 184, 0), - - UNITY("UNITY", 242, 156, 255), - - VELOCITY("VELO", 255, 95, 138), - - VERSE("VERSE", 0, 237, 155), - - VITALITY("VITAL", 255, 94, 142), - - VORTEX("VORTX", 142, 200, 255), - - WAGE("WAGE", 242, 190, 106), - - WINCH("WINCH", 208, 195, 203), - - WIND("WIND", 0, 236, 255), - - WISH("WISH", 255, 85, 206), - - // Player Counters - - ENERGY("ENRGY"), - - EXPERIENCE("EXP"), - - POISON("POISN"); - - private String name, counterOnCardDisplayName; - private int red, green, blue; - - CounterType() { - this.name = this.name().substring(0, 1).toUpperCase() + this.name().substring(1).toLowerCase(); - if (red == 0 && green == 0 && blue == 0) { - red = 255; - green = 255; - blue = 255; +import com.google.common.collect.Maps; +import com.google.common.collect.Ordering; + +public class CounterType implements Comparable, Serializable { + private static final long serialVersionUID = -7575835723159144478L; + + private CounterEnumType eVal = null; + private String sVal = null; + + // Rule 122.1b + static ImmutableList keywordCounter = ImmutableList.of( + "Flying", "First Strike", "Double Strike", "Deathtouch", "Haste", "Hexproof", + "Indestructible", "Lifelink", "Menace", "Reach", "Trample", "Vigilance"); + + private static Map eMap = Maps.newEnumMap(CounterEnumType.class); + private static Map sMap = Maps.newHashMap(); + + private CounterType(CounterEnumType e, String s) { + this.eVal = e; + this.sVal = s; + } + + public static CounterType get(CounterEnumType e) { + if (!eMap.containsKey(e)) { + eMap.put(e, new CounterType(e, null)); + } + return eMap.get(e); + } + + public static CounterType get(String s) { + if (!sMap.containsKey(s)) { + sMap.put(s, new CounterType(null, s)); + } + return sMap.get(s); + } + + public static CounterType getType(String name) { + try { + return get(CounterEnumType.getType(name)); + } catch (final IllegalArgumentException ex) { + return get(name); } } - CounterType(final String counterOnCardDisplayName) { - this(); - this.counterOnCardDisplayName = counterOnCardDisplayName; + + @Override + public int hashCode() { + return Objects.hash(eVal, sVal); } - CounterType(final String counterOnCardDisplayName, final int red, final int green, final int blue) { - this(counterOnCardDisplayName); - this.red = red; - this.green = green; - this.blue = blue; + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj.getClass() != getClass()) { + return false; + } + CounterType rhs = (CounterType) obj; + return new EqualsBuilder() + .append(eVal, rhs.eVal) + .append(sVal, rhs.sVal) + .isEquals(); } - CounterType(final String name, final String counterOnCardDisplayName, final int red, final int green, final int blue) { - this(counterOnCardDisplayName, red, green, blue); - this.name = name; + @Override + public String toString() { + return eVal != null ? eVal.toString() : sVal; } public String getName() { - return this.name; - } - - public int getRed() { - return red; - } - - public int getGreen() { - return green; - } - - public int getBlue() { - return blue; + return eVal != null ? eVal.getName() : getKeywordDescription(); } public String getCounterOnCardDisplayName() { - return counterOnCardDisplayName; + return eVal != null ? eVal.getCounterOnCardDisplayName() : getKeywordDescription(); } - public static CounterType getType(final String name) { - final String replacedName = name.replace("/", "").replaceAll("\\+", "p").replaceAll("\\-", "m").toUpperCase(); - return Enum.valueOf(CounterType.class, replacedName); + private String getKeywordDescription() { + if (sVal.startsWith("Hexproof:")) { + final String[] k = sVal.split(":"); + return "Hexproof from " + k[2]; + } + return sVal; } - public static final ImmutableList values = ImmutableList.copyOf(values()); + @Override + public int compareTo(CounterType o) { + return ComparisonChain.start() + .compare(eVal, o.eVal, Ordering.natural().nullsLast()) + .compare(sVal, o.sVal, Ordering.natural().nullsLast()) + .result(); + } + public boolean is(CounterEnumType eType) { + return eVal == eType; + } + + public boolean isKeywordCounter() { + if (eVal != null) { + return false; + } + if (sVal.startsWith("Hexproof:")) { + return true; + } + return keywordCounter.contains(sVal); + } + + public int getRed() { + return eVal != null ? eVal.getRed() : 255; + } + + public int getGreen() { + return eVal != null ? eVal.getGreen() : 255; + } + + public int getBlue() { + return eVal != null ? eVal.getBlue() : 255; + } } diff --git a/forge-game/src/main/java/forge/game/card/token/TokenInfo.java b/forge-game/src/main/java/forge/game/card/token/TokenInfo.java index c540ad0b56a..260265e1fa7 100644 --- a/forge-game/src/main/java/forge/game/card/token/TokenInfo.java +++ b/forge-game/src/main/java/forge/game/card/token/TokenInfo.java @@ -36,18 +36,6 @@ public class TokenInfo { final int baseToughness; final String color; - public TokenInfo(String name, String imageName, String manaCost, String[] types, - String[] intrinsicKeywords, int basePower, int baseToughness) { - this.name = name; - this.imageName = imageName; - this.manaCost = manaCost; - this.color = manaCost; // FIXME: somehow ensure that color and mana cost are completely differentiated - this.types = types; - this.intrinsicKeywords = intrinsicKeywords; - this.basePower = basePower; - this.baseToughness = baseToughness; - } - public TokenInfo(Card c) { // TODO: Figure out how to handle legacy images? this.name = c.getName(); @@ -55,23 +43,17 @@ public class TokenInfo { this.manaCost = c.getManaCost().toString(); this.color = MagicColor.toShortString(c.getCurrentState().getColor()); this.types = getCardTypes(c); - + List list = Lists.newArrayList(); for (KeywordInterface inst : c.getKeywords()) { list.add(inst.getOriginal()); } - + this.intrinsicKeywords = list.toArray(new String[0]); this.basePower = c.getBasePower(); this.baseToughness = c.getBaseToughness(); } - public TokenInfo(Card c, Card source) { - // TODO If Source has type/color changes on it, apply them now. - // Permanently apply them for casccading tokens? Reef Worm? - this(c); - } - public TokenInfo(String str) { final String[] tokenInfo = str.split(","); int power = 0; @@ -157,13 +139,13 @@ public class TokenInfo { return sb.toString(); } - public static List makeToken(final Card prototype, final Player controller, + public static List makeToken(final Card prototype, final Player owner, final boolean applyMultiplier, final int num) { final List list = Lists.newArrayList(); - final Game game = controller.getGame(); + final Game game = owner.getGame(); int multiplier = num; - Player player = controller; + Player player = owner; Card proto = prototype; final Map repParams = AbilityKey.mapFromAffected(player); @@ -192,8 +174,12 @@ public class TokenInfo { for (int i = 0; i < multiplier; i++) { // need to set owner or copyCard will fail with assign new ID - proto.setOwner(player); + proto.setOwner(owner); Card copy = CardFactory.copyCard(proto, true); + // need to assign player after token is copied + if (player != owner) { + copy.setController(player, timestamp); + } copy.setTimestamp(timestamp); copy.setToken(true); list.add(copy); @@ -202,12 +188,13 @@ public class TokenInfo { return list; } + @Deprecated public List makeTokenWithMultiplier(final Player controller, int amount, final boolean applyMultiplier) { return makeToken(makeOneToken(controller), controller, applyMultiplier, amount); } - static public List makeTokensFromPrototype(Card prototype, final Player controller, int amount, final boolean applyMultiplier) { - return makeToken(prototype, controller, applyMultiplier, amount); + static public List makeTokensFromPrototype(Card prototype, final Player owner, int amount, final boolean applyMultiplier) { + return makeToken(prototype, owner, applyMultiplier, amount); } public Card makeOneToken(final Player controller) { @@ -224,7 +211,124 @@ public class TokenInfo { return c; } + static protected void protoTypeApplyTextChange(final Card result, final SpellAbility sa) { + // update Token with CardTextChanges + Map colorMap = sa.getChangedTextColors(); + Map typeMap = sa.getChangedTextTypes(); + if (!colorMap.isEmpty()) { + if (!result.isColorless()) { + // change Token Colors + byte color = CardUtil.getColors(result).getColor(); + + for (final Map.Entry e : colorMap.entrySet()) { + byte v = MagicColor.fromName(e.getValue()); + // Any used by Swirl the Mists + if ("Any".equals(e.getKey())) { + for (final byte c : MagicColor.WUBRG) { + // try to replace color flips + if ((color & c) != 0) { + color &= ~c; + color |= v; + } + } + } else { + byte c = MagicColor.fromName(e.getKey()); + // try to replace color flips + if ((color & c) != 0) { + color &= ~c; + color |= v; + } + } + } + + result.setColor(color); + } + } + if (!typeMap.isEmpty()) { + String oldName = result.getName(); + + CardType type = new CardType(result.getType()); + String joinedName = StringUtils.join(type.getSubtypes(), " "); + final boolean nameGenerated = oldName.equals(joinedName); + boolean typeChanged = false; + + if (!Iterables.isEmpty(type.getSubtypes())) { + for (final Map.Entry e : typeMap.entrySet()) { + if (type.hasSubtype(e.getKey())) { + type.remove(e.getKey()); + type.add(e.getValue()); + typeChanged = true; + } + } + } + + if (typeChanged) { + result.setType(type); + + // update generated Name + if (nameGenerated) { + result.setName(StringUtils.join(type.getSubtypes(), " ")); + } + } + } + + // replace Intrinsic Keyword + List toRemove = Lists.newArrayList(); + List toAdd = Lists.newArrayList(); + for (final KeywordInterface k : result.getCurrentState().getIntrinsicKeywords()) { + final String o = k.getOriginal(); + // only Modifiable should go there + if (!CardUtil.isKeywordModifiable(o)) { + continue; + } + String r = o; + // replace types + for (final Map.Entry e : typeMap.entrySet()) { + final String key = e.getKey(); + final String pkey = CardType.getPluralType(key); + final String value = e.getValue(); + final String pvalue = CardType.getPluralType(e.getValue()); + r = r.replaceAll(pkey, pvalue); + r = r.replaceAll(key, value); + } + // replace color words + for (final Map.Entry e : colorMap.entrySet()) { + final String vName = e.getValue(); + final String vCaps = StringUtils.capitalize(vName); + final String vLow = vName.toLowerCase(); + if ("Any".equals(e.getKey())) { + for (final byte c : MagicColor.WUBRG) { + final String cName = MagicColor.toLongString(c); + final String cNameCaps = StringUtils.capitalize(cName); + final String cNameLow = cName.toLowerCase(); + r = r.replaceAll(cNameCaps, vCaps); + r = r.replaceAll(cNameLow, vLow); + } + } else { + final String cName = e.getKey(); + final String cNameCaps = StringUtils.capitalize(cName); + final String cNameLow = cName.toLowerCase(); + r = r.replaceAll(cNameCaps, vCaps); + r = r.replaceAll(cNameLow, vLow); + } + } + if (!r.equals(o)) { + toRemove.add(k); + toAdd.add(r); + } + } + for (final KeywordInterface k : toRemove) { + result.getCurrentState().removeIntrinsicKeyword(k); + } + result.addIntrinsicKeywords(toAdd); + + result.getCurrentState().changeTextIntrinsic(colorMap, typeMap); + } + static public Card getProtoType(final String script, final SpellAbility sa) { + return getProtoType(script, sa, true); + } + static public Card getProtoType(final String script, final SpellAbility sa, boolean applyTextChange) { // script might be null, or sa might be null if (script == null || sa == null) { return null; @@ -235,135 +339,41 @@ public class TokenInfo { String edition = ObjectUtils.firstNonNull(sa.getOriginalHost(), host).getSetCode(); PaperToken token = StaticData.instance().getAllTokens().getToken(script, edition); - if (token != null) { - final Card result = Card.fromPaperCard(token, null, game); + if (token == null) { + return null; + } + final Card result = Card.fromPaperCard(token, null, game); - if (sa.hasParam("TokenPower")) { - String str = sa.getParam("TokenPower"); - result.setBasePowerString(str); - result.setBasePower(AbilityUtils.calculateAmount(host, str, sa)); - } - - if (sa.hasParam("TokenToughness")) { - String str = sa.getParam("TokenToughness"); - result.setBaseToughnessString(str); - result.setBaseToughness(AbilityUtils.calculateAmount(host, str, sa)); - } - - // update Token with CardTextChanges - Map colorMap = sa.getChangedTextColors(); - Map typeMap = sa.getChangedTextTypes(); - if (!colorMap.isEmpty()) { - if (!result.isColorless()) { - // change Token Colors - byte color = CardUtil.getColors(result).getColor(); - - for (final Map.Entry e : colorMap.entrySet()) { - byte v = MagicColor.fromName(e.getValue()); - // Any used by Swirl the Mists - if ("Any".equals(e.getKey())) { - for (final byte c : MagicColor.WUBRG) { - // try to replace color flips - if ((color & c) != 0) { - color &= ~c; - color |= v; - } - } - } else { - byte c = MagicColor.fromName(e.getKey()); - // try to replace color flips - if ((color & c) != 0) { - color &= ~c; - color |= v; - } - } - } - - result.setColor(color); - } - } - if (!typeMap.isEmpty()) { - String oldName = result.getName(); - - CardType type = new CardType(result.getType()); - String joinedName = StringUtils.join(type.getSubtypes(), " "); - final boolean nameGenerated = oldName.equals(joinedName); - boolean typeChanged = false; - - if (!Iterables.isEmpty(type.getSubtypes())) { - for (final Map.Entry e : typeMap.entrySet()) { - if (type.hasSubtype(e.getKey())) { - type.remove(e.getKey()); - type.add(e.getValue()); - typeChanged = true; - } - } - } - - if (typeChanged) { - result.setType(type); - - // update generated Name - if (nameGenerated) { - result.setName(StringUtils.join(type.getSubtypes(), " ")); - } - } - } - - // replace Intrinsic Keyword - List toRemove = Lists.newArrayList(); - List toAdd = Lists.newArrayList(); - for (final KeywordInterface k : result.getCurrentState().getIntrinsicKeywords()) { - final String o = k.getOriginal(); - // only Modifiable should go there - if (!CardUtil.isKeywordModifiable(o)) { - continue; - } - String r = o; - // replace types - for (final Map.Entry e : typeMap.entrySet()) { - final String key = e.getKey(); - final String pkey = CardType.getPluralType(key); - final String value = e.getValue(); - final String pvalue = CardType.getPluralType(e.getValue()); - r = r.replaceAll(pkey, pvalue); - r = r.replaceAll(key, value); - } - // replace color words - for (final Map.Entry e : colorMap.entrySet()) { - final String vName = e.getValue(); - final String vCaps = StringUtils.capitalize(vName); - final String vLow = vName.toLowerCase(); - if ("Any".equals(e.getKey())) { - for (final byte c : MagicColor.WUBRG) { - final String cName = MagicColor.toLongString(c); - final String cNameCaps = StringUtils.capitalize(cName); - final String cNameLow = cName.toLowerCase(); - r = r.replaceAll(cNameCaps, vCaps); - r = r.replaceAll(cNameLow, vLow); - } - } else { - final String cName = e.getKey(); - final String cNameCaps = StringUtils.capitalize(cName); - final String cNameLow = cName.toLowerCase(); - r = r.replaceAll(cNameCaps, vCaps); - r = r.replaceAll(cNameLow, vLow); - } - } - if (!r.equals(o)) { - toRemove.add(k); - toAdd.add(r); - } - } - for (final KeywordInterface k : toRemove) { - result.getCurrentState().removeIntrinsicKeyword(k); - } - result.addIntrinsicKeywords(toAdd); - - result.getCurrentState().changeTextIntrinsic(colorMap, typeMap); - return result; + if (sa.hasParam("TokenPower")) { + String str = sa.getParam("TokenPower"); + result.setBasePowerString(str); + result.setBasePower(AbilityUtils.calculateAmount(host, str, sa)); } - return null; + if (sa.hasParam("TokenToughness")) { + String str = sa.getParam("TokenToughness"); + result.setBaseToughnessString(str); + result.setBaseToughness(AbilityUtils.calculateAmount(host, str, sa)); + } + + if (applyTextChange) { + protoTypeApplyTextChange(result, sa); + } + + // need to be done after text change so it isn't affected by that + if (sa.hasParam("TokenTypes")) { + String types = sa.getParam("TokenTypes"); + types = types.replace("ChosenType", sa.getHostCard().getChosenType()); + result.addType(types); + result.setName(types); + } + + if (sa.hasParam("TokenColors")) { + String colors = sa.getParam("TokenColors"); + colors = colors.replace("ChosenColor", sa.getHostCard().getChosenColor()); + result.setColor(MagicColor.toShortString(colors)); + } + + return result; } } \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/combat/AttackConstraints.java b/forge-game/src/main/java/forge/game/combat/AttackConstraints.java index 181c27e769e..cee20c3d256 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackConstraints.java +++ b/forge-game/src/main/java/forge/game/combat/AttackConstraints.java @@ -37,7 +37,7 @@ public class AttackConstraints { // Number of "must attack" constraints on each creature with a magnet counter (equal to the number of permanents requiring that constraint). int nMagnetRequirements = 0; - final CardCollectionView magnetAttackers = CardLists.filter(possibleAttackers, CardPredicates.hasCounter(CounterType.MAGNET)); + final CardCollectionView magnetAttackers = CardLists.filter(possibleAttackers, CardPredicates.hasCounter(CounterEnumType.MAGNET)); // Only require if a creature with a magnet counter on it attacks. if (!magnetAttackers.isEmpty()) { nMagnetRequirements = CardLists.getAmountOfKeyword( @@ -68,7 +68,7 @@ public class AttackConstraints { } } - if (possibleAttacker.getCounters(CounterType.MAGNET) > 0) { + if (possibleAttacker.getCounters(CounterEnumType.MAGNET) > 0) { for (final Card c : magnetAttackers) { if (c != possibleAttacker) { causesToAttack.add(c, nMagnetRequirements); diff --git a/forge-game/src/main/java/forge/game/combat/Combat.java b/forge-game/src/main/java/forge/game/combat/Combat.java index 4fc83734d74..4c85ccd5cac 100644 --- a/forge-game/src/main/java/forge/game/combat/Combat.java +++ b/forge-game/src/main/java/forge/game/combat/Combat.java @@ -882,6 +882,10 @@ public class Combat { return true; // is blocking something at the moment } + if (!blocker.isLKI()) { + return false; + } + CombatLki lki = lkiCache.get(blocker); return null != lki && !lki.isAttacker; // was blocking something anyway } @@ -892,7 +896,11 @@ public class Combat { if (blockers != null && blockers.contains(blocker)) { return true; // is blocking the attacker's band at the moment } - + + if (!blocker.isLKI()) { + return false; + } + CombatLki lki = lkiCache.get(blocker); return null != lki && !lki.isAttacker && lki.relatedBands.contains(ab); // was blocking that very band } diff --git a/forge-game/src/main/java/forge/game/cost/Cost.java b/forge-game/src/main/java/forge/game/cost/Cost.java index e8e92ce1aff..46c2ab66987 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -311,7 +311,7 @@ public class Cost implements Serializable { final String description = splitStr.length > 3 ? splitStr[3] : null; final ZoneType zone = splitStr.length > 4 ? ZoneType.smartValueOf(splitStr[4]) : ZoneType.Battlefield; - return new CostRemoveCounter(splitStr[0], CounterType.valueOf(splitStr[1]), type, description, zone); + return new CostRemoveCounter(splitStr[0], CounterType.getType(splitStr[1]), type, description, zone); } if (parse.startsWith("AddCounter<")) { @@ -319,7 +319,7 @@ public class Cost implements Serializable { final String[] splitStr = abCostParse(parse, 4); final String target = splitStr.length > 2 ? splitStr[2] : "CARDNAME"; final String description = splitStr.length > 3 ? splitStr[3] : null; - return new CostPutCounter(splitStr[0], CounterType.valueOf(splitStr[1]), target, description); + return new CostPutCounter(splitStr[0], CounterType.getType(splitStr[1]), target, description); } // While no card has "PayLife<2> PayLife<3> there might be a card that diff --git a/forge-game/src/main/java/forge/game/cost/CostDiscard.java b/forge-game/src/main/java/forge/game/cost/CostDiscard.java index 517b374b6b8..3edd069cfde 100644 --- a/forge-game/src/main/java/forge/game/cost/CostDiscard.java +++ b/forge-game/src/main/java/forge/game/cost/CostDiscard.java @@ -6,17 +6,23 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package forge.game.cost; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.Sets; + +import forge.game.ability.AbilityKey; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; @@ -24,6 +30,7 @@ import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.TextUtil; @@ -35,6 +42,8 @@ public class CostDiscard extends CostPartWithList { // Inputs + protected boolean firstTime = false; + /** * Serializables need a version ID. */ @@ -42,7 +51,7 @@ public class CostDiscard extends CostPartWithList { /** * Instantiates a new cost discard. - * + * * @param amount * the amount * @param type @@ -58,7 +67,7 @@ public class CostDiscard extends CostPartWithList { /* * (non-Javadoc) - * + * * @see forge.card.cost.CostPart#toString() */ @Override @@ -77,6 +86,9 @@ public class CostDiscard extends CostPartWithList { else if (this.getType().equals("LastDrawn")) { sb.append("the last card you drew this turn"); } + else if (this.getType().equals("DifferentNames")) { + sb.append(Cost.convertAmountTypeToWords(i, this.getAmount(), "Card")).append(" with different names"); + } else { final StringBuilder desc = new StringBuilder(); @@ -98,7 +110,7 @@ public class CostDiscard extends CostPartWithList { /* * (non-Javadoc) - * + * * @see * forge.card.cost.CostPart#canPay(forge.card.spellability.SpellAbility, * forge.Card, forge.Player, forge.card.cost.Cost) @@ -123,6 +135,13 @@ public class CostDiscard extends CostPartWithList { final Card c = payer.getLastDrawnCard(); return handList.contains(c); } + else if (type.equals("DifferentNames")) { + Set cardNames = Sets.newHashSet(); + for (Card c : handList) { + cardNames.add(c.getName()); + } + return amount != null && cardNames.size() >= amount; + } else { boolean sameName = false; if (type.contains("+WithSameName")) { @@ -163,7 +182,7 @@ public class CostDiscard extends CostPartWithList { */ @Override protected Card doPayment(SpellAbility ability, Card targetCard) { - return targetCard.getController().discard(targetCard, ability, null); + return targetCard.getController().discard(targetCard, null, null); } /* (non-Javadoc) @@ -181,4 +200,23 @@ public class CostDiscard extends CostPartWithList { public T accept(ICostVisitor visitor) { return visitor.visit(this); } + + protected void handleBeforePayment(Player ai, SpellAbility ability, CardCollectionView targetCards) { + firstTime = ai.getNumDiscardedThisTurn() == 0; + } + + @Override + protected void handleChangeZoneTrigger(Player payer, SpellAbility ability, CardCollectionView targetCards) { + super.handleChangeZoneTrigger(payer, ability, targetCards); + + if (!targetCards.isEmpty()) + { + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Player, payer); + runParams.put(AbilityKey.Cards, new CardCollection(targetCards)); + runParams.put(AbilityKey.Cause, ability); + runParams.put(AbilityKey.FirstTime, firstTime); + payer.getGame().getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false); + } + } } diff --git a/forge-game/src/main/java/forge/game/cost/CostDraw.java b/forge-game/src/main/java/forge/game/cost/CostDraw.java index b33630f7808..543215712c4 100644 --- a/forge-game/src/main/java/forge/game/cost/CostDraw.java +++ b/forge-game/src/main/java/forge/game/cost/CostDraw.java @@ -96,7 +96,7 @@ public class CostDraw extends CostPart { @Override public final boolean payAsDecided(final Player ai, final PaymentDecision decision, SpellAbility ability) { for (final Player p : getPotentialPlayers(ai, ability.getHostCard())) { - p.drawCards(decision.c); + p.drawCards(decision.c, ability); } return true; } diff --git a/forge-game/src/main/java/forge/game/cost/CostMill.java b/forge-game/src/main/java/forge/game/cost/CostMill.java index 8660a212666..6cdaec69f2b 100644 --- a/forge-game/src/main/java/forge/game/cost/CostMill.java +++ b/forge-game/src/main/java/forge/game/cost/CostMill.java @@ -19,6 +19,8 @@ package forge.game.cost; import forge.game.ability.AbilityUtils; import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.card.CardZoneTable; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.PlayerZone; @@ -29,7 +31,7 @@ import forge.game.zone.ZoneType; * your graveyard as a cost. This Cost doesn't appear on very many cards, but * might appear in more in the future. This will show up in the form of Mill<1> */ -public class CostMill extends CostPartWithList { +public class CostMill extends CostPart { /** * Serializables need a version ID. @@ -49,18 +51,6 @@ public class CostMill extends CostPartWithList { @Override public int paymentOrder() { return 10; } - /* (non-Javadoc) - * @see forge.card.cost.CostPartWithList#getHashForList() - */ - @Override - public String getHashForLKIList() { - return "Milled"; - } - @Override - public String getHashForCardList() { - return "MilledCards"; - } - /* * (non-Javadoc) * @@ -96,7 +86,7 @@ public class CostMill extends CostPartWithList { public final String toString() { final StringBuilder sb = new StringBuilder(); final Integer i = this.convertAmount(); - sb.append("Put the top "); + sb.append("Mill "); if (i != null) { sb.append(i); @@ -108,17 +98,16 @@ public class CostMill extends CostPartWithList { if ((i == null) || (i > 1)) { sb.append("s"); } - sb.append(" from the top of your library into your graveyard"); return sb.toString(); } - /* (non-Javadoc) - * @see forge.card.cost.CostPartWithList#executePayment(forge.card.spellability.SpellAbility, forge.Card) - */ @Override - protected Card doPayment(SpellAbility ability, Card targetCard) { - return targetCard.getGame().getAction().moveToGraveyard(targetCard, null); + public final boolean payAsDecided(final Player ai, final PaymentDecision decision, SpellAbility ability) { + CardZoneTable table = new CardZoneTable(); + ability.getPaidHash().put("Milled", (CardCollection) ai.mill(decision.c, ZoneType.Graveyard, false, ability, table)); + table.triggerChangesZoneAll(ai.getGame()); + return true; } public T accept(ICostVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/cost/CostPartMana.java b/forge-game/src/main/java/forge/game/cost/CostPartMana.java index 32f1ae41f7b..66fdad8ce3e 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPartMana.java +++ b/forge-game/src/main/java/forge/game/cost/CostPartMana.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -68,7 +68,7 @@ public class CostPartMana extends CostPart { /** * Gets the mana. - * + * * @return the mana */ public final ManaCost getMana() { @@ -89,30 +89,30 @@ public class CostPartMana extends CostPart { /** * Gets the mana to pay. - * + * * @return the mana to pay */ public final ManaCost getManaToPay() { return cost; } - + /** * @return the isExiledCreatureCost */ public boolean isExiledCreatureCost() { return isExiledCreatureCost; } - + public boolean isEnchantedCreatureCost() { return isEnchantedCreatureCost; } - + @Override public boolean isReusable() { return true; } @Override public boolean isUndoable() { return true; } - + @Override public final String toString() { @@ -145,7 +145,17 @@ public class CostPartMana extends CostPart { return getManaToPay(); } } - + + @Override + public CostPart copy() { + CostPart copied = super.copy(); + // when copied, clear cardMatrix + if (copied instanceof CostPartMana) { + ((CostPartMana)copied).cardMatrix = null; + } + return copied; + } + @Override public boolean payAsDecided(Player payer, PaymentDecision pd, SpellAbility sa) { // TODO Auto-generated method stub diff --git a/forge-game/src/main/java/forge/game/cost/CostPartWithList.java b/forge-game/src/main/java/forge/game/cost/CostPartWithList.java index b934d3d0be7..9679d84e262 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPartWithList.java +++ b/forge-game/src/main/java/forge/game/cost/CostPartWithList.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -45,7 +45,7 @@ public abstract class CostPartWithList extends CostPart { public final CardCollectionView getLKIList() { return lkiList; } - + public final CardCollectionView getCardList() { return cardList; } @@ -61,13 +61,16 @@ public abstract class CostPartWithList extends CostPart { /** * Adds the list to hash. - * + * * @param sa * the sa * @param hash * the hash */ public final void reportPaidCardsTo(final SpellAbility sa) { + if (sa == null) { + return; + } final String lkiPaymentMethod = getHashForLKIList(); for (final Card card : lkiList) { sa.addCostToHashList(card, lkiPaymentMethod); @@ -77,18 +80,18 @@ public abstract class CostPartWithList extends CostPart { sa.addCostToHashList(card, cardPaymentMethod); } } - - // public abstract List getValidCards(); + + // public abstract List getValidCards(); /** * Instantiates a new cost part with list. */ public CostPartWithList() { } - + /** * Instantiates a new cost part with list. - * + * * @param amount * the amount * @param type @@ -121,17 +124,19 @@ public abstract class CostPartWithList extends CostPart { } // always returns true, made this to inline with return - public boolean executePayment(SpellAbility ability, CardCollectionView targetCards) { - if (canPayListAtOnce()) { // This is used by reveal. Without it when opponent would reveal hand, you'll get N message boxes. - lkiList.addAll(targetCards); + protected boolean executePayment(Player payer, SpellAbility ability, CardCollectionView targetCards) { + handleBeforePayment(payer, ability, targetCards); + if (canPayListAtOnce()) { // This is used by reveal. Without it when opponent would reveal hand, you'll get N message boxes. + for (Card c: targetCards) { + lkiList.add(CardUtil.getLKICopy(c)); + } cardList.addAll(doListPayment(ability, targetCards)); - handleChangeZoneTrigger(ability); - return true; + } else { + for (Card c: targetCards) { + executePayment(ability, c); + } } - for (Card c: targetCards) { - executePayment(ability, c); - } - handleChangeZoneTrigger(ability); + handleChangeZoneTrigger(payer, ability, targetCards); return true; } @@ -152,15 +157,19 @@ public abstract class CostPartWithList extends CostPart { */ public abstract String getHashForLKIList(); public abstract String getHashForCardList(); - + @Override public boolean payAsDecided(Player ai, PaymentDecision decision, SpellAbility ability) { - executePayment(ability, decision.cards); + executePayment(ai, ability, decision.cards); reportPaidCardsTo(ability); return true; } - protected void handleChangeZoneTrigger(SpellAbility ability) { + protected void handleBeforePayment(Player ai, SpellAbility ability, CardCollectionView targetCards) { + + } + + protected void handleChangeZoneTrigger(Player payer, SpellAbility ability, CardCollectionView targetCards) { if (table.isEmpty()) { return; } @@ -168,7 +177,7 @@ public abstract class CostPartWithList extends CostPart { // copy table because the original get cleaned after the cost is done final CardZoneTable copyTable = new CardZoneTable(); copyTable.putAll(table); - copyTable.triggerChangesZoneAll(ability.getHostCard().getGame()); + copyTable.triggerChangesZoneAll(payer.getGame()); } } diff --git a/forge-game/src/main/java/forge/game/cost/CostPayEnergy.java b/forge-game/src/main/java/forge/game/cost/CostPayEnergy.java index 768a7876c15..5402e5e6749 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPayEnergy.java +++ b/forge-game/src/main/java/forge/game/cost/CostPayEnergy.java @@ -21,7 +21,7 @@ import com.google.common.base.Strings; import forge.game.ability.AbilityUtils; import forge.game.card.Card; -import forge.game.card.CounterType; +import forge.game.card.CounterEnumType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -89,7 +89,7 @@ public class CostPayEnergy extends CostPart { } } - return payer.getCounters(CounterType.ENERGY) >= amount; + return payer.getCounters(CounterEnumType.ENERGY) >= amount; } @Override diff --git a/forge-game/src/main/java/forge/game/cost/CostPayment.java b/forge-game/src/main/java/forge/game/cost/CostPayment.java index ca002861212..c4bd1b0446e 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPayment.java +++ b/forge-game/src/main/java/forge/game/cost/CostPayment.java @@ -145,11 +145,17 @@ public class CostPayment extends ManaConversionMatrix { } if (pd == null || !part.payAsDecided(decisionMaker.getPlayer(), pd, ability)) { + if (part instanceof CostPartMana) { + ((CostPartMana)part).setCardMatrix(null); + } game.costPaymentStack.pop(); // cost is resolved return false; } this.paidCostParts.add(part); + if (part instanceof CostPartMana) { + ((CostPartMana)part).setCardMatrix(null); + } game.costPaymentStack.pop(); // cost is resolved } diff --git a/forge-game/src/main/java/forge/game/cost/CostPutCounter.java b/forge-game/src/main/java/forge/game/cost/CostPutCounter.java index 63c64457ea2..d48ab6b73ed 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPutCounter.java +++ b/forge-game/src/main/java/forge/game/cost/CostPutCounter.java @@ -21,6 +21,7 @@ import forge.game.GameEntityCounterTable; import forge.game.card.Card; import forge.game.card.CardLists; import forge.game.card.CardPredicates; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -78,7 +79,7 @@ public class CostPutCounter extends CostPartWithList { @Override public boolean isReusable() { - return counter != CounterType.M1M1; + return !counter.is(CounterEnumType.M1M1); } /* @@ -89,7 +90,7 @@ public class CostPutCounter extends CostPartWithList { @Override public final String toString() { final StringBuilder sb = new StringBuilder(); - if (this.counter == CounterType.LOYALTY) { + if (this.counter.is(CounterEnumType.LOYALTY)) { if (this.getAmount().equals("0")) { sb.append("0"); } @@ -164,7 +165,7 @@ public class CostPutCounter extends CostPartWithList { if (this.payCostFromSource()) { executePayment(ability, ability.getHostCard()); } else { - executePayment(ability, decision.cards); + executePayment(ai, ability, decision.cards); } triggerCounterPutAll(ability); return true; diff --git a/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java b/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java index 7434d7add6f..d03d99cd45b 100644 --- a/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java +++ b/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java @@ -21,6 +21,7 @@ import com.google.common.collect.Lists; import forge.game.card.Card; import forge.game.card.CardLists; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -82,7 +83,7 @@ public class CostRemoveCounter extends CostPartWithList { @Override public final String toString() { final StringBuilder sb = new StringBuilder(); - if (this.counter == CounterType.LOYALTY) { + if (this.counter.is(CounterEnumType.LOYALTY)) { sb.append("-").append(this.getAmount()); } else { sb.append("Remove "); diff --git a/forge-game/src/main/java/forge/game/event/GameEventCombatUpdate.java b/forge-game/src/main/java/forge/game/event/GameEventCombatUpdate.java new file mode 100644 index 00000000000..ce71cea0105 --- /dev/null +++ b/forge-game/src/main/java/forge/game/event/GameEventCombatUpdate.java @@ -0,0 +1,22 @@ +package forge.game.event; + +import forge.game.card.Card; + +import java.util.List; + +public class GameEventCombatUpdate extends GameEvent { + + public final List attackers; + public final List blockers; + + public GameEventCombatUpdate(List attackers, List blockers) { + this.attackers = attackers; + this.blockers = blockers; + } + + @Override + public T visit(IGameEventVisitor visitor) { + return visitor.visit(this); + } + +} diff --git a/forge-game/src/main/java/forge/game/event/IGameEventVisitor.java b/forge-game/src/main/java/forge/game/event/IGameEventVisitor.java index d78e2c4fc66..2ded38b9cc4 100644 --- a/forge-game/src/main/java/forge/game/event/IGameEventVisitor.java +++ b/forge-game/src/main/java/forge/game/event/IGameEventVisitor.java @@ -21,6 +21,7 @@ public interface IGameEventVisitor { T visit(GameEventCardCounters event); T visit(GameEventCombatChanged event); T visit(GameEventCombatEnded event); + T visit(GameEventCombatUpdate event); T visit(GameEventGameFinished event); T visit(GameEventGameOutcome event); T visit(GameEventFlipCoin event); @@ -69,6 +70,7 @@ public interface IGameEventVisitor { public T visit(GameEventCardPhased event) { return null; } public T visit(GameEventCombatChanged event) { return null; } public T visit(GameEventCombatEnded event) { return null; } + public T visit(GameEventCombatUpdate event) { return null; } public T visit(GameEventGameFinished event) { return null; } public T visit(GameEventGameOutcome event) { return null; } public T visit(GameEventFlipCoin event) { return null; } diff --git a/forge-game/src/main/java/forge/game/keyword/Companion.java b/forge-game/src/main/java/forge/game/keyword/Companion.java new file mode 100644 index 00000000000..f13dc902ef0 --- /dev/null +++ b/forge-game/src/main/java/forge/game/keyword/Companion.java @@ -0,0 +1,44 @@ +package forge.game.keyword; + +public class Companion extends SimpleKeyword { + + private String deckRestriction = null; + private String description = null; + private String specialRules = null; + + public Companion() { } + + @Override + protected void parse(String details) { + String[] splitString = details.split(":"); + int descriptionIndex = splitString.length - 1; + + if (splitString.length < 2) { + System.out.println("Did not parse a long enough value for Companion."); + return; + } + + deckRestriction = splitString[0]; + + if (deckRestriction.equals("Special")) { + specialRules = splitString[1]; + } + description = splitString[descriptionIndex]; + } + + public String getDeckRestriction() { + return deckRestriction; + } + + public boolean hasSpecialRestriction() { + return specialRules != null; + } + + public String getDescription() { + return description; + } + + public String getSpecialRules() { + return specialRules; + } +} diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index 5a6e3c33c6c..afadbc71ff8 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -23,13 +23,14 @@ public enum Keyword { BANDING("Banding", SimpleKeyword.class, true, "Any creatures with banding, and up to one without, can attack in a band. Bands are blocked as a group. If any creatures with banding you control are blocking or being blocked by a creature, you divide that creature's combat damage, not its controller, among any of the creatures it's being blocked by or is blocking."), BATTLE_CRY("Battle cry", SimpleKeyword.class, false, "Whenever this creature attacks, each other attacking creature gets +1/+0 until end of turn."), BESTOW("Bestow", KeywordWithCost.class, false, "If you cast this card for its bestow cost, it's an Aura spell with enchant creature. It becomes a creature again if it's not attached to a creature."), - BLOODTHIRST("Bloodthrist", KeywordWithAmount.class, false, "If an opponent was dealt damage this turn, this creature enters the battlefield with {%d:+1/+1 counter} on it."), + BLOODTHIRST("Bloodthirst", KeywordWithAmount.class, false, "If an opponent was dealt damage this turn, this creature enters the battlefield with {%d:+1/+1 counter} on it."), BUSHIDO("Bushido", KeywordWithAmount.class, false, "Whenever this creature blocks or becomes blocked, it gets +%1$d/+%1$d until end of turn."), BUYBACK("Buyback", KeywordWithCost.class, false, "You may pay an additional %s as you cast this spell. If you do, put it into your hand instead of your graveyard as it resolves."), CASCADE("Cascade", SimpleKeyword.class, false, "When you cast this spell, exile cards from the top of your library until you exile a nonland card that costs less. You may cast it without paying its mana cost. Put the exiled cards on the bottom of your library in a random order."), CHAMPION("Champion", KeywordWithType.class, false, "When this enters the battlefield, sacrifice it unless you exile another %s you control. When this leaves the battlefield, that card returns to the battlefield."), CHANGELING("Changeling", SimpleKeyword.class, true, "This card is every creature type."), CIPHER("Cipher", SimpleKeyword.class, true, "Then you may exile this spell card encoded on a creature you control. Whenever that creature deals combat damage to a player, its controller may cast a copy of the encoded card without paying its mana cost."), + COMPANION("Companion", Companion.class, true, "Reveal your companion from outside the game if your deck meets the companion restriction."), CONSPIRE("Conspire", SimpleKeyword.class, false, "As an additional cost to cast this spell, you may tap two untapped creatures you control that each share a color with it. If you do, copy it."), CONVOKE("Convoke", SimpleKeyword.class, true, "Your creatures can help cast this spell. Each creature you tap while playing this spell reduces its cost by {1} or by one mana of that creature's color."), CREW("Crew", KeywordWithAmount.class, false, "Tap any number of creatures you control with total power %1$d or more: This Vehicle becomes an artifact creature until end of turn."), @@ -90,7 +91,7 @@ public enum Keyword { LIFELINK("Lifelink", SimpleKeyword.class, true, "Damage dealt by this creature also causes its controller to gain that much life."), LIVING_WEAPON("Living weapon", SimpleKeyword.class, true, "When this Equipment enters the battlefield, create a 0/0 black Germ creature token, then attach this to it."), MADNESS("Madness", KeywordWithCost.class, false, "If you discard this card, discard it into exile. When you do, cast it for %s or put it into your graveyard."), - MELEE("Melee", SimpleKeyword.class, false, "Whenever this creature attacks, it gets +1/+1 until end of turn for each opponent you attacked with a creature this combat."), + MELEE("Melee", SimpleKeyword.class, false, "Whenever this creature attacks, it gets +1/+1 until end of turn for each opponent you attacked this combat."), MENTOR("Mentor", SimpleKeyword.class, false, "Whenever this creature attacks, put a +1/+1 counter on target attacking creature with lesser power."), MENACE("Menace", SimpleKeyword.class, true, "This creature can't be blocked except by two or more creatures."), MEGAMORPH("Megamorph", KeywordWithCost.class, false, "You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its megamorph cost and put a +1/+1 counter on it."), @@ -127,7 +128,7 @@ public enum Keyword { SHROUD("Shroud", SimpleKeyword.class, true, "This can't be the target of spells or abilities."), SKULK("Skulk", SimpleKeyword.class, true, "This creature can't be blocked by creatures with greater power."), SCAVENGE("Scavenge", KeywordWithCost.class, false, "%s, Exile this card from your graveyard: Put a number of +1/+1 counters equal to this card's power on target creature. Scavenge only as a sorcery."), - SOULBOND("Souldbond", SimpleKeyword.class, true, "You may pair this creature with another unpaired creature when either enters the battlefield. They remain paired for as long as you control both of them"), + SOULBOND("Soulbond", SimpleKeyword.class, true, "You may pair this creature with another unpaired creature when either enters the battlefield. They remain paired for as long as you control both of them."), SOULSHIFT("Soulshift", KeywordWithAmount.class, false, "When this creature dies, you may return target Spirit card with converted mana cost %d or less from your graveyard to your hand."), SPECTACLE("Spectacle", KeywordWithCost.class, false, "You may cast this spell for its spectacle cost rather than its mana cost if an opponent lost life this turn."), SPLICE("Splice", KeywordWithCostAndType.class, false, "As you cast an %2$s spell, you may reveal this card from your hand and pay its splice cost. If you do, add this card's effects to that spell."), diff --git a/forge-game/src/main/java/forge/game/mana/ManaCostBeingPaid.java b/forge-game/src/main/java/forge/game/mana/ManaCostBeingPaid.java index f3484f6192b..acb70163baf 100644 --- a/forge-game/src/main/java/forge/game/mana/ManaCostBeingPaid.java +++ b/forge-game/src/main/java/forge/game/mana/ManaCostBeingPaid.java @@ -543,6 +543,9 @@ public class ManaCostBeingPaid { if (shard.isSnow() && !mana.isSnow()) { return false; } + if (mana.isRestricted() && !mana.getManaAbility().meetsManaShardRestrictions(shard, mana.getColor())) { + return false; + } byte color = mana.getColor(); return pool.canPayForShardWithColor(shard, color); diff --git a/forge-game/src/main/java/forge/game/mana/ManaPool.java b/forge-game/src/main/java/forge/game/mana/ManaPool.java index e90e6eb9b50..d11afbe776c 100644 --- a/forge-game/src/main/java/forge/game/mana/ManaPool.java +++ b/forge-game/src/main/java/forge/game/mana/ManaPool.java @@ -254,7 +254,7 @@ public class ManaPool extends ManaConversionMatrix implements Iterable { } } if (mana.addsCounters(sa)) { - mana.getManaAbility().createETBCounters(host); + mana.getManaAbility().createETBCounters(host, this.owner); } if (mana.triggersWhenSpent()) { mana.getManaAbility().addTriggersWhenSpent(sa, host); diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index 80d122ed83e..f74e2061a93 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -28,9 +28,9 @@ import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; -import forge.game.card.CounterType; import forge.game.card.CardPredicates.Presets; import forge.game.card.CardZoneTable; +import forge.game.card.CounterEnumType; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.cost.Cost; @@ -59,7 +59,7 @@ import java.util.*; *

* Phase class. *

- * + * * @author Forge * @version $Id: PhaseHandler.java 13001 2012-01-08 12:25:25Z Sloth $ */ @@ -71,7 +71,7 @@ public class PhaseHandler implements java.io.Serializable { private int turn = 0; private final transient Stack extraTurns = new Stack<>(); - private final transient Map> extraPhases = Maps.newEnumMap(PhaseType.class); + private final transient Map> DextraPhases = Maps.newEnumMap(PhaseType.class); private int nUpkeepsThisTurn = 0; private int nUpkeepsThisGame = 0; @@ -151,12 +151,12 @@ public class PhaseHandler implements java.io.Serializable { else { // If the phase that's ending has a stack of additional phases // Take the LIFO one and move to that instead of the normal one - if (extraPhases.containsKey(phase)) { - PhaseType nextPhase = extraPhases.get(phase).pop(); + if (DextraPhases.containsKey(phase)) { + PhaseType nextPhase = DextraPhases.get(phase).removeFirst(); // If no more additional phases are available, remove it from the map // and let the next add, reput the key - if (extraPhases.get(phase).isEmpty()) { - extraPhases.remove(phase); + if (DextraPhases.get(phase).isEmpty()) { + DextraPhases.remove(phase); } setPhase(nextPhase); } @@ -268,7 +268,7 @@ public class PhaseHandler implements java.io.Serializable { // all Saga get Lore counter at the begin of pre combat for (Card c : playerTurn.getCardsIn(ZoneType.Battlefield)) { if (c.getType().hasSubtype("Saga")) { - c.addCounter(CounterType.LORE, 1, null, false, table); + c.addCounter(CounterEnumType.LORE, 1, null, false, table); } } table.triggerCountersPutAll(game); @@ -369,10 +369,23 @@ public class PhaseHandler implements java.io.Serializable { if (numDiscard > 0) { final CardZoneTable table = new CardZoneTable(); + final CardCollection discarded = new CardCollection(); + boolean firstDiscarded = playerTurn.getNumDiscardedThisTurn() == 0; for (Card c : playerTurn.getController().chooseCardsToDiscardToMaximumHandSize(numDiscard)){ - playerTurn.discard(c, null, table); + if (playerTurn.discard(c, null, table) != null) { + discarded.add(c); + } } table.triggerChangesZoneAll(game); + + if (!discarded.isEmpty()) { + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Player, playerTurn); + runParams.put(AbilityKey.Cards, discarded); + runParams.put(AbilityKey.Cause, null); + runParams.put(AbilityKey.FirstTime, firstDiscarded); + game.getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false); + } } // Rule 514.2 @@ -387,7 +400,6 @@ public class PhaseHandler implements java.io.Serializable { game.getEndOfTurn().registerUntilEndCommand(playerTurn); for (Player player : game.getPlayers()) { - player.onCleanupPhase(); player.getController().autoPassCancel(); // autopass won't wrap to next turn } for (Player player : game.getLostPlayers()) { @@ -439,7 +451,7 @@ public class PhaseHandler implements java.io.Serializable { for (Player p : game.getPlayers()) { int burn = p.getManaPool().clearPool(true).size(); - + boolean manaBurns = game.getRules().hasManaBurn(); if (manaBurns) { p.loseLife(burn,true); @@ -488,6 +500,10 @@ public class PhaseHandler implements java.io.Serializable { case CLEANUP: bPreventCombatDamageThisTurn = false; if (!bRepeatCleanup) { + // only call onCleanupPhase when Cleanup is not repeated + for (Player player : game.getPlayers()) { + player.onCleanupPhase(); + } setPlayerTurn(handleNextTurn()); // "Trigger" for begin turn to get around a phase skipping final Map runParams = AbilityKey.newMap(); @@ -736,7 +752,7 @@ public class PhaseHandler implements java.io.Serializable { runParams.put(AbilityKey.DefendingPlayer, combat.getDefenderPlayerByAttacker(a)); game.getTriggerHandler().runTrigger(TriggerType.AttackerBlocked, runParams, false); } - + // Run this trigger once for each blocker for (final Card b : blockers) { @@ -782,11 +798,6 @@ public class PhaseHandler implements java.io.Serializable { // reset mustAttackEntity playerTurn.setMustAttackEntity(null); - for (final Player p1 : game.getPlayers()) { - for (final ZoneType z : Player.ALL_ZONES) { - p1.getZone(z).resetCardsAddedThisTurn(); - } - } for (Player p : game.getPlayers()) { p.clearNextTurn(); } @@ -836,7 +847,7 @@ public class PhaseHandler implements java.io.Serializable { if (nextPlayer.hasKeyword("Skip your next turn.")) { nextPlayer.removeKeyword("Skip your next turn.", false); - if (extraTurn == null) { + if (extraTurn == null) { setPlayerTurn(nextPlayer); } return getNextActivePlayer(); @@ -850,7 +861,7 @@ public class PhaseHandler implements java.io.Serializable { boolean untapTimeVault = nextPlayer.getController().chooseBinary(fakeSA, "Skip a turn to untap a Time Vault?", BinaryChoiceType.UntapTimeVault, false); if (untapTimeVault) { if (vaults.size() > 1) { - Card c = nextPlayer.getController().chooseSingleEntityForEffect(vaults, fakeSA, "Which Time Vault do you want to Untap?"); + Card c = nextPlayer.getController().chooseSingleEntityForEffect(vaults, fakeSA, "Which Time Vault do you want to Untap?", null); if (c != null) crd = c; } @@ -861,7 +872,7 @@ public class PhaseHandler implements java.io.Serializable { return getNextActivePlayer(); } } - + if (extraTurn != null) { if (extraTurn.isSkipUntap()) { nextPlayer.addKeyword("Skip the untap step of this turn."); @@ -918,10 +929,10 @@ public class PhaseHandler implements java.io.Serializable { public final void addExtraPhase(final PhaseType afterPhase, final PhaseType extraPhase) { // 500.8. Some effects can add phases to a turn. They do this by adding the phases directly after the specified phase. // If multiple extra phases are created after the same phase, the most recently created phase will occur first. - if (!extraPhases.containsKey(afterPhase)) { - extraPhases.put(afterPhase, new Stack<>()); + if (!DextraPhases.containsKey(afterPhase)) { + DextraPhases.put(afterPhase, new ArrayDeque<>()); } - extraPhases.get(afterPhase).push(extraPhase); + DextraPhases.get(afterPhase).addFirst(extraPhase); } public final boolean isFirstCombat() { @@ -1015,7 +1026,7 @@ public class PhaseHandler implements java.io.Serializable { triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost); triggerList.triggerChangesZoneAll(game); } - + } loopCount++; } while (loopCount < 999 || !pPlayerPriority.getController().isAI()); @@ -1159,7 +1170,7 @@ public class PhaseHandler implements java.io.Serializable { public final void endTurnByEffect() { endCombat(); - extraPhases.clear(); + DextraPhases.clear(); setPhase(PhaseType.CLEANUP); onPhaseBegin(); } diff --git a/forge-game/src/main/java/forge/game/phase/PhaseType.java b/forge-game/src/main/java/forge/game/phase/PhaseType.java index 0cea056d0ab..a1f5a208a75 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseType.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseType.java @@ -55,6 +55,11 @@ public enum PhaseType { return result; } + public final boolean isCombatPhase() { + return ((ALL_PHASES.indexOf(this) >= ALL_PHASES.indexOf(COMBAT_BEGIN)) + && (ALL_PHASES.indexOf(this) <= ALL_PHASES.indexOf(COMBAT_END))); + } + public final boolean isAfter(final PhaseType phase) { return ALL_PHASES.indexOf(this) > ALL_PHASES.indexOf(phase); } 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 6b317fa3dff..6fe6668b151 100644 --- a/forge-game/src/main/java/forge/game/phase/Untap.java +++ b/forge-game/src/main/java/forge/game/phase/Untap.java @@ -185,7 +185,7 @@ public class Untap extends Phase { } } Card chosen = player.getController().chooseSingleEntityForEffect(cardList, new SpellAbility.EmptySa(ApiType.Untap, null, player), - "Select a card to untap\r\n(Selected:" + restrictUntapped + ")\r\n" + "Remaining cards that can untap: " + remaining); + "Select a card to untap\r\n(Selected:" + restrictUntapped + ")\r\n" + "Remaining cards that can untap: " + remaining, null); if (chosen != null) { for (Entry rest : restrictUntap.entrySet()) { if (chosen.isValid(rest.getKey(), player, null, null)) { 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 93e53b59b3d..55c0df3b019 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -20,10 +20,13 @@ package forge.game.player; import com.google.common.base.Function; import com.google.common.base.Predicates; import com.google.common.collect.*; - import forge.ImageKeys; import forge.LobbyPlayer; +import forge.card.CardStateName; +import forge.card.CardType; import forge.card.MagicColor; +import forge.card.mana.ManaCost; +import forge.card.mana.ManaCostShard; import forge.game.*; import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; @@ -33,11 +36,8 @@ import forge.game.ability.effects.DetachedCardEffect; import forge.game.card.*; import forge.game.card.CardPredicates.Presets; import forge.game.event.*; -import forge.game.keyword.Keyword; -import forge.game.keyword.KeywordCollection; +import forge.game.keyword.*; import forge.game.keyword.KeywordCollection.KeywordCollectionView; -import forge.game.keyword.KeywordInterface; -import forge.game.keyword.KeywordsChange; import forge.game.mana.ManaPool; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -55,10 +55,7 @@ import forge.game.zone.Zone; import forge.game.zone.ZoneType; import forge.item.IPaperCard; import forge.item.PaperCard; -import forge.util.Aggregates; -import forge.util.Lang; -import forge.util.MyRandom; -import forge.util.TextUtil; +import forge.util.*; import forge.util.collect.FCollection; import forge.util.collect.FCollectionView; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -93,6 +90,7 @@ public class Player extends GameEntity implements Comparable { private int landsPlayedLastTurn = 0; private int investigatedThisTurn = 0; private int surveilThisTurn = 0; + private int cycledThisTurn = 0; private int lifeLostThisTurn = 0; private int lifeLostLastTurn = 0; private int lifeGainedThisTurn = 0; @@ -108,6 +106,7 @@ public class Player extends GameEntity implements Comparable { private int numDrawnThisTurn = 0; private int numDrawnThisDrawStep = 0; private int numDiscardedThisTurn = 0; + private int numTokenCreatedThisTurn = 0; private int numCardsInHandStartedThisTurnWith = 0; private final Map> notes = Maps.newHashMap(); @@ -115,7 +114,7 @@ public class Player extends GameEntity implements Comparable { private CardCollection sacrificedThisTurn = new CardCollection(); - private Map countersAddedtoPermThisTurn = Maps.newEnumMap(CounterType.class); + private Map countersAddedtoPermThisTurn = Maps.newHashMap(); /** A list of tokens not in play, but on their way. * This list is kept in order to not break ETB-replacement @@ -162,6 +161,10 @@ public class Player extends GameEntity implements Comparable { private Card blessingEffect = null; private Card keywordEffect = null; + private Map additionalVotes = Maps.newHashMap(); + private Map additionalOptionalVotes = Maps.newHashMap(); + private SortedSet controlVotes = Sets.newTreeSet(); + private final AchievementTracker achievementTracker = new AchievementTracker(); private final PlayerView view; @@ -252,6 +255,8 @@ public class Player extends GameEntity implements Comparable { game.getAction().moveTo(ZoneType.Command, activeScheme, null); game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone); + game.getTriggerHandler().registerActiveTrigger(activeScheme, false); + // Run triggers final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.Scheme, activeScheme); @@ -471,7 +476,7 @@ public class Player extends GameEntity implements Comparable { } public final int loseLife(final int toLose) { - return loseLife(toLose,false); + return loseLife(toLose, false); } public final int loseLife(final int toLose, final boolean manaBurn) { @@ -541,17 +546,17 @@ public class Player extends GameEntity implements Comparable { } public final boolean canPayEnergy(final int energyPayment) { - int cnt = getCounters(CounterType.ENERGY); + int cnt = getCounters(CounterEnumType.ENERGY); return cnt >= energyPayment; } public final int loseEnergy(int lostEnergy) { - int cnt = getCounters(CounterType.ENERGY); + int cnt = getCounters(CounterEnumType.ENERGY); if (lostEnergy > cnt) { return -1; } cnt -= lostEnergy; - this.setCounters(CounterType.ENERGY, cnt, true); + this.setCounters(CounterEnumType.ENERGY, cnt, true); return cnt; } @@ -906,12 +911,8 @@ public class Player extends GameEntity implements Comparable { @Override public int addCounter(CounterType counterType, int n, final Player source, boolean applyMultiplier, boolean fireEvents, GameEntityCounterTable table) { - if (!canReceiveCounters(counterType)) { - return 0; - } - int addAmount = n; - if (addAmount <= 0) { + if (addAmount <= 0 || !canReceiveCounters(counterType)) { // Can't add negative or 0 counters, bail out now return 0; } @@ -932,6 +933,10 @@ public class Player extends GameEntity implements Comparable { default: return 0; } + if (addAmount <= 0) { + // Can't add negative or 0 counters, bail out now + return 0; + } final int oldValue = getCounters(counterType); final int newValue = addAmount + oldValue; @@ -982,6 +987,10 @@ public class Player extends GameEntity implements Comparable { getGame().fireEvent(new GameEventPlayerCounters(this, null, 0, 0)); } + public void setCounters(final CounterEnumType counterType, final Integer num, boolean fireEvents) { + this.setCounters(CounterType.get(counterType), num, fireEvents); + } + public void setCounters(final CounterType counterType, final Integer num, boolean fireEvents) { Integer old = getCounters(counterType); setCounters(counterType, num); @@ -1000,26 +1009,26 @@ public class Player extends GameEntity implements Comparable { // TODO Merge These calls into the primary counter calls public final int getPoisonCounters() { - return getCounters(CounterType.POISON); + return getCounters(CounterEnumType.POISON); } public final void setPoisonCounters(final int num, Card source) { - int oldPoison = getCounters(CounterType.POISON); - setCounters(CounterType.POISON, num, true); + int oldPoison = getCounters(CounterEnumType.POISON); + setCounters(CounterEnumType.POISON, num, true); game.fireEvent(new GameEventPlayerPoisoned(this, source, oldPoison, num)); } public final void addPoisonCounters(final int num, final Card source, GameEntityCounterTable table) { - int oldPoison = getCounters(CounterType.POISON); - addCounter(CounterType.POISON, num, source.getController(), false, true, table); + int oldPoison = getCounters(CounterEnumType.POISON); + addCounter(CounterEnumType.POISON, num, source.getController(), false, true, table); - if (oldPoison != getCounters(CounterType.POISON)) { + if (oldPoison != getCounters(CounterEnumType.POISON)) { game.fireEvent(new GameEventPlayerPoisoned(this, source, oldPoison, num)); } } public final void removePoisonCounters(final int num, final Card source) { - int oldPoison = getCounters(CounterType.POISON); - subtractCounter(CounterType.POISON, num); + int oldPoison = getCounters(CounterEnumType.POISON); + subtractCounter(CounterEnumType.POISON, num); - if (oldPoison != getCounters(CounterType.POISON)) { + if (oldPoison != getCounters(CounterEnumType.POISON)) { game.fireEvent(new GameEventPlayerPoisoned(this, source, oldPoison, num)); } } @@ -1094,7 +1103,7 @@ public class Player extends GameEntity implements Comparable { * @param keyword the keyword to remove. */ public final void removeKeyword(final String keyword) { - removeKeyword(keyword, true); + removeKeyword(keyword, true); } @@ -1105,7 +1114,7 @@ public class Player extends GameEntity implements Comparable { if (ck.removeKeywordfromAdd(keyword)) { keywordRemoved = true; if (!allInstances) { - break; + break; } } } @@ -1151,6 +1160,7 @@ public class Player extends GameEntity implements Comparable { } } view.updateKeywords(this); + updateKeywordCardAbilityText(); } public final KeywordCollectionView getKeywords() { @@ -1221,9 +1231,16 @@ public class Player extends GameEntity implements Comparable { if (kw.startsWith("Protection")) { if (kw.startsWith("Protection:")) { // uses isValid final String characteristic = kw.split(":")[1]; - final String[] characteristics = characteristic.split(","); - if (source.isValid(characteristics, this, null, null)) { - return true; + if (characteristic.startsWith("Player")) { + // Protection:PlayerUID + if (source.getController().isValid(characteristic, this, null, null)) { + return true; + } + } else { + final String[] characteristics = characteristic.split(","); + if (source.isValid(characteristics, this, null, null)) { + return true; + } } } else if (kw.equals("Protection from everything")) { return true; @@ -1265,7 +1282,7 @@ public class Player extends GameEntity implements Comparable { } public final CardCollectionView drawCard() { - return drawCards(1); + return drawCards(1, null); } public void surveil(int num, SpellAbility cause) { @@ -1335,8 +1352,11 @@ public class Player extends GameEntity implements Comparable { } public final CardCollectionView drawCards(final int n) { + return drawCards(n, null); + } + public final CardCollectionView drawCards(final int n, SpellAbility cause) { final CardCollection drawn = new CardCollection(); - final CardCollection toReveal = new CardCollection(); + final Map toReveal = Maps.newHashMap(); // Replacement effects final Map repRunParams = AbilityKey.mapFromAffected(this); @@ -1353,11 +1373,14 @@ public class Player extends GameEntity implements Comparable { if (gameStarted && !canDraw()) { return drawn; } - drawn.addAll(doDraw(toReveal)); + drawn.addAll(doDraw(toReveal, cause)); } - if (toReveal.size() > 1) { - // reveal multiple drawn cards when playing with the top of the library revealed - game.getAction().reveal(toReveal, this, true, "Revealing cards drawn from "); + + // reveal multiple drawn cards when playing with the top of the library revealed + for (Map.Entry e : toReveal.entrySet()) { + if (e.getValue().size() > 1) { + game.getAction().revealTo(e.getValue(), e.getKey(), "Revealing cards drawn from "); + } } return drawn; } @@ -1365,31 +1388,36 @@ public class Player extends GameEntity implements Comparable { /** * @return a CardCollectionView of cards actually drawn */ - private CardCollectionView doDraw(CardCollection revealed) { + private CardCollectionView doDraw(Map revealed, SpellAbility cause) { final CardCollection drawn = new CardCollection(); final PlayerZone library = getZone(ZoneType.Library); // Replacement effects - if (game.getReplacementHandler().run(ReplacementType.Draw, AbilityKey.mapFromAffected(this)) != ReplacementResult.NotReplaced) { + Map repParams = AbilityKey.mapFromAffected(this); + repParams.put(AbilityKey.Cause, cause); + if (game.getReplacementHandler().run(ReplacementType.Draw, repParams) != ReplacementResult.NotReplaced) { return drawn; } if (!library.isEmpty()) { Card c = library.get(0); - boolean topCardRevealed = false; - for (Player p : this.getAllOtherPlayers()) { + List pList = Lists.newArrayList(); + + for (Player p : getAllOtherPlayers()) { if (c.mayPlayerLook(p)) { - topCardRevealed = true; - break; + pList.add(p); } } - c = game.getAction().moveToHand(c, null); + c = game.getAction().moveToHand(c, cause); drawn.add(c); - if (topCardRevealed) { - revealed.add(c); + for(Player p : pList) { + if (!revealed.containsKey(p)) { + revealed.put(p, new CardCollection()); + } + revealed.get(p).add(c); } setLastDrawnCard(c); @@ -1493,6 +1521,7 @@ public class Player extends GameEntity implements Comparable { cl.addAll(getZone(ZoneType.Library).getCardsPlayerCanActivate(this)); if (includeCommandZone) { cl.addAll(getZone(ZoneType.Command).getCardsPlayerCanActivate(this)); + cl.addAll(getZone(ZoneType.Sideboard).getCardsPlayerCanActivate(this)); } //External activatables from all opponents @@ -1597,6 +1626,22 @@ public class Player extends GameEntity implements Comparable { return newCard; } + public final int getNumTokensCreatedThisTurn() { + return numTokenCreatedThisTurn; + } + + public final void addTokensCreatedThisTurn() { + numTokenCreatedThisTurn++; + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Player, this); + runParams.put(AbilityKey.Num, numTokenCreatedThisTurn); + game.getTriggerHandler().runTrigger(TriggerType.TokenCreated, runParams, false); + } + + public final void resetNumTokenCreatedThisTurn() { + numTokenCreatedThisTurn = 0; + } + public final int getNumDiscardedThisTurn() { return numDiscardedThisTurn; } @@ -1624,11 +1669,32 @@ public class Player extends GameEntity implements Comparable { return notes.get(notedFor); } - public final CardCollectionView mill(final int n, final ZoneType destination, + public final CardCollectionView mill(int n, final ZoneType destination, final boolean bottom, SpellAbility sa, CardZoneTable table) { final CardCollectionView lib = getCardsIn(ZoneType.Library); final CardCollection milled = new CardCollection(); + // Replacement effects + final Map repRunParams = AbilityKey.mapFromAffected(this); + repRunParams.put(AbilityKey.Number, n); + + if (destination == ZoneType.Graveyard && !bottom) { + switch (getGame().getReplacementHandler().run(ReplacementType.Mill, repRunParams)) { + case NotReplaced: + break; + case Updated: + // check if this is still the affected player + if (this.equals(repRunParams.get(AbilityKey.Affected))) { + n = (int) repRunParams.get(AbilityKey.Number); + } else { + return milled; + } + break; + default: + return milled; + } + } + final int max = Math.min(n, lib.size()); for (int i = 0; i < max; i++) { @@ -1844,14 +1910,14 @@ public class Player extends GameEntity implements Comparable { } public boolean hasTappedLandForManaThisTurn() { - return tappedLandForManaThisTurn; - } + return tappedLandForManaThisTurn; + } - public void setTappedLandForManaThisTurn(boolean tappedLandForManaThisTurn) { - this.tappedLandForManaThisTurn = tappedLandForManaThisTurn; - } + public void setTappedLandForManaThisTurn(boolean tappedLandForManaThisTurn) { + this.tappedLandForManaThisTurn = tappedLandForManaThisTurn; + } - public final boolean getActivateLoyaltyAbilityThisTurn() { + public final boolean getActivateLoyaltyAbilityThisTurn() { return activateLoyaltyAbilityThisTurn; } public final void setActivateLoyaltyAbilityThisTurn(final boolean b) { @@ -1957,7 +2023,7 @@ public class Player extends GameEntity implements Comparable { } // Rule 704.5c - If a player has ten or more poison counters, he or she loses the game. - if (getCounters(CounterType.POISON) >= 10) { + if (getCounters(CounterEnumType.POISON) >= 10) { return loseConditionMet(GameLossReason.Poisoned, null); } @@ -2189,7 +2255,7 @@ public class Player extends GameEntity implements Comparable { final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.Player, this); runParams.put(AbilityKey.Num, investigatedThisTurn); - game.getTriggerHandler().runTrigger(TriggerType.Investigated, runParams,false); + game.getTriggerHandler().runTrigger(TriggerType.Investigated, runParams, false); } public final void resetInvestigatedThisTurn() { investigatedThisTurn = 0; @@ -2442,6 +2508,7 @@ public class Player extends GameEntity implements Comparable { resetPreventNextDamageWithEffect(); resetNumDrawnThisTurn(); resetNumDiscardedThisTurn(); + resetNumTokenCreatedThisTurn(); setNumCardsInHandStartedThisTurnWith(getCardsIn(ZoneType.Hand).size()); clearCreaturesAttackedThisTurn(); setActivateLoyaltyAbilityThisTurn(false); @@ -2450,6 +2517,7 @@ public class Player extends GameEntity implements Comparable { resetLandsPlayedThisTurn(); resetInvestigatedThisTurn(); resetSurveilThisTurn(); + resetCycledThisTurn(); resetSacrificedThisTurn(); resetCounterToPermThisTurn(); clearAssignedDamage(); @@ -2521,6 +2589,10 @@ public class Player extends GameEntity implements Comparable { } public boolean isSkippingCombat() { + if (hasLost()) { + return true; + } + if (hasKeyword("Skip your next combat phase.")) { return true; } @@ -2582,7 +2654,7 @@ public class Player extends GameEntity implements Comparable { //Run PlaneswalkedTo triggers here. final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.Cards, currentPlanes); - game.getTriggerHandler().runTrigger(TriggerType.PlaneswalkedTo, runParams,false); + game.getTriggerHandler().runTrigger(TriggerType.PlaneswalkedTo, runParams, false); view.updateCurrentPlaneName(currentPlanes.toString().replaceAll(" \\(.*","").replace("[","")); } @@ -2593,7 +2665,7 @@ public class Player extends GameEntity implements Comparable { final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.Cards, new CardCollection(currentPlanes)); - game.getTriggerHandler().runTrigger(TriggerType.PlaneswalkedFrom, runParams,false); + game.getTriggerHandler().runTrigger(TriggerType.PlaneswalkedFrom, runParams, false); for (final Card plane : currentPlanes) { //game.getZoneOf(plane).remove(plane); @@ -2686,7 +2758,7 @@ public class Player extends GameEntity implements Comparable { return commanders; } public void setCommanders(List commanders0) { - if (commanders0 == commanders) { return; } + if (commanders0 == commanders) { return; } commanders = commanders0; view.updateCommander(this); } @@ -2830,6 +2902,122 @@ public class Player extends GameEntity implements Comparable { } } + public boolean allCardsUniqueManaSymbols() { + for (final Card c : getCardsIn(ZoneType.Library)) { + Set cardStateNames = c.isSplitCard() ? EnumSet.of(CardStateName.LeftSplit, CardStateName.RightSplit) : EnumSet.of(CardStateName.Original); + Set coloredManaSymbols = new HashSet<>(); + Set genericManaSymbols = new HashSet<>(); + + for (final CardStateName cardStateName : cardStateNames) { + final ManaCost manaCost = c.getState(cardStateName).getManaCost(); + for (final ManaCostShard manaSymbol : manaCost) { + if (!coloredManaSymbols.add(manaSymbol)) { + return false; + } + } + int generic = manaCost.getGenericCost(); + if (generic > 0 || manaCost.getCMC() == 0) { + if (!genericManaSymbols.add(Integer.valueOf(generic))) { + return false; + } + } + } + } + return true; + } + + public Card assignCompanion(Game game, PlayerController player) { + List legalCompanions = Lists.newArrayList(); + + boolean uniqueNames = true; + Set cardNames = new HashSet<>(); + Set cardTypes = EnumSet.allOf(CardType.CoreType.class); + final CardCollection nonLandInDeck = CardLists.getNotType(getCardsIn(ZoneType.Library), "Land"); + for (final Card c : nonLandInDeck) { + if (uniqueNames) { + if (cardNames.contains(c.getName())) { + uniqueNames = false; + } else { + cardNames.add(c.getName()); + } + } + + cardTypes.retainAll((Collection) c.getPaperCard().getRules().getType().getCoreTypes()); + } + + int deckSize = getCardsIn(ZoneType.Library).size(); + int minSize = game.getMatch().getRules().getGameType().getDeckFormat().getMainRange().getMinimum(); + + for (final Card c : getCardsIn(ZoneType.Sideboard)) { + for (KeywordInterface inst : c.getKeywords()) { + if (!(inst instanceof Companion)) { + continue; + } + + Companion kwInstance = (Companion) inst; + if (kwInstance.hasSpecialRestriction()) { + String specialRules = kwInstance.getSpecialRules(); + if (specialRules.equals("UniqueNames")) { + if (uniqueNames) { + legalCompanions.add(c); + } + } else if (specialRules.equals("UniqueManaSymbols")) { + if (this.allCardsUniqueManaSymbols()) { + legalCompanions.add(c); + } + } else if (specialRules.equals("DeckSizePlus20")) { + // +20 deck size to min deck size + if (deckSize >= minSize + 20) { + legalCompanions.add(c); + } + } else if (specialRules.equals("SharesCardType")) { + // Shares card type + if (!cardTypes.isEmpty()) { + legalCompanions.add(c); + } + } + + } else { + String restriction = kwInstance.getDeckRestriction(); + if (deckMatchesDeckRestriction(c, restriction)) { + legalCompanions.add(c); + } + } + } + } + + if (legalCompanions.isEmpty()) { + return null; + } + + CardCollectionView view = CardCollection.getView(legalCompanions); + + SpellAbility fakeSa = new SpellAbility.EmptySa(ApiType.CompanionChoose, null, this); + return controller.chooseSingleEntityForEffect(view, fakeSa, Localizer.getInstance().getMessage("lblChooseACompanion"), true, null); + } + + public boolean deckMatchesDeckRestriction(Card source, String restriction) { + for (final Card c : getCardsIn(ZoneType.Library)) { + if (!c.isValid(restriction.split(","), this, source, null)) { + return false; + } + } + return true; + } + + public static DetachedCardEffect createCompanionEffect(Game game, Card companion) { + final String name = Lang.getPossesive(companion.getName()) + " Companion Effect"; + DetachedCardEffect eff = new DetachedCardEffect(companion, name); + + String addToHandAbility = "Mode$ Continuous | EffectZone$ Command | Affected$ Card.YouOwn+EffectSource | AffectedZone$ Command | AddAbility$ MoveToHand"; + String moveToHand = "ST$ ChangeZone | Cost$ 3 | Defined$ Self | Origin$ Command | Destination$ Hand | ActivationZone$ Command | SpellDescription$ Companion - Put CARDNAME in to your hand"; + eff.setSVar("MoveToHand", moveToHand); + eff.addStaticAbility(addToHandAbility); + + // TODO Probably remove this effect when the moved to hand + return eff; + } + public static DetachedCardEffect createCommanderEffect(Game game, Card commander) { final String name = Lang.getPossesive(commander.getName()) + " Commander Effect"; DetachedCardEffect eff = new DetachedCardEffect(commander, name); @@ -2908,7 +3096,7 @@ public class Player extends GameEntity implements Comparable { public void createMonarchEffect() { final PlayerZone com = getZone(ZoneType.Command); if (monarchEffect == null) { - monarchEffect = new Card(game.nextCardId(), null, false, game); + monarchEffect = new Card(game.nextCardId(), null, game); monarchEffect.setOwner(this); monarchEffect.setImageKey("t:monarch"); monarchEffect.setName("The Monarch"); @@ -2948,7 +3136,49 @@ public class Player extends GameEntity implements Comparable { this.updateZoneForView(com); } } - + public void updateKeywordCardAbilityText() { + if(getKeywordCard() == null) + return; + final PlayerZone com = getZone(ZoneType.Command); + keywordEffect.setText(""); + keywordEffect.updateAbilityTextForView(); + boolean headerAdded = false; + StringBuilder kw = new StringBuilder(); + for(String k : keywords) { + if(!headerAdded) { + headerAdded = true; + kw.append(this.getName()).append(" has: \n"); + } + kw.append(k).append("\n"); + } + if(!kw.toString().isEmpty()) { + keywordEffect.setText(trimKeywords(kw.toString())); + keywordEffect.updateAbilityTextForView(); + } + this.updateZoneForView(com); + } + public String trimKeywords(String keywordTexts) { + keywordTexts = TextUtil.fastReplace(keywordTexts,":Card.named", " from "); + keywordTexts = TextUtil.fastReplace(keywordTexts, ":Card.Black:", " from "); + keywordTexts = TextUtil.fastReplace(keywordTexts, ":Card.Blue:", " from "); + keywordTexts = TextUtil.fastReplace(keywordTexts, ":Card.Red:", " from "); + keywordTexts = TextUtil.fastReplace(keywordTexts, ":Card.Green:", " from "); + keywordTexts = TextUtil.fastReplace(keywordTexts, ":Card.White:", " from "); + keywordTexts = TextUtil.fastReplace(keywordTexts, ":Card.MonoColor:", " from "); + keywordTexts = TextUtil.fastReplace(keywordTexts, ":Card.MultiColor:", " from "); + keywordTexts = TextUtil.fastReplace(keywordTexts, ":Card.Colorless:", " from "); + return keywordTexts; + } + public void checkKeywordCard() { + if (keywordEffect == null) + return; + final PlayerZone com = getZone(ZoneType.Command); + if (keywordEffect.getAbilityText().isEmpty()) { + com.remove(keywordEffect); + this.updateZoneForView(com); + keywordEffect = null; + } + } public boolean hasBlessing() { return blessingEffect != null; } @@ -2962,7 +3192,7 @@ public class Player extends GameEntity implements Comparable { final PlayerZone com = getZone(ZoneType.Command); if(bless) { - blessingEffect = new Card(game.nextCardId(), null, false, game); + blessingEffect = new Card(game.nextCardId(), null, game); blessingEffect.setOwner(this); blessingEffect.setImageKey("t:blessing"); blessingEffect.setName("City's Blessing"); @@ -2995,6 +3225,9 @@ public class Player extends GameEntity implements Comparable { } public final void clearNextTurn() { + for (final PlayerZone pz : zones.values()) { + pz.resetCardsAddedThisTurn(); + } resetProwl(); setSpellsCastLastTurn(getSpellsCastThisTurn()); resetSpellsCastThisTurn(); @@ -3053,7 +3286,7 @@ public class Player extends GameEntity implements Comparable { final PlayerZone com = getZone(ZoneType.Command); - keywordEffect = new Card(game.nextCardId(), null, false, game); + keywordEffect = new Card(game.nextCardId(), null, game); keywordEffect.setImmutable(true); keywordEffect.setOwner(this); keywordEffect.setName("Keyword Effects"); @@ -3067,4 +3300,104 @@ public class Player extends GameEntity implements Comparable { this.updateZoneForView(com); return keywordEffect; } + + public void addAdditionalVote(long timestamp, int value) { + additionalVotes.put(timestamp, value); + getView().updateAdditionalVote(this); + getGame().fireEvent(new GameEventPlayerStatsChanged(this, false)); + } + + public void removeAdditionalVote(long timestamp) { + if (additionalVotes.remove(timestamp) != null) { + getView().updateAdditionalVote(this); + getGame().fireEvent(new GameEventPlayerStatsChanged(this, false)); + } + } + + public int getAdditionalVotesAmount() { + int value = 0; + for (Integer i : additionalVotes.values()) { + value += i; + } + return value; + } + + public void addAdditionalOptionalVote(long timestamp, int value) { + additionalOptionalVotes.put(timestamp, value); + getView().updateOptionalAdditionalVote(this); + getGame().fireEvent(new GameEventPlayerStatsChanged(this, false)); + } + public void removeAdditionalOptionalVote(long timestamp) { + if (additionalOptionalVotes.remove(timestamp) != null) { + getView().updateOptionalAdditionalVote(this); + getGame().fireEvent(new GameEventPlayerStatsChanged(this, false)); + } + } + + public int getAdditionalOptionalVotesAmount() { + int value = 0; + for (Integer i : additionalOptionalVotes.values()) { + value += i; + } + return value; + } + + public boolean addControlVote(long timestamp) { + if (controlVotes.add(timestamp)) { + updateControlVote(); + return true; + } + return false; + } + public boolean removeControlVote(long timestamp) { + if (controlVotes.remove(timestamp)) { + updateControlVote(); + return true; + } + return false; + } + + void updateControlVote() { + // need to update all players because it can't know + Player control = getGame().getControlVote(); + for (Player pl : getGame().getPlayers()) { + pl.getView().updateControlVote(pl.equals(control)); + getGame().fireEvent(new GameEventPlayerStatsChanged(pl, false)); + } + } + + public Set getControlVote() { + return controlVotes; + } + + public void setControlVote(Set value) { + controlVotes.clear(); + controlVotes.addAll(value); + updateControlVote(); + } + + public Long getHighestControlVote() { + if (controlVotes.isEmpty()) { + return null; + } + return controlVotes.last(); + } + + public void addCycled(SpellAbility sp) { + cycledThisTurn++; + + Map cycleParams = AbilityKey.mapFromCard(sp.getHostCard()); + cycleParams.put(AbilityKey.Cause, sp); + cycleParams.put(AbilityKey.Player, this); + cycleParams.put(AbilityKey.NumThisTurn, cycledThisTurn); + game.getTriggerHandler().runTrigger(TriggerType.Cycled, cycleParams, false); + } + + public int getCycledThisTurn() { + return cycledThisTurn; + } + + public void resetCycledThisTurn() { + cycledThisTurn = 0; + } } diff --git a/forge-game/src/main/java/forge/game/player/PlayerController.java b/forge-game/src/main/java/forge/game/player/PlayerController.java index 62d57539212..8c8f7312410 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -106,20 +106,23 @@ public abstract class PlayerController { public abstract Pair chooseTarget(SpellAbility sa, List> allTargets); // Q: why is there min/max and optional at once? A: This is to handle cases like 'choose 3 to 5 cards or none at all' - public abstract CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional); + public abstract CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional, Map params); - public final T chooseSingleEntityForEffect(FCollectionView optionList, SpellAbility sa, String title) { return chooseSingleEntityForEffect(optionList, null, sa, title, false, null); } - public final T chooseSingleEntityForEffect(FCollectionView optionList, SpellAbility sa, String title, boolean isOptional) { return chooseSingleEntityForEffect(optionList, null, sa, title, isOptional, null); } - public abstract T chooseSingleEntityForEffect(FCollectionView optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player relatedPlayer); + public final T chooseSingleEntityForEffect(FCollectionView optionList, SpellAbility sa, String title, Map params) { return chooseSingleEntityForEffect(optionList, null, sa, title, false, null, params); } + public final T chooseSingleEntityForEffect(FCollectionView optionList, SpellAbility sa, String title, boolean isOptional, Map params) { return chooseSingleEntityForEffect(optionList, null, sa, title, isOptional, null, params); } + public abstract T chooseSingleEntityForEffect(FCollectionView optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player relatedPlayer, Map params); + + public abstract List chooseSpellAbilitiesForEffect(List spells, SpellAbility sa, String title, int num, Map params); + public abstract SpellAbility chooseSingleSpellForEffect(List spells, SpellAbility sa, String title, Map params); - public abstract List chooseEntitiesForEffect(FCollectionView optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer); + public abstract List chooseEntitiesForEffect(FCollectionView optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer, Map params); public abstract boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message); public abstract boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode bidlife, String string, int bid, Player winner); public abstract boolean confirmStaticApplication(Card hostCard, GameEntity affected, String logic, String message); - public abstract boolean confirmTrigger(WrappedAbility sa, Map triggerParams, boolean isMandatory); + public abstract boolean confirmTrigger(WrappedAbility sa); public abstract Player chooseStartingPlayer(boolean isFirstGame); public abstract CardCollection orderBlockers(Card attacker, CardCollection blockers); @@ -166,7 +169,7 @@ public abstract class PlayerController { return chooseSomeType(kindOfType, sa, validTypes, invalidTypes, false); } - public abstract Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes); + public abstract Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes, Player forPlayer); public abstract boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, String question); public abstract CardCollectionView getCardsToMulligan(Player firstPlayer); @@ -274,5 +277,5 @@ public abstract class PlayerController { public abstract boolean confirmMulliganScry(final Player p); public abstract CardCollection chooseCardsForEffectMultiple(Map validMap, - SpellAbility sa, String title); + SpellAbility sa, String title, boolean isOptional); } diff --git a/forge-game/src/main/java/forge/game/player/PlayerPredicates.java b/forge-game/src/main/java/forge/game/player/PlayerPredicates.java index 01c1d0d35cc..a8068a2e0d7 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerPredicates.java +++ b/forge-game/src/main/java/forge/game/player/PlayerPredicates.java @@ -7,6 +7,7 @@ import com.google.common.base.Predicates; import forge.game.card.Card; import forge.game.card.CardLists; +import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; @@ -102,6 +103,14 @@ public final class PlayerPredicates { } }; } + + public static final Predicate hasCounter(final CounterEnumType type) { + return hasCounter(CounterType.get(type), 1); + } + + public static final Predicate hasCounter(final CounterEnumType type, final int n) { + return hasCounter(CounterType.get(type), n); + } public static final Predicate hasKeyword(final String keyword) { return new Predicate() { diff --git a/forge-game/src/main/java/forge/game/player/PlayerProperty.java b/forge-game/src/main/java/forge/game/player/PlayerProperty.java index 18593cbb8aa..16a68e6bfbe 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerProperty.java +++ b/forge-game/src/main/java/forge/game/player/PlayerProperty.java @@ -1,11 +1,5 @@ package forge.game.player; -import java.util.ArrayList; -import java.util.List; - -import forge.util.TextUtil; -import org.apache.commons.lang3.StringUtils; - import forge.game.Game; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -16,6 +10,11 @@ import forge.game.card.CardPredicates.Presets; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.Expressions; +import forge.util.TextUtil; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; public class PlayerProperty { @@ -207,6 +206,11 @@ public class PlayerProperty { if (!player.isEnchantedBy(source)) { return false; } + } else if (property.equals("EnchantedController")) { + Card enchanting = source.getEnchantingCard(); + if (enchanting != null && !player.equals(enchanting.getController())) { + return false; + } } else if (property.equals("Chosen")) { if (source.getChosenPlayer() == null || !source.getChosenPlayer().equals(player)) { return false; diff --git a/forge-game/src/main/java/forge/game/player/PlayerView.java b/forge-game/src/main/java/forge/game/player/PlayerView.java index 6aa49b215b0..1bf7565454b 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerView.java +++ b/forge-game/src/main/java/forge/game/player/PlayerView.java @@ -20,6 +20,7 @@ import forge.card.MagicColor; import forge.game.GameEntityView; import forge.game.card.Card; import forge.game.card.CardView; +import forge.game.card.CounterEnumType; import forge.game.zone.PlayerZone; import forge.game.zone.ZoneType; import forge.trackable.TrackableCollection; @@ -194,6 +195,9 @@ public class PlayerView extends GameEntityView { } return 0; } + public int getCounters(CounterEnumType counterType) { + return getCounters(CounterType.get(counterType)); + } void updateCounters(Player p) { set(TrackableProperty.Counters, p.getCounters()); } @@ -271,6 +275,27 @@ public class PlayerView extends GameEntityView { set(TrackableProperty.NumDrawnThisTurn, p.getNumDrawnThisTurn()); } + public int getAdditionalVote() { + return get(TrackableProperty.AdditionalVote); + } + public void updateAdditionalVote(Player p) { + set(TrackableProperty.AdditionalVote, p.getAdditionalVotesAmount()); + } + + public int getOptionalAdditionalVote() { + return get(TrackableProperty.OptionalAdditionalVote); + } + public void updateOptionalAdditionalVote(Player p) { + set(TrackableProperty.OptionalAdditionalVote, p.getAdditionalOptionalVotesAmount()); + } + + public boolean getControlVote() { + return get(TrackableProperty.ControlVotes); + } + public void updateControlVote(boolean val) { + set(TrackableProperty.ControlVotes, val); + } + public ImmutableMultiset getKeywords() { return get(TrackableProperty.Keywords); } @@ -443,6 +468,7 @@ public class PlayerView extends GameEntityView { //update flashback zone when graveyard, library, or exile zones updated switch (zone.getZoneType()) { + case Command: case Graveyard: case Library: case Exile: @@ -497,6 +523,19 @@ public class PlayerView extends GameEntityView { details.add(Localizer.getInstance().getMessage("lblCardDrawnThisTurnHas", String.valueOf(getNumDrawnThisTurn()))); details.add(Localizer.getInstance().getMessage("lblDamagepreventionHas", String.valueOf(getPreventNextDamage()))); + int v = getAdditionalVote(); + if (v > 0) { + details.add(Localizer.getInstance().getMessage("lblAdditionalVotes", String.valueOf(v))); + } + v = getOptionalAdditionalVote(); + if (v > 0) { + details.add(Localizer.getInstance().getMessage("lblOptionalAdditionalVotes", String.valueOf(v))); + } + + if (getControlVote()) { + details.add(Localizer.getInstance().getMessage("lblControlsVote")); + } + if (getIsExtraTurn()) { details.add(Localizer.getInstance().getMessage("lblIsExtraTurn")); } diff --git a/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java b/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java index 094b94ad796..11606355bcd 100644 --- a/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java +++ b/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java @@ -135,8 +135,8 @@ public class RegisteredPlayer { start.planes = planes; } if (appliedVariants.contains(GameType.Vanguard) || appliedVariants.contains(GameType.MomirBasic) - || appliedVariants.contains(GameType.MoJhoSto)) { - start.setVanguardAvatars(vanguardAvatar.toFlatList()); + || appliedVariants.contains(GameType.MoJhoSto)) { //fix the crash, if somehow the avatar is null, get it directly from the deck + start.setVanguardAvatars(vanguardAvatar == null ? deck.get(DeckSection.Avatar).toFlatList():vanguardAvatar.toFlatList()); } return start; } diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceAttached.java b/forge-game/src/main/java/forge/game/replacement/ReplaceAttached.java new file mode 100644 index 00000000000..6bdf445fbed --- /dev/null +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceAttached.java @@ -0,0 +1,51 @@ +package forge.game.replacement; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.spellability.SpellAbility; + +import java.util.Map; + +/** + * TODO: Write javadoc for this type. + * + */ +public class ReplaceAttached extends ReplacementEffect { + + /** + * + * TODO: Write javadoc for Constructor. + * @param mapParams   HashMap + * @param host   Card + */ + public ReplaceAttached(final Map mapParams, final Card host, final boolean intrinsic) { + super(mapParams, host, intrinsic); + } + + /* (non-Javadoc) + * @see forge.card.replacement.ReplacementEffect#canReplace(java.util.HashMap) + */ + @Override + public boolean canReplace(Map runParams) { + if (hasParam("ValidCard")) { + if (!matchesValid(runParams.get(AbilityKey.Affected), getParam("ValidCard").split(","), this.getHostCard())) { + return false; + } + } + if (hasParam("ValidTarget")) { + if (!matchesValid(runParams.get(AbilityKey.AttachTarget), getParam("ValidTarget").split(","), this.getHostCard())) { + return false; + } + } + return true; + } + + /* (non-Javadoc) + * @see forge.card.replacement.ReplacementEffect#setReplacingObjects(java.util.HashMap, forge.card.spellability.SpellAbility) + */ + @Override + public void setReplacingObjects(Map runParams, SpellAbility sa) { + sa.setReplacingObject(AbilityKey.Card, runParams.get(AbilityKey.Affected)); + } + +} diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceDamage.java b/forge-game/src/main/java/forge/game/replacement/ReplaceDamage.java index 2d8681ef508..f6eaa9325fe 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplaceDamage.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceDamage.java @@ -20,7 +20,6 @@ package forge.game.replacement; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.card.Card; -import forge.game.card.CardFactoryUtil; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.util.Expressions; @@ -97,12 +96,7 @@ public class ReplaceDamage extends ReplacementEffect { String full = getParam("DamageAmount"); String operator = full.substring(0, 2); String operand = full.substring(2); - int intoperand = 0; - try { - intoperand = Integer.parseInt(operand); - } catch (NumberFormatException e) { - intoperand = CardFactoryUtil.xCount(getHostCard(), getHostCard().getSVar(operand)); - } + int intoperand = AbilityUtils.calculateAmount(getHostCard(), operand, this); if (!Expressions.compare((Integer) runParams.get(AbilityKey.DamageAmount), operator, intoperand)) { return false; diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceDraw.java b/forge-game/src/main/java/forge/game/replacement/ReplaceDraw.java index 9dbaf529cad..c930b352334 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplaceDraw.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceDraw.java @@ -6,17 +6,18 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package forge.game.replacement; +import forge.game.Game; import forge.game.ability.AbilityKey; import forge.game.card.Card; import forge.game.phase.PhaseType; @@ -25,7 +26,7 @@ import forge.game.spellability.SpellAbility; import java.util.Map; -/** +/** * TODO: Write javadoc for this type. * */ @@ -46,16 +47,29 @@ public class ReplaceDraw extends ReplacementEffect { */ @Override public boolean canReplace(Map runParams) { + final Game game = this.getHostCard().getGame(); if (hasParam("ValidPlayer")) { - if (!matchesValid(runParams.get(AbilityKey.Affected), getParam("ValidPlayer").split(","), this.getHostCard())) { + if (!matchesValid(runParams.get(AbilityKey.Affected), getParam("ValidPlayer").split(","), getHostCard())) { return false; } } + + if (hasParam("ValidCause")) { + if (!runParams.containsKey(AbilityKey.Cause)) { + return false; + } + SpellAbility cause = (SpellAbility) runParams.get(AbilityKey.Cause); + if (cause == null) { + return false; + } + if (!matchesValid(cause, getParam("ValidCause").split(","), getHostCard())) { + return false; + } + } + if (hasParam("NotFirstCardInDrawStep")) { final Player p = (Player)runParams.get(AbilityKey.Affected); - if (p.numDrawnThisDrawStep() == 0 - && this.getHostCard().getGame().getPhaseHandler().is(PhaseType.DRAW) - && this.getHostCard().getGame().getPhaseHandler().isPlayerTurn(p)) { + if (p.numDrawnThisDrawStep() == 0 && game.getPhaseHandler().is(PhaseType.DRAW, p)) { return false; } } @@ -71,5 +85,12 @@ public class ReplaceDraw extends ReplacementEffect { @Override public void setReplacingObjects(Map runParams, SpellAbility sa) { sa.setReplacingObject(AbilityKey.Player, runParams.get(AbilityKey.Affected)); + if (runParams.containsKey(AbilityKey.Cause)) { + SpellAbility cause = (SpellAbility) runParams.get(AbilityKey.Cause); + if (cause != null) { + sa.setReplacingObject(AbilityKey.Cause, cause); + sa.setReplacingObject(AbilityKey.Source, cause.getHostCard()); + } + } } } diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceMill.java b/forge-game/src/main/java/forge/game/replacement/ReplaceMill.java new file mode 100644 index 00000000000..e2aa8cf2009 --- /dev/null +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceMill.java @@ -0,0 +1,66 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.game.replacement; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.spellability.SpellAbility; +import java.util.Map; + +/** + * TODO: Write javadoc for this type. + * + */ +public class ReplaceMill extends ReplacementEffect { + + /** + * Instantiates a new replace mill. + * + * @param params the params + * @param host the host + */ + public ReplaceMill(final Map params, final Card host, final boolean intrinsic) { + super(params, host, intrinsic); + } + + /* (non-Javadoc) + * @see forge.card.replacement.ReplacementEffect#canReplace(java.util.HashMap) + */ + @Override + public boolean canReplace(Map runParams) { + + if (hasParam("ValidPlayer")) { + if (!matchesValid(runParams.get(AbilityKey.Affected), getParam("ValidPlayer").split(","), getHostCard())) { + return false; + } + } + + return true; + } + + + + /* (non-Javadoc) + * @see forge.card.replacement.ReplacementEffect#setReplacingObjects(java.util.HashMap, forge.card.spellability.SpellAbility) + */ + @Override + public void setReplacingObjects(Map runParams, SpellAbility sa) { + sa.setReplacingObject(AbilityKey.Player, runParams.get(AbilityKey.Affected)); + sa.setReplacingObject(AbilityKey.Number, runParams.get(AbilityKey.Number)); + } +} diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceProduceMana.java b/forge-game/src/main/java/forge/game/replacement/ReplaceProduceMana.java index 13e8b68f782..5c95623af22 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplaceProduceMana.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceProduceMana.java @@ -1,8 +1,8 @@ package forge.game.replacement; import forge.game.ability.AbilityKey; +import forge.game.ability.AbilityUtils; import forge.game.card.Card; -import forge.game.card.CardFactoryUtil; import forge.game.spellability.SpellAbility; import forge.util.Expressions; @@ -34,7 +34,7 @@ public class ReplaceProduceMana extends ReplacementEffect { //Check for tapping if (!hasParam("NoTapCheck")) { final SpellAbility manaAbility = (SpellAbility) runParams.get(AbilityKey.AbilityMana); - if (manaAbility == null || manaAbility.getRootAbility().getPayCosts() == null || !manaAbility.getRootAbility().getPayCosts().hasTapCost()) { + if (manaAbility == null || !manaAbility.getRootAbility().getPayCosts().hasTapCost()) { return false; } } @@ -43,12 +43,7 @@ public class ReplaceProduceMana extends ReplacementEffect { String full = getParam("ManaAmount"); String operator = full.substring(0, 2); String operand = full.substring(2); - int intoperand = 0; - try { - intoperand = Integer.parseInt(operand); - } catch (NumberFormatException e) { - intoperand = CardFactoryUtil.xCount(getHostCard(), getHostCard().getSVar(operand)); - } + int intoperand = AbilityUtils.calculateAmount(getHostCard(), operand, this); int manaAmount = StringUtils.countMatches((String) runParams.get(AbilityKey.Mana), " ") + 1; if (!Expressions.compare(manaAmount, operator, intoperand)) { return false; diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java b/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java index 2ff6a07d9ec..3e96fd46275 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java @@ -210,13 +210,7 @@ public abstract class ReplacementEffect extends TriggerReplacementBase { this.layer = layer0; } - /** - * To string. - * - * @return a String - */ - @Override - public String toString() { + public String getDescription() { if (hasParam("Description") && !this.isSuppressed()) { String desc = AbilityUtils.applyDescriptionTextChangeEffects(getParam("Description"), this); if (desc.contains("CARDNAME")) { @@ -228,6 +222,16 @@ public abstract class ReplacementEffect extends TriggerReplacementBase { } } + /** + * To string. + * + * @return a String + */ + @Override + public String toString() { + return getHostCard().toString() + " - " + getDescription(); + } + /** {@inheritDoc} */ @Override public final Object clone() { diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java index fb9be6b94f2..ba037dd2d2e 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java @@ -260,7 +260,7 @@ public class ReplacementHandler { chosenRE.setHasRun(false); hasRun.remove(chosenRE); chosenRE.setOtherChoices(null); - String message = chosenRE.toString(); + String message = chosenRE.getDescription(); if ( !StringUtils.isEmpty(message)) if (chosenRE.getHostCard() != null) { message = TextUtil.fastReplace(message, "CARDNAME", chosenRE.getHostCard().getName()); @@ -336,7 +336,7 @@ public class ReplacementHandler { } Card cardForUi = host.getCardForUi(); - String effectDesc = TextUtil.fastReplace(replacementEffect.toString(), "CARDNAME", CardTranslation.getTranslatedName(cardForUi.getName())); + String effectDesc = TextUtil.fastReplace(replacementEffect.getDescription(), "CARDNAME", CardTranslation.getTranslatedName(cardForUi.getName())); final String question = replacementEffect instanceof ReplaceDiscard ? Localizer.getInstance().getMessage("lblApplyCardReplacementEffectToCardConfirm", CardTranslation.getTranslatedName(cardForUi.getName()), runParams.get(AbilityKey.Card).toString(), effectDesc) : Localizer.getInstance().getMessage("lblApplyReplacementEffectOfCardConfirm", CardTranslation.getTranslatedName(cardForUi.getName()), effectDesc); diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementType.java b/forge-game/src/main/java/forge/game/replacement/ReplacementType.java index 9ddaa8dd6d7..c13b3f172d0 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementType.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementType.java @@ -12,6 +12,7 @@ import java.util.Map; */ public enum ReplacementType { AddCounter(ReplaceAddCounter.class), + Attached(ReplaceAttached.class), Counter(ReplaceCounter.class), CreateToken(ReplaceToken.class), DamageDone(ReplaceDamage.class), @@ -21,6 +22,7 @@ public enum ReplacementType { DrawCards(ReplaceDrawCards.class), GainLife(ReplaceGainLife.class), GameLoss(ReplaceGameLoss.class), + Mill(ReplaceMill.class), Moved(ReplaceMoved.class), ProduceMana(ReplaceProduceMana.class), SetInMotion(ReplaceSetInMotion.class), diff --git a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java index da064a4cbd6..ddee3945b53 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java @@ -6,12 +6,12 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -19,9 +19,12 @@ package forge.game.spellability; import com.google.common.collect.Lists; import com.google.common.collect.Maps; + import forge.card.ColorSet; import forge.card.MagicColor; import forge.card.mana.ManaAtom; +import forge.card.mana.ManaCostShard; +import forge.game.Game; import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; import forge.game.card.Card; @@ -34,6 +37,8 @@ import forge.game.replacement.*; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerType; +import forge.game.zone.ZoneType; +import forge.util.Lang; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -46,7 +51,7 @@ import java.util.regex.Pattern; *

* Abstract AbilityMana class. *

- * + * * @author Forge * @version $Id$ */ @@ -78,7 +83,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* Constructor for AbilityMana. *

- * + * * @param sourceCard * a {@link forge.game.card.Card} object. */ @@ -111,7 +116,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* produceMana. *

- * + * * @param produced * a {@link java.lang.String} object. * @param player @@ -169,7 +174,7 @@ public class AbilityManaPart implements java.io.Serializable { * cannotCounterPaidWith. *

* @param saBeingPaid - * + * * @return a {@link java.lang.String} object. */ public boolean cannotCounterPaidWith(SpellAbility saBeingPaid) { @@ -186,7 +191,7 @@ public class AbilityManaPart implements java.io.Serializable { * addKeywords. *

* @param saBeingPaid - * + * * @return a {@link java.lang.String} object. */ public boolean addKeywords(SpellAbility saBeingPaid) { @@ -205,7 +210,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* getKeywords. *

- * + * * @return a {@link java.lang.String} object. */ public String getKeywords() { @@ -217,7 +222,7 @@ public class AbilityManaPart implements java.io.Serializable { * addsCounters. *

* @param saBeingPaid - * + * * @return a {@link java.lang.String} object. */ public boolean addsCounters(SpellAbility saBeingPaid) { @@ -227,10 +232,25 @@ public class AbilityManaPart implements java.io.Serializable { /** * createETBCounters */ - public void createETBCounters(Card c) { + public void createETBCounters(Card c, Player controller) { String[] parse = this.addsCounters.split("_"); // Convert random SVars if there are other cards with this effect if (c.isValid(parse[0], c.getController(), c, null)) { + final Game game = this.sourceCard.getGame(); + final Card eff = new Card(game.nextCardId(), game); + eff.setTimestamp(game.getNextTimestamp()); + eff.setName(sourceCard.getName() + "'s Effect"); + eff.addType("Effect"); + eff.setOwner(controller); + + eff.setImageKey(sourceCard.getImageKey()); + eff.setColor(MagicColor.COLORLESS); + eff.setImmutable(true); + // try to get the SpellAbility from the mana ability + //eff.setEffectSource((SpellAbility)null); + + eff.addRemembered(c); + String abStr = "DB$ PutCounter | Defined$ ReplacedCard | CounterType$ " + parse[1] + " | ETB$ True | CounterNum$ " + parse[2]; @@ -240,15 +260,37 @@ public class AbilityManaPart implements java.io.Serializable { } CardFactoryUtil.setupETBReplacementAbility(sa); - String repeffstr = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield " - + " | Secondary$ True | Description$ CARDNAME" - + " enters the battlefield with " + CounterType.valueOf(parse[1]).getName() + " counters."; + String desc = "It enters the battlefield with "; + desc += Lang.nounWithNumeral(parse[2], CounterType.getType(parse[1]).getName() + " counter"); + desc += " on it."; - ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, c, false); + String repeffstr = "Event$ Moved | ValidCard$ Card.IsRemembered | Destination$ Battlefield | Description$ " + desc; + + ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true); re.setLayer(ReplacementLayer.Other); re.setOverridingAbility(sa); - c.addReplacementEffect(re); + eff.addReplacementEffect(re); + + // Forgot Trigger + String trig = "Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Stack | Destination$ Any | TriggerZones$ Command | Static$ True"; + String forgetEffect = "DB$ Pump | ForgetObjects$ TriggeredCard"; + String exileEffect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile" + + " | ConditionDefined$ Remembered | ConditionPresent$ Card | ConditionCompare$ EQ0"; + + SpellAbility saForget = AbilityFactory.getAbility(forgetEffect, eff); + AbilitySub saExile = (AbilitySub) AbilityFactory.getAbility(exileEffect, eff); + saForget.setSubAbility(saExile); + + final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, eff, true); + parsedTrigger.setOverridingAbility(saForget); + eff.addTrigger(parsedTrigger); + eff.updateStateForView(); + + // TODO: Add targeting to the effect so it knows who it's dealing with + game.getTriggerHandler().suppressMode(TriggerType.ChangesZone); + game.getAction().moveTo(ZoneType.Command, eff, null); + game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone); } } @@ -269,7 +311,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* getManaRestrictions. *

- * + * * @return a {@link java.lang.String} object. */ public String getManaRestrictions() { @@ -280,7 +322,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* meetsManaRestrictions. *

- * + * * @param sa * a {@link forge.game.spellability.SpellAbility} object. * @return a boolean. @@ -296,7 +338,7 @@ public class AbilityManaPart implements java.io.Serializable { if (restriction.equals("nonSpell")) { return !sa.isSpell(); } - + if (restriction.equals("CumulativeUpkeep")) { if (sa.isCumulativeupkeep()) { return true; @@ -323,6 +365,10 @@ public class AbilityManaPart implements java.io.Serializable { if (sa.isValid(restriction, this.getSourceCard().getController(), this.getSourceCard(), null)) { return true; } + + if (restriction.equals("CantPayGenericCosts")) { + return true; + } if (sa.isAbility()) { if (restriction.startsWith("Activated")) { @@ -344,12 +390,60 @@ public class AbilityManaPart implements java.io.Serializable { return false; } + + /** + *

+ * meetsManaShardRestrictions. + *

+ * + * @param shard + * a {@link forge.card.mana.ManaCostShard} object. + * @param color + * the color of mana being paid + * @return a boolean. + */ + public boolean meetsManaShardRestrictions(final ManaCostShard shard, final byte color) { + if (this.manaRestrictions.isEmpty()) { + return true; + } + for (String restriction : this.manaRestrictions.split(",")) { + if (restriction.equals("CantPayGenericCosts")) { + if (shard.isGeneric()) { + if (shard.isOr2Generic() && shard.isColor(color)) { + continue; + } else { + return false; + } + } else { + continue; + } + } + } + return true; + } + + /** + *

+ * meetsSpellAndShardRestrictions. + *

+ * + * @param sa + * a {@link forge.game.spellability.SpellAbility} object. + * @param shard + * a {@link forge.card.mana.ManaCostShard} object. + * @param color + * the color of mana being paid + * @return a boolean. + */ + public boolean meetsSpellAndShardRestrictions(final SpellAbility sa, final ManaCostShard shard, final byte color) { + return this.meetsManaRestrictions(sa) && this.meetsManaShardRestrictions(shard, color); + } /** *

* mana. *

- * + * * @return a {@link java.lang.String} object. */ public final String mana() { @@ -438,7 +532,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* canProduce. *

- * + * * @param s * a {@link java.lang.String} object. * @return a boolean. @@ -468,7 +562,7 @@ public class AbilityManaPart implements java.io.Serializable { *

* isBasic. *

- * + * * @return a boolean. */ public final boolean isBasic() { @@ -541,7 +635,7 @@ public class AbilityManaPart implements java.io.Serializable { public Card getSourceCard() { return sourceCard; } - + public void setSourceCard(final Card host) { sourceCard = host; } diff --git a/forge-game/src/main/java/forge/game/spellability/Spell.java b/forge-game/src/main/java/forge/game/spellability/Spell.java index 2978d406f34..7901469e500 100644 --- a/forge-game/src/main/java/forge/game/spellability/Spell.java +++ b/forge-game/src/main/java/forge/game/spellability/Spell.java @@ -145,10 +145,8 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable return false; } - if (this.getPayCosts() != null) { - if (!CostPayment.canPayAdditionalCosts(this.getPayCosts(), this)) { - return false; - } + if (!CostPayment.canPayAdditionalCosts(this.getPayCosts(), this)) { + return false; } return checkOtherRestrictions(card); 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 75479eaadfc..e5ee795a76d 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -32,6 +32,7 @@ import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardDamageMap; import forge.game.card.CardFactory; +import forge.game.card.CardPredicates; import forge.game.card.CardZoneTable; import forge.game.cost.Cost; import forge.game.cost.CostPart; @@ -46,6 +47,7 @@ import forge.game.trigger.Trigger; import forge.game.trigger.TriggerType; import forge.game.trigger.WrappedAbility; import forge.game.zone.ZoneType; +import forge.util.Aggregates; import forge.util.Expressions; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -130,6 +132,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private final List payingMana = Lists.newArrayList(); private final List paidAbilities = Lists.newArrayList(); + private Integer xManaCostPaid = null; private HashMap paidLists = Maps.newHashMap(); @@ -189,9 +192,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit view0 = new SpellAbilityView(this); } view = view0; - if (hostCard != null && hostCard.getGame() != null) { - hostCard.getGame().addSpellAbility(this); - } } @Override @@ -210,13 +210,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit @Override public void setHostCard(final Card c) { if (hostCard == c) { return; } - Game oldGame = hostCard != null ? hostCard.getGame() : null; - Game newGame = c != null ? c.getGame() : null; super.setHostCard(c); - if (oldGame != newGame) { - if (oldGame != null) { oldGame.removeSpellAbility(this); } - if (newGame != null) { newGame.addSpellAbility(this); } - } if (manaPart != null) { manaPart.setSourceCard(c); @@ -447,6 +441,17 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit payCosts = abCost; } + public boolean costHasX() { + return getPayCosts().hasXInAnyCostPart(); + } + + public boolean costHasManaX() { + if (getPayCosts().hasNoManaCost()) { + return false; + } + return getPayCosts().getCostMana().getAmountOfX() > 0; + } + public SpellAbilityRestriction getRestrictions() { return restrictions; } @@ -603,6 +608,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit resetTriggeringObjects(); resetTriggerRemembered(); + // reset last state when finished resolving + setLastStateBattlefield(CardCollection.EMPTY); + setLastStateGraveyard(CardCollection.EMPTY); + // Clear SVars for (final String store : Card.getStorableSVars()) { final String value = hostCard.getSVar(store); @@ -867,20 +876,15 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit try { clone = (SpellAbility) clone(); clone.id = lki ? id : nextId(); - clone.view = new SpellAbilityView(clone); + clone.view = new SpellAbilityView(clone, lki || host.getGame() == null ? null : host.getGame().getTracker()); - // dont use setHostCard to not trigger the not copied parts yet + // don't use setHostCard to not trigger the not copied parts yet copyHelper(clone, host); - if (!lki && host != null && host.getGame() != null) { - host.getGame().addSpellAbility(clone); - } clone.triggeringObjects = AbilityKey.newMap(this.triggeringObjects); - if (getPayCosts() != null) { - clone.setPayCosts(getPayCosts().copy()); - } + clone.setPayCosts(getPayCosts().copy()); if (manaPart != null) { clone.manaPart = new AbilityManaPart(host, mapParams); } @@ -897,6 +901,8 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit clone.changeZoneTable.putAll(changeZoneTable); } + clone.setPaidHash(Maps.newHashMap(getPaidHash())); + // clear maps for copy, the values will be added later clone.additionalAbilities = Maps.newHashMap(); clone.additionalAbilityLists = Maps.newHashMap(); @@ -992,7 +998,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } public boolean isMandatory() { - return false; + return isTrigger() && !isOptionalTrigger(); } public final boolean canTarget(final GameObject entity) { @@ -1081,12 +1087,29 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } if (hasParam("MaxTotalTargetCMC") && entity instanceof Card) { - final Card c = (Card) entity; - if (c.getCMC() > tr.getMaxTotalCMC(c, this)) { + int soFar = Aggregates.sum(getTargets().getTargetCards(), CardPredicates.Accessors.fnGetCmc); + // only add if it isn't already targeting + if (!isTargeting(entity)) { + final Card c = (Card) entity; + soFar += c.getCMC(); + } + + if (soFar > tr.getMaxTotalCMC(getHostCard(), this)) { return false; } } + if (tr.isSameController()) { + Player newController; + if (entity instanceof Card) { + newController = ((Card) entity).getController(); + for (final Card c : targetChosen.getTargetCards()) { + if (entity != c && !c.getController().equals(newController)) + return false; + } + } + } + if (tr.isDifferentControllers()) { Player newController; if (entity instanceof Card) { @@ -1299,6 +1322,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit String announce = getParam("Announce"); if (StringUtils.isBlank(announce)) { mapParams.put("Announce", variable); + originalMapParams.put("Announce", variable); return; } String[] announcedOnes = TextUtil.split(announce, ','); @@ -1308,6 +1332,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } } mapParams.put("Announce", announce + ";" + variable); + originalMapParams.put("Announce", announce + ";" + variable); } public boolean isXCost() { @@ -1400,16 +1425,15 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit int maxTargets = getTargetRestrictions().getMaxTargets(hostCard, this); int numTargets = getTargets().getNumTargeted(); - if (maxTargets == 0 && this.getPayCosts() != null - && this.getPayCosts().hasSpecificCostType(CostRemoveCounter.class) - && this.hasSVar(this.getParam("TargetMax")) - && this.getSVar(this.getParam("TargetMax")).startsWith("Count$CardCounters") - && this.getHostCard() != null && this.getHostCard().hasSVar("CostCountersRemoved")) { + if (maxTargets == 0 && getPayCosts().hasSpecificCostType(CostRemoveCounter.class) + && hasSVar(getParam("TargetMax")) + && getSVar(getParam("TargetMax")).startsWith("Count$CardCounters") + && getHostCard() != null && getHostCard().hasSVar("CostCountersRemoved")) { // TODO: Current AI implementation removes the counters during payment before the // ability is added to stack, resulting in maxTargets=0 at this point. We are // assuming here that the AI logic specified a legal number, and that number ended // up being in CostCountersRemoved that is created on the card during payment. - maxTargets = Integer.parseInt(this.getHostCard().getSVar("CostCountersRemoved")); + maxTargets = Integer.parseInt(getHostCard().getSVar("CostCountersRemoved")); } return minTargets <= numTargets && maxTargets >= numTargets; @@ -1753,9 +1777,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit targetRestrictions.applyTargetTextChanges(this); } - if (getPayCosts() != null) { - getPayCosts().applyTextChangeEffects(this); - } + getPayCosts().applyTextChangeEffects(this); stackDescription = AbilityUtils.applyDescriptionTextChangeEffects(originalStackDescription, this); description = AbilityUtils.applyDescriptionTextChangeEffects(originalDescription, this); @@ -1961,4 +1983,11 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public void setAlternativeCost(AlternativeCost ac) { altCost = ac; } + + public Integer getXManaCostPaid() { + return xManaCostPaid; + } + public void setXManaCostPaid(final Integer n) { + xManaCostPaid = n; + } } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityCondition.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityCondition.java index f6f5954cdc7..219aceed181 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityCondition.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityCondition.java @@ -197,6 +197,10 @@ public class SpellAbilityCondition extends SpellAbilityVariables { this.setManaSpent(params.get("ConditionManaSpent")); } + if (params.containsKey("ConditionManaNotSpent")) { + this.setManaNotSpent(params.get("ConditionManaNotSpent")); + } + if (params.containsKey("ConditionCheckSVar")) { this.setSvarToCheck(params.get("ConditionCheckSVar")); } @@ -343,15 +347,8 @@ public class SpellAbilityCondition extends SpellAbilityVariables { list = CardLists.getValidCards(list, this.getIsPresent().split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa); - int right; final String rightString = this.getPresentCompare().substring(2); - try { // If this is an Integer, just parse it - right = Integer.parseInt(rightString); - } catch (final NumberFormatException e) { // Otherwise, grab it from - // the - // SVar - right = CardFactoryUtil.xCount(host, host.getSVar(rightString)); - } + int right = AbilityUtils.calculateAmount(host, rightString, sa); final int left = list.size(); @@ -432,10 +429,21 @@ public class SpellAbilityCondition extends SpellAbilityVariables { } } - if (StringUtils.isNotEmpty(this.getManaSpent())) { - byte manaSpent = MagicColor.fromName(getManaSpent()); // they always check for single color - if( 0 == (manaSpent & sa.getHostCard().getColorsPaid())) // no match of colors + if (StringUtils.isNotEmpty(getManaSpent())) { + for (String s : getManaSpent().split(" ")) { + byte manaSpent = MagicColor.fromName(s); + if( 0 == (manaSpent & sa.getHostCard().getColorsPaid())) // no match of colors + return false; + } + } + if (StringUtils.isNotEmpty(getManaNotSpent())) { + byte toPay = 0; + for (String s : getManaNotSpent().split(" ")) { + toPay |= MagicColor.fromName(s); + } + if (toPay == (toPay & sa.getHostCard().getColorsPaid())) { return false; + } } if (this.getsVarToCheck() != null) { diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java index d4244588f4f..d718f6c9be4 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java @@ -17,18 +17,11 @@ */ package forge.game.spellability; -import java.util.List; -import java.util.Map; - import forge.game.Game; import forge.game.GameType; import forge.game.ability.AbilityUtils; -import forge.game.card.Card; -import forge.game.card.CardCollectionView; -import forge.game.card.CardFactoryUtil; -import forge.game.card.CardLists; -import forge.game.card.CardPlayOption; -import forge.game.card.CardUtil; +import forge.game.card.*; +import forge.game.combat.Combat; import forge.game.cost.IndividualCostPaymentInstance; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -36,6 +29,9 @@ import forge.game.zone.Zone; import forge.game.zone.ZoneType; import forge.util.Expressions; +import java.util.List; +import java.util.Map; + /** *

* SpellAbilityRestriction class. @@ -114,20 +110,8 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { this.setOpponentTurn(true); } - if (params.containsKey("AnyPlayer")) { - this.setAnyPlayer(true); - } - - if (params.containsKey("AnyOpponent")) { - this.setOpponentOnly(true); - } - - if (params.containsKey("EnchantedControllerActivator")) { - this.setEnchantedControllerOnly(true); - } - - if (params.containsKey("OwnerOnly")) { - this.setOwnerOnly(true); + if (params.containsKey("Activator")) { + this.setActivator(params.get("Activator")); } if (params.containsKey("ActivationLimit")) { @@ -326,34 +310,19 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { */ public final boolean checkActivatorRestrictions(final Card c, final SpellAbility sa) { Player activator = sa.getActivatingPlayer(); - if (this.isAnyPlayer()) { - return true; - } - - if (this.isOwnerOnly()) { - return activator.equals(c.getOwner()); - } - - if (activator.equals(c.getController()) && !this.isOpponentOnly() && !isEnchantedControllerOnly()) { - return true; - } - - if (activator.isOpponentOf(c.getController()) && this.isOpponentOnly()) { - return true; - } - - if (c.getEnchantingCard() != null && activator.equals(c.getEnchantingCard().getController()) && this.isEnchantedControllerOnly()) { - return true; - } + final Game game = activator.getGame(); + final Combat combat = game.getPhaseHandler().getCombat(); if (sa.isSpell()) { + // Spells should always default to "controller" but use mayPlay check. final CardPlayOption o = c.mayPlay(sa.getMayPlay()); if (o != null && o.getPlayer() == activator) { return true; } } - - return false; + + String validPlayer = this.getActivator(); + return activator.isValid(validPlayer, c.getController(), c, sa); } public final boolean checkOtherRestrictions(final Card c, final SpellAbility sa, final Player activator) { 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 15dd3573998..f0fe4a740b4 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java @@ -83,7 +83,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { // private ArrayList payingMana = new ArrayList(); // private ArrayList paidAbilities = new // ArrayList(); - private int xManaPaid = 0; + private Integer xManaPaid = null; // Other Paid things private final HashMap paidHash; @@ -116,7 +116,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { splicedCards = sa.getSplicedCards(); // TODO getXManaCostPaid should be on the SA, not the Card - xManaPaid = sa.getHostCard().getXManaCostPaid(); + xManaPaid = sa.getXManaCostPaid(); // Triggering info triggeringObjects = sa.getTriggeringObjects(); @@ -194,13 +194,12 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { // Saved sub-SA needs to be reset on the way out if (subInstance != null) { ability.setSubAbility((AbilitySub) subInstance.getSpellAbility(true)); - ability.getSubAbility().setParent(ability); } // Set Cost specific things here ability.setPaidHash(paidHash); ability.setSplicedCards(splicedCards); - ability.getHostCard().setXManaCostPaid(xManaPaid); + ability.setXManaCostPaid(xManaPaid); // Triggered ability.setTriggeringObjects(triggeringObjects); diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityVariables.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityVariables.java index a40263e218f..bde3575ddf0 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityVariables.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityVariables.java @@ -17,15 +17,14 @@ */ package forge.game.spellability; -import java.util.EnumSet; -import java.util.Set; - import com.google.common.collect.Sets; - import forge.game.GameType; import forge.game.phase.PhaseType; import forge.game.zone.ZoneType; +import java.util.EnumSet; +import java.util.Set; + /** *

* SpellAbilityVariables class. @@ -61,10 +60,7 @@ public class SpellAbilityVariables implements Cloneable { this.gameTypes = Sets.newEnumSet(sav.getGameTypes(), GameType.class); this.sorcerySpeed = sav.isSorcerySpeed(); this.instantSpeed = sav.isInstantSpeed(); - this.anyPlayer = sav.isAnyPlayer(); - this.opponentOnly = sav.isOpponentOnly(); - this.enchantedControllerOnly = sav.isEnchantedControllerOnly(); - this.ownerOnly = sav.isOwnerOnly(); + this.activator = sav.getActivator(); this.opponentTurn = sav.isOpponentTurn(); this.playerTurn = sav.isPlayerTurn(); this.activationLimit = sav.getActivationLimit(); @@ -89,6 +85,7 @@ public class SpellAbilityVariables implements Cloneable { this.lifeTotal = sav.getLifeTotal(); this.lifeAmount = sav.getLifeAmount(); this.manaSpent = sav.getManaSpent(); + this.manaNotSpent = sav.getManaNotSpent(); this.targetValidTargeting = sav.getTargetValidTargeting(); this.targetsSingleTarget = sav.targetsSingleTarget(); this.presenceCondition = sav.getPresenceCondition(); @@ -111,7 +108,7 @@ public class SpellAbilityVariables implements Cloneable { private boolean instantSpeed = false; /** The b any player. */ - private boolean anyPlayer = false; + private String activator = "You"; /** The b opponent only. */ private boolean opponentOnly = false; @@ -119,6 +116,9 @@ public class SpellAbilityVariables implements Cloneable { /** The b opponent only. */ private boolean enchantedControllerOnly = false; + /** The b opponent only. */ + private boolean attackedPlayerOnly = false; + /** The b owner only. */ private boolean ownerOnly = false; @@ -193,6 +193,7 @@ public class SpellAbilityVariables implements Cloneable { /** The mana spent. */ private String manaSpent = ""; + private String manaNotSpent = ""; /** The chosen colors string. */ private String chosenColors = null; @@ -229,6 +230,13 @@ public class SpellAbilityVariables implements Cloneable { return this.manaSpent; } + public final void setManaNotSpent(final String s) { + this.manaNotSpent = s; + } + public final String getManaNotSpent() { + return this.manaNotSpent; + } + /** *

* Setter for the field zone. @@ -252,62 +260,28 @@ public class SpellAbilityVariables implements Cloneable { return this.zone; } - /** - *

- * setSorcerySpeed. - *

- * - * @param bSpeed - * a boolean. - */ public final void setSorcerySpeed(final boolean bSpeed) { this.sorcerySpeed = bSpeed; } - /** - *

- * getSorcerySpeed. - *

- * - * @return a boolean. - */ public final boolean isSorcerySpeed() { return this.sorcerySpeed; } - /** - *

- * setInstantSpeed. - *

- * - * @param bSpeed - * a boolean. - */ public final void setInstantSpeed(final boolean bSpeed) { this.instantSpeed = bSpeed; } - /** - *

- * getInstantSpeed. - *

- * - * @return a boolean. - */ public final boolean isInstantSpeed() { return this.instantSpeed; } - /** - *

- * setAnyPlayer. - *

- * - * @param anyPlayer - * a boolean. - */ - public final void setAnyPlayer(final boolean anyPlayer) { - this.anyPlayer = anyPlayer; + public final void setActivator(final String player) { + this.activator = player; + } + + public String getActivator() { + return this.activator; } /** @@ -851,60 +825,6 @@ public class SpellAbilityVariables implements Cloneable { return this.isPresent; } - /** - * Checks if is any player. - * - * @return the anyPlayer - */ - public final boolean isAnyPlayer() { - return this.anyPlayer; - } - - /** - * @return the opponentOnly - */ - public boolean isOpponentOnly() { - return opponentOnly; - } - - /** - * @param opponentOnly the opponentOnly to set - */ - public void setOpponentOnly(boolean opponentOnly) { - this.opponentOnly = opponentOnly; - } - - /** - * @return the opponentOnly - */ - public boolean isEnchantedControllerOnly() { - return enchantedControllerOnly; - } - - public void setEnchantedControllerOnly(boolean enchantedControllerOnly) { - this.enchantedControllerOnly = enchantedControllerOnly; - } - /** - * @return the ownerOnly - */ - public boolean isOwnerOnly() { - return ownerOnly; - } - - /** - * @param ownerOnly the ownerOnly to set - */ - public void setOwnerOnly(boolean ownerOnly) { - this.ownerOnly = ownerOnly; - } - /** - *

- * Setter for the field ColorToCheck. - *

- * - * @param s - * a {@link java.lang.String} object. - */ public final void setColorToCheck(final String s) { this.chosenColors = s; } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityView.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityView.java index 20e3d91e522..f34a4a6629d 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityView.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityView.java @@ -1,10 +1,14 @@ package forge.game.spellability; +import java.util.Map; + +import com.google.common.collect.Maps; + import forge.game.card.CardView; import forge.game.card.IHasCardView; -import forge.trackable.TrackableCollection; import forge.trackable.TrackableObject; import forge.trackable.TrackableProperty; +import forge.trackable.Tracker; public class SpellAbilityView extends TrackableObject implements IHasCardView { private static final long serialVersionUID = 2514234930798754769L; @@ -13,19 +17,19 @@ public class SpellAbilityView extends TrackableObject implements IHasCardView { return spab == null ? null : spab.getView(); } - public static TrackableCollection getCollection(Iterable spabs) { - if (spabs == null) { - return null; + public static Map getMap(Iterable spabs) { + Map spellViewCache = Maps.newLinkedHashMap(); + for (T spellAbility : spabs) { + spellViewCache.put(spellAbility.getView(), spellAbility); } - TrackableCollection collection = new TrackableCollection<>(); - for (SpellAbility spab : spabs) { - collection.add(spab.getView()); - } - return collection; + return spellViewCache; } SpellAbilityView(final SpellAbility sa) { - super(sa.getId(), sa.getHostCard() == null || sa.getHostCard().getGame() == null ? null : sa.getHostCard().getGame().getTracker()); + this(sa, sa.getHostCard() == null || sa.getHostCard().getGame() == null ? null : sa.getHostCard().getGame().getTracker()); + } + SpellAbilityView(final SpellAbility sa, Tracker tracker) { + super(sa.getId(), tracker); updateHostCard(sa); updateDescription(sa); updatePromptIfOnlyPossibleAbility(sa); 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 4d1c812d141..1559a847038 100644 --- a/forge-game/src/main/java/forge/game/spellability/TargetChoices.java +++ b/forge-game/src/main/java/forge/game/spellability/TargetChoices.java @@ -19,6 +19,7 @@ package forge.game.spellability; import com.google.common.collect.Iterables; +import forge.game.GameEntity; import forge.game.GameObject; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -116,6 +117,14 @@ public class TargetChoices implements Cloneable { return targetSpells; } + public final List getTargetEntities() { + final List tgts = new ArrayList<>(); + tgts.addAll(targetPlayers); + tgts.addAll(targetCards); + + return tgts; + } + public final List getTargets() { final List tgts = new ArrayList<>(); tgts.addAll(targetPlayers); diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java index f8442310ec0..febcf7b5275 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -181,15 +181,9 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone layers.add(StaticAbilityLayer.MODIFYPT); } - if (hasParam("AddHiddenKeyword")) { - layers.add(StaticAbilityLayer.RULES); - } - - if (hasParam("IgnoreEffectCost") || hasParam("Goad") || hasParam("CanBlockAny") || hasParam("CanBlockAmount")) { - layers.add(StaticAbilityLayer.RULES); - } - - if (hasParam("AdjustLandPlays")) { + if (hasParam("AddHiddenKeyword") + || hasParam("IgnoreEffectCost") || hasParam("Goad") || hasParam("CanBlockAny") || hasParam("CanBlockAmount") + || hasParam("AdjustLandPlays") || hasParam("ControlVote") || hasParam("AdditionalVote") || hasParam("AdditionalOptionalVote")) { layers.add(StaticAbilityLayer.RULES); } @@ -680,7 +674,7 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone final String type = getParam("Presence"); int revealed = AbilityUtils.calculateAmount(hostCard, "Revealed$Valid " + type, hostCard.getCastSA()); - int ctrl = AbilityUtils.calculateAmount(hostCard, "Count$Valid " + type + ".inZoneBattlefield+YouCtrl", hostCard.getCastSA()); + int ctrl = AbilityUtils.calculateAmount(hostCard, "Count$LastStateBattlefield " + type + ".YouCtrl", hostCard.getCastSA()); if (revealed + ctrl == 0) { return false; diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantPutCounter.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantPutCounter.java index c66923a5168..37952b2e563 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantPutCounter.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantPutCounter.java @@ -11,7 +11,7 @@ public class StaticAbilityCantPutCounter { final Card hostCard = staticAbility.getHostCard(); if (staticAbility.hasParam("CounterType")) { - CounterType t = CounterType.valueOf(staticAbility.getParam("CounterType")); + CounterType t = CounterType.getType(staticAbility.getParam("CounterType")); if (t != null && !type.equals(t)) { return false; } @@ -34,7 +34,7 @@ public class StaticAbilityCantPutCounter { final Card hostCard = staticAbility.getHostCard(); if (staticAbility.hasParam("CounterType")) { - CounterType t = CounterType.valueOf(staticAbility.getParam("CounterType")); + CounterType t = CounterType.getType(staticAbility.getParam("CounterType")); if (t != null && !type.equals(t)) { return false; } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java index f0b0813bfc3..457f3c3cf8e 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java @@ -220,6 +220,9 @@ public final class StaticAbilityContinuous { if (!hostCard.hasChosenName() && input.contains("ChosenName")) { return true; } + if (!hostCard.hasChosenEvenOdd() && (input.contains("ChosenEvenOdd") || input.contains("chosenEvenOdd"))) { + return true; + } // two variants for Red vs. red in keyword if (input.contains("ColorsYouCtrl") || input.contains("colorsYouCtrl")) { @@ -261,6 +264,10 @@ public final class StaticAbilityContinuous { final String chosenName = hostCard.getChosenName().replace(",", ";"); input = input.replaceAll("ChosenName", "Card.named" + chosenName); } + if (hostCard.hasChosenEvenOdd()) { + input = input.replaceAll("ChosenEvenOdd", hostCard.getChosenEvenOdd().toString()); + input = input.replaceAll("chosenEvenOdd", hostCard.getChosenEvenOdd().toString().toLowerCase()); + } input = input.replace("HostCardUID", hostCardUID); return input; } @@ -513,6 +520,20 @@ public final class StaticAbilityContinuous { } } + if (params.containsKey("ControlVote")) { + p.addControlVote(se.getTimestamp()); + } + if (params.containsKey("AdditionalVote")) { + String mhs = params.get("AdditionalVote"); + int add = AbilityUtils.calculateAmount(hostCard, mhs, stAb); + p.addAdditionalVote(se.getTimestamp(), add); + } + if (params.containsKey("AdditionalOptionalVote")) { + String mhs = params.get("AdditionalOptionalVote"); + int add = AbilityUtils.calculateAmount(hostCard, mhs, stAb); + p.addAdditionalOptionalVote(se.getTimestamp(), add); + } + if (params.containsKey("RaiseMaxHandSize")) { String rmhs = params.get("RaiseMaxHandSize"); int rmax = AbilityUtils.calculateAmount(hostCard, rmhs, stAb); diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityETBTapped.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityETBTapped.java index 43dd0cb64df..589053aee89 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityETBTapped.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityETBTapped.java @@ -17,9 +17,9 @@ */ package forge.game.staticability; +import forge.game.CardTraitBase; import forge.game.card.Card; -import java.util.Map; /** * The Class StaticAbility_CantBeCast. @@ -36,11 +36,15 @@ public class StaticAbilityETBTapped { * @return true, if successful */ public static boolean applyETBTappedAbility(final StaticAbility stAb, final Card card) { - final Map params = stAb.getMapParams(); final Card hostCard = stAb.getHostCard(); - return !params.containsKey("ValidCard") - || card.isValid(params.get("ValidCard").split(","), hostCard.getController(), hostCard, null); + if (stAb.hasParam("ValidCard")) { + if (!CardTraitBase.matchesValid(card, stAb.getParam("ValidCard").split(","), hostCard)) { + return false; + } + } + + return true; } } 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 8388fa3ccdf..2f39ff3b0e5 100644 --- a/forge-game/src/main/java/forge/game/trigger/Trigger.java +++ b/forge-game/src/main/java/forge/game/trigger/Trigger.java @@ -29,7 +29,6 @@ import forge.game.card.CardState; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; -import forge.game.spellability.Ability; import forge.game.spellability.OptionalCost; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; @@ -67,40 +66,12 @@ public abstract class Trigger extends TriggerReplacementBase { private TriggerType mode; - private Map storedTriggeredObjects = null; - private List triggerRemembered = Lists.newArrayList(); // number of times this trigger was activated this this turn // used to handle once-per-turn triggers like Crawling Sensation private int numberTurnActivations = 0; - /** - *

- * Setter for the field storedTriggeredObjects. - *

- * - * @param storedTriggeredObjects - * a {@link java.util.HashMap} object. - * @since 1.0.15 - */ - public final void setStoredTriggeredObjects(final Map storedTriggeredObjects) { - this.storedTriggeredObjects = AbilityKey.newMap(storedTriggeredObjects); - } - - /** - *

- * Getter for the field storedTriggeredObjects. - *

- * - * @return a {@link java.util.HashMap} object. - * @since 1.0.15 - */ - public final Map getStoredTriggeredObjects() { - return this.storedTriggeredObjects; - } - - private Set validPhases; /** @@ -472,27 +443,6 @@ public abstract class Trigger extends TriggerReplacementBase { this.id = id; } - private Ability triggeredSA; - - /** - * Gets the triggered sa. - * - * @return the triggered sa - */ - public final Ability getTriggeredSA() { - return this.triggeredSA; - } - - /** - * Sets the triggered sa. - * - * @param sa - * the triggered sa to set - */ - public void setTriggeredSA(final Ability sa) { - this.triggeredSA = sa; - } - public void addRemembered(Object o) { this.triggerRemembered.add(o); } @@ -586,10 +536,10 @@ public abstract class Trigger extends TriggerReplacementBase { } super.changeText(); - ensureAbility(); + SpellAbility sa = ensureAbility(); - if (getOverridingAbility() != null) { - getOverridingAbility().changeText(); + if (sa != null) { + sa.changeText(); } } @@ -603,14 +553,14 @@ public abstract class Trigger extends TriggerReplacementBase { } super.changeTextIntrinsic(colorMap, typeMap); - ensureAbility(); + SpellAbility sa = ensureAbility(); - if (getOverridingAbility() != null) { - getOverridingAbility().changeTextIntrinsic(colorMap, typeMap); + if (sa != null) { + sa.changeTextIntrinsic(colorMap, typeMap); } } - private SpellAbility ensureAbility() { + public SpellAbility ensureAbility() { SpellAbility sa = getOverridingAbility(); if (sa == null && hasParam("Execute")) { sa = AbilityFactory.getAbility(getHostCard(), getParam("Execute")); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerAttackersDeclared.java b/forge-game/src/main/java/forge/game/trigger/TriggerAttackersDeclared.java index c174343b171..010104e4868 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerAttackersDeclared.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerAttackersDeclared.java @@ -89,7 +89,7 @@ public class TriggerAttackersDeclared extends Trigger { /** {@inheritDoc} */ @Override public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { - sa.setTriggeringObjectsFrom(runParams, AbilityKey.Attackers, AbilityKey.AttackingPlayer); + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Attackers, AbilityKey.AttackingPlayer, AbilityKey.AttackedTarget); } @Override diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java b/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java index 7b1e8670005..9a05067dcba 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java @@ -17,7 +17,6 @@ */ package forge.game.trigger; -import forge.game.Game; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -102,16 +101,13 @@ public class TriggerChangesZone extends Trigger { if (hasParam("ValidCard")) { Card moved = (Card) runParams.get(AbilityKey.Card); - final Game game = getHostCard().getGame(); boolean leavesBattlefield = "Battlefield".equals(getParam("Origin")); - boolean isDiesTrig = leavesBattlefield && "Graveyard".equals(getParam("Destination")); - if (isDiesTrig) { - moved = game.getChangeZoneLKIInfo(moved); + if (leavesBattlefield) { + moved = (Card) runParams.get(AbilityKey.CardLKI); } - if (!moved.isValid(getParam("ValidCard").split(","), getHostCard().getController(), - getHostCard(), null)) { + if (!matchesValid(moved, getParam("ValidCard").split(","), getHostCard())) { return false; } } @@ -124,9 +120,10 @@ public class TriggerChangesZone extends Trigger { if (cause == null) { return false; } - if (!cause.getHostCard().isValid(getParam("ValidCause").split(","), getHostCard().getController(), - getHostCard(), null)) { - return false; + if (!matchesValid(cause, getParam("ValidCause").split(","), getHostCard())) { + if (!matchesValid(cause.getHostCard(), getParam("ValidCause").split(","), getHostCard())) { + return false; + } } } @@ -151,17 +148,11 @@ public class TriggerChangesZone extends Trigger { return false; } - final Card card; - final int rightSide; - try { - card = (Card) runParams.get(AbilityKey.Card); - rightSide = Integer.parseInt(cond.substring(2)); - } catch (NumberFormatException | ClassCastException e) { - return false; - } + final Card card = (Card) runParams.get(AbilityKey.Card); if (card == null) { return false; } + final int rightSide = AbilityUtils.calculateAmount(getHostCard(), cond.substring(2), this); // need to check the ChangeZone LKI copy for damage, otherwise it'll return 0 for a new object in the new zone Card lkiCard = card.getGame().getChangeZoneLKIInfo(card); @@ -214,7 +205,11 @@ public class TriggerChangesZone extends Trigger { /** {@inheritDoc} */ @Override public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { - sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card); + if ("Battlefield".equals(getParam("Origin"))) { + sa.setTriggeringObject(AbilityKey.Card, runParams.get(AbilityKey.CardLKI)); + } else { + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card); + } } @Override diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java b/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java index 3fc236f94bc..e72b72c6a60 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java @@ -19,6 +19,7 @@ package forge.game.trigger; import forge.game.ability.AbilityKey; import forge.game.card.Card; +import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.util.Localizer; @@ -53,7 +54,7 @@ public class TriggerCycled extends Trigger { /** {@inheritDoc} */ @Override public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { - sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card); + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card, AbilityKey.Cause); } @Override @@ -68,8 +69,22 @@ public class TriggerCycled extends Trigger { @Override public final boolean performTest(final Map runParams) { if (hasParam("ValidCard")) { - return matchesValid(runParams.get(AbilityKey.Card), getParam("ValidCard").split(","), - this.getHostCard()); + if (!matchesValid(runParams.get(AbilityKey.Card), getParam("ValidCard").split(","), getHostCard())) { + return false; + } + } + + if (hasParam("ValidPlayer")) { + Player p = (Player) runParams.get(AbilityKey.Player); + if (!matchesValid(p, getParam("ValidPlayer").split(","), getHostCard())) { + return false; + } + } + + if (hasParam("OnlyFirst")) { + if ((int) runParams.get(AbilityKey.NumThisTurn) != 1) { + return false; + } } return true; } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDealtOnce.java b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDealtOnce.java index c47f29118ec..a4faa7ced1b 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDealtOnce.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDealtOnce.java @@ -21,12 +21,13 @@ import forge.game.GameEntity; import forge.game.ability.AbilityKey; import forge.game.card.Card; import forge.game.spellability.SpellAbility; -import forge.util.Expressions; import forge.util.Localizer; import java.util.Map; import java.util.Set; +import com.google.common.collect.Sets; + /** *

* Trigger_DamageDone class. @@ -59,7 +60,6 @@ public class TriggerDamageDealtOnce extends Trigger { @Override public final boolean performTest(final Map runParams) { final Card srcs = (Card) runParams.get(AbilityKey.DamageSource); - final Set tgt = (Set) runParams.get(AbilityKey.DamageTargets); if (hasParam("CombatDamage")) { if (getParam("CombatDamage").equals("True")) { @@ -72,33 +72,17 @@ public class TriggerDamageDealtOnce extends Trigger { } } } - + if (hasParam("ValidTarget")) { - boolean valid = false; - for (GameEntity c : tgt) { - if (c.isValid(getParam("ValidTarget").split(","), this.getHostCard().getController(),this.getHostCard(), null)) { - valid = true; - } - } - if (!valid) { + final Map damageMap = (Map) runParams.get(AbilityKey.DamageMap); + + if (getDamageAmount(damageMap) <= 0) { return false; } } if (hasParam("ValidSource")) { - if (!matchesValid(srcs, getParam("ValidSource").split(","), this.getHostCard())) { - return false; - } - } - - if (hasParam("DamageAmount")) { - final String fullParam = getParam("DamageAmount"); - - final String operator = fullParam.substring(0, 2); - final int operand = Integer.parseInt(fullParam.substring(2)); - final int actualAmount = (Integer) runParams.get(AbilityKey.DamageAmount); - - if (!Expressions.compare(actualAmount, operator, operand)) { + if (!matchesValid(srcs, getParam("ValidSource").split(","), getHostCard())) { return false; } } @@ -109,9 +93,12 @@ public class TriggerDamageDealtOnce extends Trigger { /** {@inheritDoc} */ @Override public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { - sa.setTriggeringObjectsFrom(runParams, AbilityKey.DamageAmount); + @SuppressWarnings("unchecked") + final Map damageMap = (Map) runParams.get(AbilityKey.DamageMap); + sa.setTriggeringObject(AbilityKey.Source, runParams.get(AbilityKey.DamageSource)); - sa.setTriggeringObject(AbilityKey.Targets, runParams.get(AbilityKey.DamageTargets)); + sa.setTriggeringObject(AbilityKey.Targets, getDamageTargets(damageMap)); + sa.setTriggeringObject(AbilityKey.DamageAmount, getDamageAmount(damageMap)); } @Override @@ -122,4 +109,27 @@ public class TriggerDamageDealtOnce extends Trigger { sb.append(Localizer.getInstance().getMessage("lblAmount")).append(": ").append(sa.getTriggeringObject(AbilityKey.DamageAmount)); return sb.toString(); } + + public int getDamageAmount(Map damageMap) { + int result = 0; + for (Map.Entry e : damageMap.entrySet()) { + if (!hasParam("ValidTarget") || matchesValid(e.getKey(), getParam("ValidTarget").split(","), getHostCard())) { + result += e.getValue(); + } + } + return result; + } + + public Set getDamageTargets(Map damageMap) { + if (!hasParam("ValidTarget")) { + return Sets.newHashSet(damageMap.keySet()); + } + Set result = Sets.newHashSet(); + for (GameEntity e : damageMap.keySet()) { + if (matchesValid(e, getParam("ValidTarget").split(","), getHostCard())) { + result.add(e); + } + } + return result; + } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDoneOnce.java b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDoneOnce.java index dbd1cf48fb6..72b6d9c4501 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDoneOnce.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDoneOnce.java @@ -3,6 +3,8 @@ package forge.game.trigger; import java.util.Map; import java.util.Set; +import com.google.common.collect.Sets; + import forge.game.GameEntity; import forge.game.ability.AbilityKey; import forge.game.card.Card; @@ -19,7 +21,6 @@ public class TriggerDamageDoneOnce extends Trigger { @SuppressWarnings("unchecked") @Override public boolean performTest(Map runParams) { - final Set srcs = (Set) runParams.get(AbilityKey.DamageSources); final GameEntity tgt = (GameEntity) runParams.get(AbilityKey.DamageTarget); if (hasParam("CombatDamage")) { @@ -33,35 +34,34 @@ public class TriggerDamageDoneOnce extends Trigger { } } } - - if (hasParam("ValidSource")) { - boolean valid = false; - for (Card c : srcs) { - if (c.isValid(getParam("ValidSource").split(","), this.getHostCard().getController(),this.getHostCard(), null)) { - valid = true; - } - } - if (!valid) { - return false; - } - } - - if (hasParam("ValidTarget")) { - if (!matchesValid(tgt, getParam("ValidTarget").split(","), this.getHostCard())) { - return false; - } - } - - + if (hasParam("ValidSource")) { + final Map damageMap = (Map) runParams.get(AbilityKey.DamageMap); + + if (getDamageAmount(damageMap) <= 0) { + return false; + } + } + + if (hasParam("ValidTarget")) { + if (!matchesValid(tgt, getParam("ValidTarget").split(","), getHostCard())) { + return false; + } + } + + + return true; } @Override public void setTriggeringObjects(SpellAbility sa, Map runParams) { + @SuppressWarnings("unchecked") + final Map damageMap = (Map) runParams.get(AbilityKey.DamageMap); + sa.setTriggeringObject(AbilityKey.Target, runParams.get(AbilityKey.DamageTarget)); - sa.setTriggeringObject(AbilityKey.Sources, runParams.get(AbilityKey.DamageSources)); - sa.setTriggeringObjectsFrom(runParams, AbilityKey.DamageAmount); + sa.setTriggeringObject(AbilityKey.Sources, getDamageSources(damageMap)); + sa.setTriggeringObject(AbilityKey.DamageAmount, getDamageAmount(damageMap)); } @Override @@ -74,4 +74,26 @@ public class TriggerDamageDoneOnce extends Trigger { return sb.toString(); } + public int getDamageAmount(Map damageMap) { + int result = 0; + for (Map.Entry e : damageMap.entrySet()) { + if (!hasParam("ValidSource") || matchesValid(e.getKey(), getParam("ValidSource").split(","), getHostCard())) { + result += e.getValue(); + } + } + return result; + } + + public Set getDamageSources(Map damageMap) { + if (!hasParam("ValidSource")) { + return Sets.newHashSet(damageMap.keySet()); + } + Set result = Sets.newHashSet(); + for (Card c : damageMap.keySet()) { + if (matchesValid(c, getParam("ValidSource").split(","), getHostCard())) { + result.add(c); + } + } + return result; + } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerDiscardedAll.java b/forge-game/src/main/java/forge/game/trigger/TriggerDiscardedAll.java new file mode 100644 index 00000000000..cb2837e71d6 --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDiscardedAll.java @@ -0,0 +1,61 @@ +package forge.game.trigger; + +import java.util.Map; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +public class TriggerDiscardedAll extends Trigger { + + public TriggerDiscardedAll(Map params, Card host, boolean intrinsic) { + super(params, host, intrinsic); + } + + @Override + public boolean performTest(Map runParams) { + if (hasParam("ValidPlayer")) { + if (!matchesValid(runParams.get(AbilityKey.Player), getParam("ValidPlayer").split(","), + this.getHostCard())) { + return false; + } + } + + if (hasParam("ValidCause")) { + if (runParams.get(AbilityKey.Cause) == null) { + return false; + } + if (!matchesValid(runParams.get(AbilityKey.Cause), getParam("ValidCause").split(","), + this.getHostCard())) { + return false; + } + } + + if (hasParam("FirstTime")) { + if (!(boolean) runParams.get(AbilityKey.FirstTime)) { + return false; + } + } + return true; + } + + @Override + public void setTriggeringObjects(SpellAbility sa, Map runParams) { + final CardCollection cards = (CardCollection) runParams.get(AbilityKey.Cards); + + sa.setTriggeringObject(AbilityKey.Cards, cards); + sa.setTriggeringObject(AbilityKey.Amount, cards.size()); + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player, AbilityKey.Cause); + } + + @Override + public String getImportantStackObjects(SpellAbility sa) { + StringBuilder sb = new StringBuilder(); + sb.append(Localizer.getInstance().getMessage("lblPlayer")).append(": ").append(sa.getTriggeringObject(AbilityKey.Player)).append(", "); + sb.append(Localizer.getInstance().getMessage("lblAmount")).append(": ").append(sa.getTriggeringObject(AbilityKey.Amount)); + return sb.toString(); + } + +} diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerDrawn.java b/forge-game/src/main/java/forge/game/trigger/TriggerDrawn.java index 4eb1b913da3..742bf9b7e63 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerDrawn.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDrawn.java @@ -21,6 +21,8 @@ import forge.game.Game; import forge.game.GameStage; import forge.game.ability.AbilityKey; import forge.game.card.Card; +import forge.game.phase.PhaseType; +import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.util.Localizer; @@ -66,12 +68,26 @@ public class TriggerDrawn extends Trigger { } } + if (hasParam("ValidPlayer")) { + if (!matchesValid(runParams.get(AbilityKey.Player), getParam("ValidPlayer").split(","), + this.getHostCard())) { + return false; + } + } + if (hasParam("Number")) { if (number != Integer.parseInt(getParam("Number"))) { return false; } } + if (hasParam("NotFirstCardInDrawStep")) { + final Player p = ((Player)runParams.get(AbilityKey.Player)); + if (p.numDrawnThisDrawStep() == 1 && game.getPhaseHandler().is(PhaseType.DRAW, p)) { + return false; + } + } + // trigger should not happen while Mulligan if (game.getAge() == GameStage.Mulligan) { return false; diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java index 33a8e48663e..b8a03d637a2 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java @@ -17,7 +17,6 @@ */ package forge.game.trigger; -import forge.card.mana.ManaCost; import forge.game.Game; import forge.game.GlobalRuleChange; import forge.game.ability.AbilityFactory; @@ -27,16 +26,15 @@ import forge.game.ability.AbilityKey; import forge.game.ability.effects.CharmEffect; import forge.game.card.Card; import forge.game.card.CardLists; +import forge.game.card.CardPredicates; import forge.game.card.CardUtil; import forge.game.card.CardZoneTable; import forge.game.keyword.KeywordInterface; import forge.game.phase.PhaseType; import forge.game.player.Player; -import forge.game.spellability.Ability; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityStackInstance; -import forge.game.spellability.TargetRestrictions; import forge.game.zone.Zone; import forge.game.zone.ZoneType; import forge.util.FileSection; @@ -528,11 +526,7 @@ public class TriggerHandler { sa = regtrig.getOverridingAbility(); if (sa == null) { if (!regtrig.hasParam("Execute")) { - sa = new Ability(host, ManaCost.ZERO) { - @Override - public void resolve() { - } - }; + sa = new SpellAbility.EmptySa(host); } else { String name = regtrig.getParam("Execute"); @@ -563,9 +557,6 @@ public class TriggerHandler { sa.setSourceTrigger(regtrig.getId()); regtrig.setTriggeringObjects(sa, runParams); sa.setTriggerRemembered(regtrig.getTriggerRemembered()); - if (regtrig.getStoredTriggeredObjects() != null) { - sa.setTriggeringObjects(regtrig.getStoredTriggeredObjects()); - } if (sa.getDeltrigActivatingPlayer() != null) { // make sure that the original delayed trigger activator is restored @@ -591,33 +582,20 @@ public class TriggerHandler { } Player decider = null; - boolean mand = false; + boolean isMandatory = false; if (regtrig.hasParam("OptionalDecider")) { sa.setOptionalTrigger(true); decider = AbilityUtils.getDefinedPlayers(host, regtrig.getParam("OptionalDecider"), sa).get(0); } else if (sa instanceof AbilitySub || !sa.hasParam("Cost") || sa.getParam("Cost").equals("0")) { - mand = true; + isMandatory = true; } else { // triggers with a cost can't be mandatory sa.setOptionalTrigger(true); decider = sa.getActivatingPlayer(); } - SpellAbility ability = sa; - while (ability != null) { - final TargetRestrictions tgt = ability.getTargetRestrictions(); - - if (tgt != null) { - tgt.setMandatory(true); - } - ability = ability.getSubAbility(); - } - final boolean isMandatory = mand; - final WrappedAbility wrapperAbility = new WrappedAbility(regtrig, sa, decider); - wrapperAbility.setTrigger(true); - wrapperAbility.setMandatory(isMandatory); //wrapperAbility.setDescription(wrapperAbility.getStackDescription()); //wrapperAbility.setDescription(wrapperAbility.toUnsuppressedString()); @@ -628,7 +606,6 @@ public class TriggerHandler { else { game.getStack().addSimultaneousStackEntry(wrapperAbility); } - regtrig.setTriggeredSA(wrapperAbility); regtrig.triggerRun(); @@ -642,10 +619,29 @@ public class TriggerHandler { private int handlePanharmonicon(final Trigger t, final Map runParams, final Player p) { Card host = t.getHostCard(); + int n = 0; + + // Sanctum of All + if (host.isShrine() && host.isInZone(ZoneType.Battlefield) && p.equals(host.getController())) { + int shrineCount = CardLists.count(p.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Shrine")); + if (shrineCount >= 6) { + for (final Card ck : p.getCardsIn(ZoneType.Battlefield)) { + for (final KeywordInterface ki : ck.getKeywords()) { + final String kw = ki.getOriginal(); + if (kw.startsWith("Shrineharmonicon")) { + final String valid = kw.split(":")[1]; + if (host.isValid(valid.split(","), p, ck, null)) { + n++; + } + } + } + } + } + } // not a changesZone trigger or changesZoneAll if (t.getMode() != TriggerType.ChangesZone && t.getMode() != TriggerType.ChangesZoneAll) { - return 0; + return n; } // leave battlefield trigger, might be dying @@ -660,7 +656,6 @@ public class TriggerHandler { return 0; } - int n = 0; if (t.getMode() == TriggerType.ChangesZone) { // iterate over all cards final List lastCards = CardLists.filterControlledBy(p.getGame().getLastStateBattlefield(), p); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerSacrificed.java b/forge-game/src/main/java/forge/game/trigger/TriggerSacrificed.java index 3cbaa734980..f483eb5cb5a 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerSacrificed.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerSacrificed.java @@ -78,15 +78,6 @@ public class TriggerSacrificed extends Trigger { return false; } } - - if (hasParam("CauseParam")) { - // For now only for Heart-Piecer Manticore, extend it if it appears on different effects too - if (sourceSA == null || !sourceSA.hasParam("SacrificeParam") - || !sourceSA.getParam("SacrificeParam").equals(getParam("CauseParam")) - || !sourceSA.getHostCard().equals(this.getHostCard())) { - return false; - } - } if (hasParam("WhileKeyword")) { final String keyword = getParam("WhileKeyword"); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerTapsForMana.java b/forge-game/src/main/java/forge/game/trigger/TriggerTapsForMana.java index bd32d65a794..6cbf96cc1aa 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerTapsForMana.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerTapsForMana.java @@ -60,7 +60,7 @@ public class TriggerTapsForMana extends Trigger { //Check for tapping if (!hasParam("NoTapCheck")) { final SpellAbility manaAbility = (SpellAbility) runParams.get(AbilityKey.AbilityMana); - if (manaAbility == null || manaAbility.getRootAbility().getPayCosts() == null || !manaAbility.getRootAbility().getPayCosts().hasTapCost()) { + if (manaAbility == null || !manaAbility.getRootAbility().getPayCosts().hasTapCost()) { return false; } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerTokenCreated.java b/forge-game/src/main/java/forge/game/trigger/TriggerTokenCreated.java new file mode 100644 index 00000000000..c1cd076bd88 --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerTokenCreated.java @@ -0,0 +1,86 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.game.trigger; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +import java.util.Map; + +/** + *

+ * Trigger_LandPlayed class. + *

+ * + * @author Forge + * @version $Id: TriggerInvestigated.java 30294 2015-10-16 01:53:32Z friarsol $ + */ +public class TriggerTokenCreated extends Trigger { + + /** + *

+ * Constructor for Trigger_Investigated. + *

+ * + * @param params + * a {@link java.util.HashMap} object. + * @param host + * a {@link forge.game.card.Card} object. + * @param intrinsic + * the intrinsic + */ + public TriggerTokenCreated(final Map params, final Card host, final boolean intrinsic) { + super(params, host, intrinsic); + } + + @Override + public String getImportantStackObjects(SpellAbility sa) { + StringBuilder sb = new StringBuilder(); + sb.append(Localizer.getInstance().getMessage("lblPlayer")).append(": ").append(sa.getTriggeringObject(AbilityKey.Player)); + return sb.toString(); + } + + /** {@inheritDoc} */ + @Override + public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player); + } + + /** {@inheritDoc} + * @param runParams*/ + @Override + public final boolean performTest(final Map runParams) { + Player p = (Player) runParams.get(AbilityKey.Player); + if (hasParam("ValidPlayer")) { + if (!matchesValid(p, getParam("ValidPlayer").split(","), getHostCard())) { + return false; + } + } + + if (hasParam("OnlyFirst")) { + if ((int) runParams.get(AbilityKey.Num) != 1) { + return false; + } + } + return true; + } + +} diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerType.java b/forge-game/src/main/java/forge/game/trigger/TriggerType.java index b0f82c1c153..e3f0b5f36fd 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerType.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerType.java @@ -52,6 +52,7 @@ public enum TriggerType { Destroyed(TriggerDestroyed.class), Devoured(TriggerDevoured.class), Discarded(TriggerDiscarded.class), + DiscardedAll(TriggerDiscardedAll.class), Drawn(TriggerDrawn.class), Evolved(TriggerEvolved.class), Exerted(TriggerExerted.class), @@ -90,6 +91,7 @@ public enum TriggerType { Surveil(TriggerSurveil.class), Taps(TriggerTaps.class), TapsForMana(TriggerTapsForMana.class), + TokenCreated(TriggerTokenCreated.class), Transformed(TriggerTransformed.class), TurnBegin(TriggerTurnBegin.class), TurnFaceUp(TriggerTurnFaceUp.class), diff --git a/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java b/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java index 1a3a9e84fe2..e996bfa30fa 100644 --- a/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java +++ b/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java @@ -33,8 +33,9 @@ public class WrappedAbility extends Ability { boolean mandatory = false; public WrappedAbility(final Trigger regtrig0, final SpellAbility sa0, final Player decider0) { - super(regtrig0.getHostCard(), ManaCost.ZERO, sa0.getView()); + super(sa0.getHostCard(), ManaCost.ZERO, sa0.getView()); setTrigger(regtrig0); + setTrigger(true); sa = sa0; decider = decider0; sa.setDescription(this.getStackDescription()); @@ -53,18 +54,6 @@ public class WrappedAbility extends Ability { return decider; } - public final void setMandatory(final boolean mand) { - this.mandatory = mand; - } - - /** - * @return the mandatory - */ - @Override - public boolean isMandatory() { - return mandatory; - } - @Override public String getParam(String key) { return sa.getParam(key); } @@ -214,7 +203,7 @@ public class WrappedAbility extends Ability { @Override public String toUnsuppressedString() { String desc = this.getStackDescription(); /* use augmented stack description as string for wrapped things */ - String card = getTrigger().getHostCard().toString(); + String card = getHostCard().toString(); if ( !desc.contains(card) && desc.contains(" this ")) { /* a hack for Evolve and similar that don't have CARDNAME */ return card + ": " + desc; } else return desc; @@ -446,9 +435,8 @@ public class WrappedAbility extends Ability { public void resolve() { final Game game = sa.getActivatingPlayer().getGame(); final Trigger regtrig = getTrigger(); - Map triggerParams = regtrig.getMapParams(); - if (!(regtrig instanceof TriggerAlways) && !triggerParams.containsKey("NoResolvingCheck")) { + if (!(regtrig instanceof TriggerAlways) && !regtrig.hasParam("NoResolvingCheck")) { // Most State triggers don't have "Intervening If" if (!regtrig.requirementsCheck(game)) { return; @@ -460,10 +448,10 @@ public class WrappedAbility extends Ability { } } - if (triggerParams.containsKey("ResolvingCheck")) { + if (regtrig.hasParam("ResolvingCheck")) { // rare cases: Hidden Predators (state trigger, but have "Intervening If" to check IsPresent2) etc. Map recheck = new HashMap<>(); - String key = triggerParams.get("ResolvingCheck"); + String key = regtrig.getParam("ResolvingCheck"); String value = regtrig.getParam(key); recheck.put(key, value); if (!meetsCommonRequirements(recheck)) { @@ -471,29 +459,18 @@ public class WrappedAbility extends Ability { } } - TriggerHandler th = game.getTriggerHandler(); - - if (decider != null && !decider.getController().confirmTrigger(this, triggerParams, this.isMandatory())) { - return; - } - // set Trigger sa.setTrigger(regtrig); - if (!triggerParams.containsKey("NoTimestampCheck")) { + if (decider != null && !decider.getController().confirmTrigger(this)) { + return; + } + + if (!regtrig.hasParam("NoTimestampCheck")) { timestampCheck(); } getActivatingPlayer().getController().playSpellAbilityNoStack(sa, false); - - // Add eventual delayed trigger. - if (triggerParams.containsKey("DelayedTrigger")) { - final String sVarName = triggerParams.get("DelayedTrigger"); - final Trigger deltrig = TriggerHandler.parseTrigger(regtrig.getHostCard().getSVar(sVarName), - regtrig.getHostCard(), true); - deltrig.setStoredTriggeredObjects(this.getTriggeringObjects()); - th.registerDelayedTrigger(deltrig); - } } /** @@ -556,4 +533,18 @@ public class WrappedAbility extends Ability { public void setAlternativeCost(AlternativeCost ac) { sa.setAlternativeCost(ac); } + + public Integer getXManaCostPaid() { + return sa.getXManaCostPaid(); + } + public void setXManaCostPaid(final Integer n) { + sa.setXManaCostPaid(n); + } + + public Card getOriginalHost() { + return sa.getOriginalHost(); + } + public void setOriginalHost(final Card c) { + sa.setOriginalHost(c); + } } \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index ee8571e5eb5..ee805d953e5 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -136,8 +136,13 @@ public class MagicStack /* extends MyObservable */ implements Iterable { // Do not add Tokens to other zones than the battlefield. // But Effects/Emblems count as Tokens too, so allow Command too. - if (zoneType == ZoneType.Battlefield || zoneType == ZoneType.Command || !c.isToken()) { + if (zoneType == ZoneType.Battlefield || !c.isToken()) { c.setZone(this); if (index == null) { diff --git a/forge-game/src/main/java/forge/trackable/TrackableProperty.java b/forge-game/src/main/java/forge/trackable/TrackableProperty.java index 2660337d82f..f9b2e6b3253 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableProperty.java +++ b/forge-game/src/main/java/forge/trackable/TrackableProperty.java @@ -1,7 +1,9 @@ package forge.trackable; + import forge.card.CardRarity; import forge.game.Direction; +import forge.game.EvenOdd; import forge.game.GameType; import forge.game.phase.PhaseType; import forge.game.zone.ZoneType; @@ -40,12 +42,14 @@ public enum TrackableProperty { CommanderAltType(TrackableTypes.StringType), Damage(TrackableTypes.IntegerType), AssignedDamage(TrackableTypes.IntegerType), + LethalDamage(TrackableTypes.IntegerType), ShieldCount(TrackableTypes.IntegerType), ChosenType(TrackableTypes.StringType), ChosenColors(TrackableTypes.StringListType), ChosenCards(TrackableTypes.CardViewCollectionType), ChosenPlayer(TrackableTypes.PlayerViewType), ChosenDirection(TrackableTypes.EnumType(Direction.class)), + ChosenEvenOdd(TrackableTypes.EnumType(EvenOdd.class)), ChosenMode(TrackableTypes.StringType), Remembered(TrackableTypes.StringType), NamedCard(TrackableTypes.StringType), @@ -61,8 +65,8 @@ public enum TrackableProperty { Haunting(TrackableTypes.CardViewType), MustBlockCards(TrackableTypes.CardViewCollectionType), PairedWith(TrackableTypes.CardViewType), - CurrentState(TrackableTypes.CardStateViewType, FreezeMode.IgnoresFreezeIfUnset), - AlternateState(TrackableTypes.CardStateViewType), + CurrentState(TrackableTypes.CardStateViewType, FreezeMode.IgnoresFreeze), + AlternateState(TrackableTypes.CardStateViewType, FreezeMode.IgnoresFreeze), HiddenId(TrackableTypes.IntegerType), ExertedThisTurn(TrackableTypes.BooleanType), @@ -134,6 +138,9 @@ public enum TrackableProperty { HasUnlimitedLandPlay(TrackableTypes.BooleanType), NumLandThisTurn(TrackableTypes.IntegerType), NumDrawnThisTurn(TrackableTypes.IntegerType), + AdditionalVote(TrackableTypes.IntegerType), + OptionalAdditionalVote(TrackableTypes.IntegerType), + ControlVotes(TrackableTypes.BooleanType), Keywords(TrackableTypes.KeywordCollectionViewType, FreezeMode.IgnoresFreeze), Commander(TrackableTypes.CardViewCollectionType, FreezeMode.IgnoresFreeze), CommanderCast(TrackableTypes.IntegerMapType), @@ -176,6 +183,7 @@ public enum TrackableProperty { BandsWithBlockers(TrackableTypes.GenericMapType, FreezeMode.IgnoresFreeze), AttackersWithPlannedBlockers(TrackableTypes.GenericMapType, FreezeMode.IgnoresFreeze), BandsWithPlannedBlockers(TrackableTypes.GenericMapType, FreezeMode.IgnoresFreeze), + CombatView(TrackableTypes.CombatViewType, FreezeMode.IgnoresFreeze), //Game Players(TrackableTypes.PlayerViewCollectionType), @@ -216,6 +224,10 @@ public enum TrackableProperty { return freezeMode; } + public TrackableType getType() { + return type; + } + @SuppressWarnings("unchecked") public void updateObjLookup(Tracker tracker, T newObj) { ((TrackableType)type).updateObjLookup(tracker, newObj); diff --git a/forge-game/src/main/java/forge/trackable/TrackableTypes.java b/forge-game/src/main/java/forge/trackable/TrackableTypes.java index 6214563f8f7..38ba05d4b47 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableTypes.java +++ b/forge-game/src/main/java/forge/trackable/TrackableTypes.java @@ -17,6 +17,7 @@ import forge.game.GameEntityView; import forge.game.card.CardView; import forge.game.card.CardView.CardStateView; import forge.game.card.CounterType; +import forge.game.combat.CombatView; import forge.game.keyword.KeywordCollection.KeywordCollectionView; import forge.game.player.PlayerView; import forge.game.spellability.StackItemView; @@ -100,17 +101,27 @@ public class TrackableTypes { if (newCollection != null) { //swap in objects in old collection for objects in new collection for (int i = 0; i < newCollection.size(); i++) { - T newObj = newCollection.get(i); - if (newObj != null) { - T existingObj = from.getTracker().getObj(itemType, newObj.getId()); - if (existingObj != null) { //if object exists already, update its changed properties - existingObj.copyChangedProps(newObj); - newCollection.remove(i); - newCollection.add(i, existingObj); - } - else { //if object is new, cache in object lookup - from.getTracker().putObj(itemType, newObj.getId(), newObj); + try { + T newObj = newCollection.get(i); + if (newObj != null) { + T existingObj = from.getTracker().getObj(itemType, newObj.getId()); + if (existingObj != null) { //fix cards with alternate state/ manifest/ morph/ adventure etc... + if (prop.getType() == TrackableTypes.CardViewCollectionType || + prop.getType() == TrackableTypes.StackItemViewListType) { + newCollection.remove(i); + newCollection.add(i, newObj); + } else { //if object exists already, update its changed properties + existingObj.copyChangedProps(newObj); + newCollection.remove(i); + newCollection.add(i, existingObj); + } + } + else { //if object is new, cache in object lookup + from.getTracker().putObj(itemType, newObj.getId(), newObj); + } } + } catch (IndexOutOfBoundsException e) { + System.err.println("got an IndexOutOfBoundsException, trying to continue ..."); } } } @@ -573,9 +584,9 @@ public class TrackableTypes { public Map deserialize(TrackableDeserializer td, Map oldValue) { int size = td.readInt(); if (size > 0) { - Map map = Maps.newEnumMap(CounterType.class); + Map map = Maps.newHashMap(); for (int i = 0; i < size; i++) { - map.put(CounterType.valueOf(td.readString()), td.readInt()); + map.put(CounterType.getType(td.readString()), td.readInt()); } return map; } @@ -586,7 +597,7 @@ public class TrackableTypes { public void serialize(TrackableSerializer ts, Map value) { ts.write(value.size()); for (Entry entry : value.entrySet()) { - ts.write(entry.getKey().name()); + ts.write(entry.getKey().toString()); ts.write(entry.getValue()); } } @@ -622,4 +633,26 @@ public class TrackableTypes { public void serialize(TrackableSerializer ts, Map value) { } }; + public static final TrackableObjectType CombatViewType = new TrackableObjectType() { + @Override + protected CombatView getDefaultValue() { + return null; + } + + @Override + protected CombatView deserialize(TrackableDeserializer td, CombatView oldValue) { + oldValue.deserialize(td); //TODO handle old value being null or changing to null + return oldValue; + } + + @Override + protected void serialize(TrackableSerializer ts, CombatView value) { + if (value == null) { + ts.write(-1); + } + else { + value.serialize(ts); + } + } + }; } diff --git a/forge-gui-android/AndroidManifest.xml b/forge-gui-android/AndroidManifest.xml index 5edbcfad66e..1f2dc8ae144 100644 --- a/forge-gui-android/AndroidManifest.xml +++ b/forge-gui-android/AndroidManifest.xml @@ -6,7 +6,8 @@ + android:targetSdkVersion="26" /> + diff --git a/forge-gui-android/libs/android-support-v4.jar b/forge-gui-android/libs/android-support-v4.jar index 187bdf48b1d..653cb767665 100644 Binary files a/forge-gui-android/libs/android-support-v4.jar and b/forge-gui-android/libs/android-support-v4.jar differ diff --git a/forge-gui-android/libs/gdx-backend-android-sources.jar b/forge-gui-android/libs/gdx-backend-android-sources.jar index 414fa961cfd..d328206fb3a 100644 Binary files a/forge-gui-android/libs/gdx-backend-android-sources.jar and b/forge-gui-android/libs/gdx-backend-android-sources.jar differ diff --git a/forge-gui-android/libs/gdx-backend-android.jar b/forge-gui-android/libs/gdx-backend-android.jar index d780673aa6b..40af9131f01 100644 Binary files a/forge-gui-android/libs/gdx-backend-android.jar and b/forge-gui-android/libs/gdx-backend-android.jar differ diff --git a/forge-gui-android/libs/gdx-freetype.jar b/forge-gui-android/libs/gdx-freetype.jar index eda8b39b168..3181586a666 100644 Binary files a/forge-gui-android/libs/gdx-freetype.jar and b/forge-gui-android/libs/gdx-freetype.jar differ diff --git a/forge-gui-android/libs/gdx-sources.jar b/forge-gui-android/libs/gdx-sources.jar index 798fee80a7b..74b5aadb926 100644 Binary files a/forge-gui-android/libs/gdx-sources.jar and b/forge-gui-android/libs/gdx-sources.jar differ diff --git a/forge-gui-android/libs/gdx.jar b/forge-gui-android/libs/gdx.jar index 0ba4a3f2bb4..2c603b032c5 100644 Binary files a/forge-gui-android/libs/gdx.jar and b/forge-gui-android/libs/gdx.jar differ diff --git a/forge-gui-android/pom.xml b/forge-gui-android/pom.xml index 76158aee896..868707a1840 100644 --- a/forge-gui-android/pom.xml +++ b/forge-gui-android/pom.xml @@ -6,7 +6,7 @@ jar -Xms1024m -Xmx1536m - 1.6.32.001 + 1.6.34.001 keystore alias storepass @@ -19,7 +19,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.36-SNAPSHOT forge-gui-android @@ -104,24 +104,6 @@ gdx-backend-android 1.9.10 - - org.cache2k - cache2k-base-bom - 1.2.4.Final - pom - - - org.cache2k - cache2k-core - 1.2.4.Final - compile - - - org.cache2k - cache2k-api - 1.2.4.Final - compile - @@ -142,7 +124,7 @@ true - 25 + 26 true ${project.basedir}/AndroidManifest.xml @@ -183,7 +165,7 @@ false - 25 + 26 false diff --git a/forge-gui-android/proguard.cfg b/forge-gui-android/proguard.cfg index 57fa2061f4d..ab6fbdeecf0 100644 --- a/forge-gui-android/proguard.cfg +++ b/forge-gui-android/proguard.cfg @@ -29,20 +29,8 @@ -dontwarn javax.** -dontwarn org.apache.logging.log4j.** -dontwarn module-info - -# mandatory proguard rules for cache2k to keep the core implementation --dontwarn org.cache2k.impl.xmlConfiguration.** --dontwarn org.cache2k.impl.serverSide.** --keep interface org.cache2k.spi.Cache2kCoreProvider --keep public class * extends org.cache2k.spi.Cache2kCoreProvider -# optional proguard rules for cache2k, to keep XML configuration code -# if only programmatic configuration is used, these rules may be ommitted --keep interface org.cache2k.core.spi.CacheConfigurationProvider --keep public class * extends org.cache2k.core.spi.CacheConfigurationProvider --keepclassmembers public class * extends org.cache2k.configuration.ConfigurationBean { - public void set*(...); - public ** get*(); -} +## Support library +-dontwarn android.support.** -keep class forge.** { *; } -keep class com.thoughtworks.xstream.** { *; } @@ -51,6 +39,7 @@ -keep class com.google.common.** { *; } -keep class io.sentry.event.Event { *; } -keep class io.netty.util.internal.logging.** { *; } +-keep class net.jpountz.** { *; } -keepclassmembers class com.badlogic.gdx.backends.android.AndroidInput* { (com.badlogic.gdx.Application, android.content.Context, java.lang.Object, com.badlogic.gdx.backends.android.AndroidApplicationConfiguration); diff --git a/forge-gui-android/project.properties b/forge-gui-android/project.properties index 735ea3db305..94206b9059a 100644 --- a/forge-gui-android/project.properties +++ b/forge-gui-android/project.properties @@ -9,4 +9,4 @@ # Project target. project.type=0 -target=android-20 +target=android-26 diff --git a/forge-gui-android/res/mipmap-hdpi/ic_launcher.png b/forge-gui-android/res/mipmap-hdpi/ic_launcher.png index 13ac6220632..4271e4c73a0 100644 Binary files a/forge-gui-android/res/mipmap-hdpi/ic_launcher.png and b/forge-gui-android/res/mipmap-hdpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-hdpi/ic_launcher_foreground.png b/forge-gui-android/res/mipmap-hdpi/ic_launcher_foreground.png index a29e46aa510..a749b470515 100644 Binary files a/forge-gui-android/res/mipmap-hdpi/ic_launcher_foreground.png and b/forge-gui-android/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/forge-gui-android/res/mipmap-hdpi/ic_launcher_round.png b/forge-gui-android/res/mipmap-hdpi/ic_launcher_round.png index 07a75d76bfa..23880358c4f 100644 Binary files a/forge-gui-android/res/mipmap-hdpi/ic_launcher_round.png and b/forge-gui-android/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/forge-gui-android/res/mipmap-ldpi/ic_launcher.png b/forge-gui-android/res/mipmap-ldpi/ic_launcher.png index ee25ac432d9..ae40d73a33c 100644 Binary files a/forge-gui-android/res/mipmap-ldpi/ic_launcher.png and b/forge-gui-android/res/mipmap-ldpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-mdpi/ic_launcher.png b/forge-gui-android/res/mipmap-mdpi/ic_launcher.png index 5fdd9db84cf..75cac9e8a25 100644 Binary files a/forge-gui-android/res/mipmap-mdpi/ic_launcher.png and b/forge-gui-android/res/mipmap-mdpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-mdpi/ic_launcher_foreground.png b/forge-gui-android/res/mipmap-mdpi/ic_launcher_foreground.png index 524a6232021..bcb5f32aa66 100644 Binary files a/forge-gui-android/res/mipmap-mdpi/ic_launcher_foreground.png and b/forge-gui-android/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/forge-gui-android/res/mipmap-mdpi/ic_launcher_round.png b/forge-gui-android/res/mipmap-mdpi/ic_launcher_round.png index bb574ba21ae..fa0400329c5 100644 Binary files a/forge-gui-android/res/mipmap-mdpi/ic_launcher_round.png and b/forge-gui-android/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/forge-gui-android/res/mipmap-xhdpi/ic_launcher.png b/forge-gui-android/res/mipmap-xhdpi/ic_launcher.png index 5d5de8a444f..ad7c6ed96bb 100644 Binary files a/forge-gui-android/res/mipmap-xhdpi/ic_launcher.png and b/forge-gui-android/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-xhdpi/ic_launcher_foreground.png b/forge-gui-android/res/mipmap-xhdpi/ic_launcher_foreground.png index 0e93289f2d1..25275afddb0 100644 Binary files a/forge-gui-android/res/mipmap-xhdpi/ic_launcher_foreground.png and b/forge-gui-android/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/forge-gui-android/res/mipmap-xhdpi/ic_launcher_round.png b/forge-gui-android/res/mipmap-xhdpi/ic_launcher_round.png index 96328be12a9..42f78d72996 100644 Binary files a/forge-gui-android/res/mipmap-xhdpi/ic_launcher_round.png and b/forge-gui-android/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher.png b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher.png index e6ea9ce1897..3f9d487ce92 100644 Binary files a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher.png and b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_foreground.png b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_foreground.png index a01a5036f4f..a01aaa6e9df 100644 Binary files a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_round.png b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_round.png index e4d1b24e68e..c1f4153cef3 100644 Binary files a/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_round.png and b/forge-gui-android/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher.png b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher.png index 3380a1063bb..6d186851cd6 100644 Binary files a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher.png and b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_foreground.png index e8ccb890b9e..aaa2395dcb9 100644 Binary files a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_round.png b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_round.png index 68aca8009d4..8ad3506407f 100644 Binary files a/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_round.png and b/forge-gui-android/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/forge-gui-android/res/values/ic_launcher_background.xml b/forge-gui-android/res/values/ic_launcher_background.xml index 7f8bb682c03..ab983282473 100644 --- a/forge-gui-android/res/values/ic_launcher_background.xml +++ b/forge-gui-android/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ - #f0f0f0 + #ffffff \ No newline at end of file diff --git a/forge-gui-android/src/forge/app/Main.java b/forge-gui-android/src/forge/app/Main.java index 70aa9389704..9337d030041 100644 --- a/forge-gui-android/src/forge/app/Main.java +++ b/forge-gui-android/src/forge/app/Main.java @@ -1,16 +1,22 @@ package forge.app; import android.app.AlarmManager; +import android.app.AlertDialog; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.StateListDrawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; @@ -18,8 +24,17 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.PowerManager; +import android.provider.Settings; +import android.text.SpannableString; +import android.text.style.StyleSpan; +import android.view.Gravity; +import android.view.View; import android.view.WindowManager; import android.webkit.MimeTypeMap; +import android.widget.Button; +import android.widget.TableLayout; +import android.widget.TableRow; +import android.widget.TextView; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.backends.android.AndroidApplication; import forge.Forge; @@ -35,45 +50,189 @@ import java.io.OutputStream; import java.util.concurrent.Callable; public class Main extends AndroidApplication { + AndroidAdapter Gadapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - AndroidAdapter adapter = new AndroidAdapter(this.getContext()); + boolean permissiongranted = checkPermission(); + Gadapter = new AndroidAdapter(this.getContext()); + initForge(Gadapter, permissiongranted); - //establish assets directory - if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { - Gdx.app.error("Forge", "Can't access external storage"); - adapter.exit(); - return; + //permission + if(!permissiongranted){ + //requestPermission(); + displayMessage(Gadapter); } - String assetsDir = Environment.getExternalStorageDirectory() + "/Forge/"; - if (!FileUtil.ensureDirectoryExists(assetsDir)) { - Gdx.app.error("Forge", "Can't access external storage"); - adapter.exit(); - return; + } + + private void displayMessage(AndroidAdapter adapter){ + TableLayout TL = new TableLayout(this); + TableRow row = new TableRow(this); + TableRow row2 = new TableRow(this); + TextView text = new TextView(this); + text.setGravity(Gravity.LEFT); + text.setTypeface(Typeface.SERIF); + + String title="Forge needs Storage Permission to run properly...\n" + + "Follow these simple steps:\n\n"; + String steps = " 1) Tap \"Open App Details\" Button.\n" + + " 2) Tap Permissions\n"+ + " 3) Turn on the Storage Permission.\n\n"+ + "(You can tap anywhere to exit and restart the app)\n\n"; + + SpannableString ss1= new SpannableString(title); + ss1.setSpan(new StyleSpan(Typeface.BOLD), 0, ss1.length(), 0); + text.append(ss1); + text.append(steps); + row.addView(text); + row.setGravity(Gravity.CENTER); + + int[] colors = {Color.TRANSPARENT,Color.TRANSPARENT}; + int[] pressed = {Color.GREEN,Color.GREEN}; + GradientDrawable gd = new GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, colors); + gd.setStroke(3, Color.DKGRAY); + gd.setCornerRadius(100); + + GradientDrawable gd2 = new GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, pressed); + gd2.setStroke(3, Color.DKGRAY); + gd2.setCornerRadius(100); + + Button button = new Button(this); + button.setText("Open App Details"); + + StateListDrawable states = new StateListDrawable(); + + states.addState(new int[] {android.R.attr.state_pressed}, gd2); + states.addState(new int[] { }, gd); + + button.setBackground(states); + + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + startActivity(intent); + } + }); + + row2.addView(button); + row2.setGravity(Gravity.CENTER); + + TL.addView(row, new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT, TableLayout.LayoutParams.WRAP_CONTENT)); + TL.addView(row2, new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT, TableLayout.LayoutParams.WRAP_CONTENT)); + TL.setGravity(Gravity.CENTER); + TL.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + adapter.exit(); + } + }); + setContentView(TL); + } + @Override + public void onBackPressed() { + if (Gadapter!=null) + Gadapter.exit(); + + super.onBackPressed(); + } + private boolean checkPermission() { + int pid = android.os.Process.myPid(); + int uid = android.os.Process.myUid(); + try { + int result = this.getBaseContext().checkPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, pid, uid); + if (result == PackageManager.PERMISSION_GRANTED) { + return true; + } else { + return false; + } + } catch (NullPointerException e) { + return false; + } + } + private void requestPermission() { + //Show Information about why you need the permission + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Storage Permission Denied..."); + builder.setMessage("This app needs storage permission to run properly.\n\n\n\n"); + builder.setPositiveButton("Open App Details", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + //ActivityCompat crashes... maybe it needs the appcompat v7??? + //ActivityCompat.requestPermissions(Main.this, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); + } + }); + /*builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + + } + });*/ + builder.show(); + } + + private void initForge(AndroidAdapter adapter, boolean permissiongranted){ + boolean isPortrait; + if (permissiongranted){ + //establish assets directory + if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { + Gdx.app.error("Forge", "Can't access external storage"); + adapter.exit(); + return; + } + String assetsDir = Environment.getExternalStorageDirectory() + "/Forge/"; + if (!FileUtil.ensureDirectoryExists(assetsDir)) { + Gdx.app.error("Forge", "Can't access external storage"); + adapter.exit(); + return; + } + + //ensure .nomedia file exists in Forge directory so its images + //and other media files don't appear in Gallery or other apps + String noMediaFile = assetsDir + ".nomedia"; + if (!FileUtil.doesFileExist(noMediaFile)) { + FileUtil.writeFile(noMediaFile, ""); + } + + //enforce orientation based on whether device is a tablet and user preference + adapter.switchOrientationFile = assetsDir + "switch_orientation.ini"; + boolean landscapeMode = adapter.isTablet == !FileUtil.doesFileExist(adapter.switchOrientationFile); + + ForgePreferences prefs = FModel.getPreferences(); + boolean propertyConfig = prefs != null && prefs.getPrefBoolean(ForgePreferences.FPref.UI_NETPLAY_COMPAT); + + if (landscapeMode) { + isPortrait = false; + Main.this.setRequestedOrientation(Build.VERSION.SDK_INT >= 26 ? + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : //Oreo and above has virtual back/menu buttons + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + } else { + isPortrait = true; + Main.this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + + initialize(Forge.getApp(new AndroidClipboard(), adapter, assetsDir, propertyConfig, isPortrait)); + } else { + isPortrait = true; + //set current orientation + Main.this.setRequestedOrientation(Main.this.getResources().getConfiguration().orientation); + initialize(Forge.getApp(new AndroidClipboard(), adapter, "", false, isPortrait)); } - //ensure .nomedia file exists in Forge directory so its images - //and other media files don't appear in Gallery or other apps - String noMediaFile = assetsDir + ".nomedia"; - if (!FileUtil.doesFileExist(noMediaFile)) { - FileUtil.writeFile(noMediaFile, ""); - } - - //enforce orientation based on whether device is a tablet and user preference - adapter.switchOrientationFile = assetsDir + "switch_orientation.ini"; - boolean landscapeMode = adapter.isTablet == !FileUtil.doesFileExist(adapter.switchOrientationFile); - if (landscapeMode) { - Main.this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - } - else { - Main.this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } - - boolean value = Build.VERSION.SDK_INT >= 26; - initialize(Forge.getApp(new AndroidClipboard(), adapter, assetsDir, value)); } /*@Override diff --git a/forge-gui-desktop/pom.xml b/forge-gui-desktop/pom.xml index c2bb8f2388f..db6d0137ee3 100644 --- a/forge-gui-desktop/pom.xml +++ b/forge-gui-desktop/pom.xml @@ -4,7 +4,7 @@ forge forge - 1.6.33-SNAPSHOT + 1.6.36-SNAPSHOT forge-gui-desktop @@ -88,7 +88,7 @@ org.codehaus.mojo build-helper-maven-plugin - 1.8 + 3.1.0 released-version @@ -213,7 +213,7 @@ com.akathist.maven.plugins.launch4j launch4j-maven-plugin - 1.5.2 + 1.7.25 l4j-gui @@ -235,7 +235,10 @@ 1.8.0 - 1024 + 4096 + + -Dfile.encoding=UTF-8 + @@ -324,7 +327,7 @@ org.codehaus.mojo build-helper-maven-plugin - 1.8 + 3.1.0 released-version @@ -367,7 +370,7 @@ com.akathist.maven.plugins.launch4j launch4j-maven-plugin - 1.5.2 + 1.7.25 l4j-gui @@ -389,7 +392,10 @@ 1.8.0 - 1024 + 4096 + + -Dfile.encoding=UTF-8 + @@ -481,7 +487,7 @@ org.codehaus.mojo build-helper-maven-plugin - 1.8 + 3.1.0 released-version @@ -564,7 +570,7 @@