diff --git a/forge-adventure/pom.xml b/forge-adventure/pom.xml index feb29f8d407..9b2fd342fe3 100644 --- a/forge-adventure/pom.xml +++ b/forge-adventure/pom.xml @@ -3,7 +3,7 @@ forge forge - 1.6.50-SNAPSHOT + 1.6.54-SNAPSHOT 4.0.0 @@ -119,10 +119,26 @@ -Dfile.encoding=UTF-8 --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.math=ALL-UNNAMED + --add-opens java.base/jdk.internal.misc=ALL-UNNAMED + --add-opens java.base/java.nio=ALL-UNNAMED + --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED + --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED + --add-opens java.desktop/java.awt.image=ALL-UNNAMED + --add-opens java.desktop/java.awt.color=ALL-UNNAMED + --add-opens java.desktop/sun.awt.image=ALL-UNNAMED + --add-opens java.desktop/javax.swing=ALL-UNNAMED + --add-opens java.desktop/javax.swing.border=ALL-UNNAMED + --add-opens java.desktop/javax.swing.event=ALL-UNNAMED + --add-opens java.desktop/sun.swing=ALL-UNNAMED + --add-opens java.desktop/java.beans=ALL-UNNAMED + --add-opens java.base/java.util.concurrent=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + -Dio.netty.tryReflectionSetAccessible=true @@ -274,7 +290,7 @@ forge forge-gui-mobile - 1.6.50-SNAPSHOT + 1.6.54-SNAPSHOT compile diff --git a/forge-adventure/src/main/config/forge-adventure.cmd b/forge-adventure/src/main/config/forge-adventure.cmd index a707e9a64d6..8779fa25870 100644 --- a/forge-adventure/src/main/config/forge-adventure.cmd +++ b/forge-adventure/src/main/config/forge-adventure.cmd @@ -10,7 +10,7 @@ java -version 1>nul 2>nul || ( for /f tokens^=2^ delims^=.-_^+^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j" if %jver% GEQ 17 ( - java --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$ + java --add-opens java.desktop/java.beans=ALL-UNNAMED --add-opens java.desktop/javax.swing.border=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED --add-opens java.desktop/sun.swing=ALL-UNNAMED --add-opens java.desktop/java.awt.image=ALL-UNNAMED --add-opens java.desktop/java.awt.color=ALL-UNNAMED --add-opens java.desktop/sun.awt.image=ALL-UNNAMED --add-opens java.desktop/javax.swing=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$ popd exit /b 0 ) diff --git a/forge-ai/pom.xml b/forge-ai/pom.xml index de797344831..cb8f95d4227 100644 --- a/forge-ai/pom.xml +++ b/forge-ai/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.50-SNAPSHOT + 1.6.54-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 64424620d11..dfa47c071e7 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -20,6 +20,7 @@ package forge.ai; import java.util.ArrayList; import java.util.List; +import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked; import org.apache.commons.lang3.tuple.Pair; import com.google.common.base.Predicate; @@ -540,7 +541,7 @@ public class AiAttackController { for (Card attacker : categorizedAttackers) { if (!CombatUtil.canBeBlocked(attacker, accountedBlockers, null) - || attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) { + || StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) { unblockedAttackers.add(attacker); } else { if (predictEvasion) { diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index dd489c3f8c3..81e001f1c41 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -42,6 +42,7 @@ import forge.game.cost.Cost; import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked; import forge.game.staticability.StaticAbilityCantAttackBlock; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerType; @@ -204,7 +205,7 @@ public class AiBlockController { } blocker = ComputerUtilCard.getWorstCreatureAI(killingBlockers); // 2.Blockers that won't get destroyed - } else if (!attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.") + } else if (!StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker) && !ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) { blocker = ComputerUtilCard.getWorstCreatureAI(safeBlockers); // check whether it's better to block a creature without trample to absorb more damage @@ -215,7 +216,7 @@ public class AiBlockController { || other.hasKeyword(Keyword.TRAMPLE) || ComputerUtilCombat.attackerHasThreateningAfflict(other, ai) || ComputerUtilCombat.canDestroyBlocker(ai, blocker, other, combat, false) - || other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) { + || StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(other)) { continue; } @@ -668,7 +669,7 @@ public class AiBlockController { Card attacker = attackers.get(0); if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > 1 - || attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.") + || StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker) || ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) { attackers.remove(0); makeChumpBlocks(combat, attackers); @@ -689,7 +690,7 @@ public class AiBlockController { } if (other.getNetCombatDamage() >= damageAbsorbed && !other.hasKeyword(Keyword.TRAMPLE) - && !other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.") + && !StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(other) && !ComputerUtilCombat.attackerHasThreateningAfflict(other, ai) && CombatUtil.canBlock(other, blocker, combat)) { combat.addBlocker(other, blocker); @@ -756,7 +757,7 @@ public class AiBlockController { for (final Card attacker : tramplingAttackers) { if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size() - || attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.") + || StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker) || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) { continue; } @@ -945,6 +946,39 @@ public class AiBlockController { } } + private void makeRequiredBlocks(Combat combat) { + // assign blockers that have to block + final CardCollection chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."); + // if an attacker with lure attacks - all that can block + for (final Card blocker : blockersLeft) { + if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) { + chumpBlockers.add(blocker); + } + } + if (!chumpBlockers.isEmpty()) { + for (final Card attacker : attackers) { + List blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false); + for (final Card blocker : blockers) { + if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker) + && (CombatUtil.mustBlockAnAttacker(blocker, combat, null) + || blocker.hasKeyword("CARDNAME blocks each combat if able."))) { + combat.addBlocker(attacker, blocker); + if (!blocker.getMustBlockCards().isEmpty()) { + int mustBlockAmt = blocker.getMustBlockCards().size(); + final CardCollectionView blockedSoFar = combat.getAttackersBlockedBy(blocker); + boolean canBlockAnother = CombatUtil.canBlockMoreCreatures(blocker, blockedSoFar); + if (!canBlockAnother || mustBlockAmt == blockedSoFar.size()) { + blockersLeft.remove(blocker); + } + } else { + blockersLeft.remove(blocker); + } + } + } + } + } + } + private void clearBlockers(final Combat combat, final List possibleBlockers) { for (final Card blocker : CardLists.filterControlledBy(combat.getAllBlockers(), ai)) { // don't touch other player's blockers @@ -1008,9 +1042,6 @@ public class AiBlockController { clearBlockers(combat, possibleBlockers); - List blockers; - List chumpBlockers; - diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO)) { diff = 0; @@ -1106,37 +1137,9 @@ public class AiBlockController { } } - // assign blockers that have to block - chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."); - // if an attacker with lure attacks - all that can block - for (final Card blocker : blockersLeft) { - if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) { - chumpBlockers.add(blocker); - } - } - if (!chumpBlockers.isEmpty()) { - CardLists.shuffle(attackers); - for (final Card attacker : attackers) { - blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false); - for (final Card blocker : blockers) { - if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker) - && (CombatUtil.mustBlockAnAttacker(blocker, combat, null) - || blocker.hasKeyword("CARDNAME blocks each combat if able."))) { - combat.addBlocker(attacker, blocker); - if (!blocker.getMustBlockCards().isEmpty()) { - int mustBlockAmt = blocker.getMustBlockCards().size(); - final CardCollectionView blockedSoFar = combat.getAttackersBlockedBy(blocker); - boolean canBlockAnother = CombatUtil.canBlockMoreCreatures(blocker, blockedSoFar); - if (!canBlockAnother || mustBlockAmt == blockedSoFar.size()) { - blockersLeft.remove(blocker); - } - } else { - blockersLeft.remove(blocker); - } - } - } - } - } + // block requirements + // TODO because this isn't done earlier, sometimes a good block will enforce a restriction that prevents another for the requirement + makeRequiredBlocks(combat); // check to see if it's possible to defend a Planeswalker under attack with a chump block, // unless life is low enough to be more worried about saving preserving the life total @@ -1186,8 +1189,7 @@ public class AiBlockController { * Orders a blocker that put onto the battlefield blocking. Depends heavily * on the implementation of orderBlockers(). */ - public static CardCollection orderBlocker(final Card attacker, final Card blocker, - final CardCollection oldBlockers) { + public static CardCollection orderBlocker(final Card attacker, final Card blocker, final CardCollection oldBlockers) { // add blocker to existing ordering // sort by evaluate, then insert it appropriately // relies on current implementation of orderBlockers() diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 9327e9666eb..b2b2ce2ea96 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -889,6 +889,10 @@ public class AiController { spellHost.setLastKnownZone(game.getStackZone()); // need to add to stack to make check Restrictions respect stack cmc spellHost.setCastFrom(card.getZone()); } + // TODO maybe other location for this? + if (!sa.isLegalAfterStack()) { + return AiPlayDecision.AnotherTime; + } if (!sa.checkRestrictions(spellHost, player)) { return AiPlayDecision.AnotherTime; } @@ -1322,7 +1326,7 @@ public class AiController { return discardList; } - public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { if (mode == PlayerActionConfirmMode.AlternativeDamageAssignment) { return true; } @@ -1335,7 +1339,7 @@ public class AiController { mode); throw new IllegalArgumentException(exMsg); } - return SpellApiToAi.Converter.get(api).confirmAction(player, sa, mode, message); + return SpellApiToAi.Converter.get(api).confirmAction(player, sa, mode, message, params); } public boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, int bid, Player winner) { @@ -1925,8 +1929,8 @@ public class AiController { return Math.min(player.getLife() -1,MyRandom.getRandom().nextInt(Math.max(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1); } else if ("HighestGetCounter".equals(logic)) { return MyRandom.getRandom().nextInt(3); - } else if (source.hasSVar("EnergyToPay")) { - return AbilityUtils.calculateAmount(source, source.getSVar("EnergyToPay"), sa); + } else if (sa.hasSVar("EnergyToPay")) { + return AbilityUtils.calculateAmount(source, sa.getSVar("EnergyToPay"), sa); } else if ("Vermin".equals(logic)) { return MyRandom.getRandom().nextInt(Math.max(player.getLife() - 5, 0)); } else if ("SweepCreatures".equals(logic)) { diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index 5fd871ed612..15c0a89f1b9 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -48,6 +48,7 @@ import forge.game.replacement.ReplacementLayer; import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; import forge.game.staticability.StaticAbility; +import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked; import forge.game.staticability.StaticAbilityMustAttack; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerType; @@ -325,8 +326,7 @@ public class ComputerUtilCombat { final List blockers = combat.getBlockers(attacker); if (blockers.size() == 0 - || attacker.hasKeyword("You may have CARDNAME assign its combat damage " - + "as though it weren't blocked.")) { + || StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) { unblocked.add(attacker); } else if (attacker.hasKeyword(Keyword.TRAMPLE) && getAttack(attacker) > totalShieldDamage(attacker, blockers)) { @@ -367,8 +367,7 @@ public class ComputerUtilCombat { final List blockers = combat.getBlockers(attacker); if (blockers.size() == 0 - || attacker.hasKeyword("You may have CARDNAME assign its combat damage" - + " as though it weren't blocked.")) { + || StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) { unblocked.add(attacker); } else if (attacker.hasKeyword(Keyword.TRAMPLE) && getAttack(attacker) > totalShieldDamage(attacker, blockers)) { diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java index 00485a66921..a9a58304bd4 100644 --- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java @@ -10,6 +10,7 @@ import forge.game.card.CounterEnumType; import forge.game.cost.CostPayEnergy; import forge.game.keyword.Keyword; import forge.game.spellability.SpellAbility; +import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked; import forge.game.staticability.StaticAbilityMustAttack; import java.util.List; @@ -62,7 +63,8 @@ public class CreatureEvaluator implements Function { if (c.hasKeyword("Unblockable")) { value += addValue(power * 10, "unblockable"); } else { - if (c.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) { + if (StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(c) + || StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(c, false)) { value += addValue(power * 6, "thorns"); } if (c.hasKeyword(Keyword.FEAR)) { diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index 4b0a7f0274a..40c0d4127ec 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -15,7 +15,6 @@ import forge.card.MagicColor; import forge.card.mana.ManaAtom; import forge.game.Game; import forge.game.GameEntity; -import forge.game.GameObject; import forge.game.ability.AbilityFactory; import forge.game.ability.effects.DetachedCardEffect; import forge.game.card.Card; @@ -207,7 +206,7 @@ public abstract class GameState { cardsReferencedByID.add(card.getExiledWith()); } if (zone == ZoneType.Battlefield) { - if (!card.getAttachedCards().isEmpty()) { + if (card.hasCardAttachments()) { // Remember the ID of cards that have attachments cardsReferencedByID.add(card); } @@ -376,7 +375,7 @@ public abstract class GameState { newText.append("|Imprinting:").append(TextUtil.join(imprintedCardIds, ",")); } - if (!c.getMergedCards().isEmpty()) { + if (c.hasMergedCard()) { List mergedCardNames = new ArrayList<>(); for (Card merged : c.getMergedCards()) { if (c.getTopMergedCard() == merged) { @@ -862,9 +861,7 @@ public abstract class GameState { } if (sa.hasParam("RememberTargets")) { - for (final GameObject o : sa.getTargets()) { - sa.getHostCard().addRemembered(o); - } + sa.getHostCard().addRemembered(sa.getTargets()); } } diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 4d944408df0..826710bf432 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -270,8 +270,8 @@ public class PlayerControllerAi extends PlayerController { } @Override - public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) { - return getAi().confirmAction(sa, mode, message); + public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { + return getAi().confirmAction(sa, mode, message, params); } @Override diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index e96dc0dcbb3..c1654ffa096 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -305,7 +305,7 @@ public abstract class SpellAbilityAi { return SpellApiToAi.Converter.get(ab.getApi()).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb)); } - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { System.err.println("Warning: default (ie. inherited from base class) implementation of confirmAction is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method"); return true; } diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index 796ad4fcf16..18742c0de7e 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -71,6 +71,7 @@ public enum SpellApiToAi { .put(ApiType.DigMultiple, DigMultipleAi.class) .put(ApiType.DigUntil, DigUntilAi.class) .put(ApiType.Discard, DiscardAi.class) + .put(ApiType.Draft, ChooseCardNameAi.class) .put(ApiType.DrainMana, DrainManaAi.class) .put(ApiType.Draw, DrawAi.class) .put(ApiType.EachDamage, DamageEachAi.class) @@ -169,6 +170,7 @@ public enum SpellApiToAi { .put(ApiType.StoreSVar, StoreSVarAi.class) .put(ApiType.Subgame, AlwaysPlayAi.class) .put(ApiType.Surveil, SurveilAi.class) + .put(ApiType.TakeInitiative, AlwaysPlayAi.class) .put(ApiType.Tap, TapAi.class) .put(ApiType.TapAll, TapAllAi.class) .put(ApiType.TapOrUntap, TapOrUntapAi.class) @@ -184,7 +186,6 @@ public enum SpellApiToAi { .put(ApiType.WinsGame, GameWinAi.class) .put(ApiType.DamageResolve, AlwaysPlayAi.class) - .put(ApiType.InternalEtbReplacement, CanPlayAsDrawbackAi.class) .put(ApiType.InternalLegendaryRule, LegendaryRuleAi.class) .put(ApiType.InternalIgnoreEffect, CannotPlayAi.class) .build()); diff --git a/forge-ai/src/main/java/forge/ai/ability/AlwaysPlayAi.java b/forge-ai/src/main/java/forge/ai/ability/AlwaysPlayAi.java index 331e072aa8e..3c80fd8b35b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AlwaysPlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AlwaysPlayAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; @@ -16,7 +18,7 @@ public class AlwaysPlayAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } } 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 85749deda23..d49341d1552 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java @@ -85,7 +85,7 @@ public class AmassAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } 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 762585f8bdb..849f4eec2d8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java @@ -240,7 +240,7 @@ public class AnimateAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return player.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2); } @@ -500,17 +500,19 @@ public class AnimateAi extends SpellAbilityAi { } // give sVars - if (sVars.size() > 0) { - for (final String s : sVars) { - String actualsVar = source.getSVar(s); + if (sa.hasParam("sVars")) { + Map sVarsMap = Maps.newHashMap(); + for (final String s : sa.getParam("sVars").split(",")) { + String actualsVar = AbilityUtils.getSVar(sa, s); String name = s; if (actualsVar.startsWith("SVar:")) { actualsVar = actualsVar.split("SVar:")[1]; name = actualsVar.split(":")[0]; actualsVar = actualsVar.split(":")[1]; } - card.setSVar(name, actualsVar); + sVarsMap.put(name, actualsVar); } + card.addChangedSVars(sVarsMap, timestamp, 0); } ComputerUtilCard.applyStaticContPT(game, card, null); } 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 85888b2f456..c0b376fbb2b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -1729,7 +1729,7 @@ public class AttachAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java b/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java deleted file mode 100644 index 2e303b29776..00000000000 --- a/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java +++ /dev/null @@ -1,35 +0,0 @@ -package forge.ai.ability; - -import forge.ai.SpellAbilityAi; -import forge.game.player.Player; -import forge.game.spellability.SpellAbility; - -public class CanPlayAsDrawbackAi 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) { - return false; - } - - /** - *

- * copySpellTriggerAI. - *

- * @param sa - * a {@link forge.game.spellability.SpellAbility} object. - * @param mandatory - * a boolean. - * @param af - * a {@link forge.game.ability.AbilityFactory} object. - * - * @return a boolean. - */ - @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return false; - } - -} 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 8e07a8b3832..c6bc4e9dadb 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -1739,7 +1739,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { // AI was never asked return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java index 4bbdebab113..f820fe99b8a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import java.util.Collections; +import java.util.Map; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; @@ -334,7 +335,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { final Card source = sa.getHostCard(); final String hostName = source.getName(); final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0); 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 4a719fa2ea5..9928a485383 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java @@ -154,7 +154,7 @@ public class CloneAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { if (sa.hasParam("AILogic") && (!sa.usesTargeting() || sa.isTargetNumberValid())) { // Had a special logic for it and managed to target, so confirm if viable if ("CloneBestCreature".equals(sa.getParam("AILogic"))) { 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 166b45ba933..2577673a7ad 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java @@ -224,7 +224,7 @@ public class CopyPermanentAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { //TODO: add logic here return true; } 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 000c52bc32c..534ad4e5b79 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java @@ -134,7 +134,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { // Chain of Acid requires special attention here since otherwise the AI will confirm the copy and then // run into the necessity of confirming a mandatory Destroy, thus destroying all of its own permanents. if ("ChainOfAcid".equals(sa.getParam("AILogic"))) { 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 29f5b555995..667aee15d71 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -752,14 +752,16 @@ public class CountersPutAi extends CountersAi { final int amount = AbilityUtils.calculateAmount(source, amountStr, sa); int left = amount; final String[] types; + String type = ""; if (sa.hasParam("CounterType")) { // TODO some cards let you choose types, should check each types = sa.getParam("CounterType").split(","); - } else { + type = types[0]; + } else if (sa.hasParam("CounterTypes")) { // all types will be added types = sa.getParam("CounterTypes").split(","); + type = types[0]; } - final String type = types[0]; if (!sa.usesTargeting()) { // No target. So must be defined @@ -911,7 +913,7 @@ public class CountersPutAi extends CountersAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { final Card source = sa.getHostCard(); if (mode == PlayerActionConfirmMode.Tribute) { // add counter if that opponent has a giant creature 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 0ae4a0187f3..9e824bc14ee 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.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.collect.Lists; @@ -151,7 +152,7 @@ public class CountersPutAllAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return player.getCreaturesInPlay().size() >= player.getWeakestOpponent().getCreaturesInPlay().size(); } 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 27fd6daf191..420a4be32b7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -53,6 +53,7 @@ import forge.util.MyRandom; public class DamageDealAi extends DamageAiBase { @Override public boolean chkAIDrawback(SpellAbility sa, Player ai) { + final SpellAbility root = sa.getRootAbility(); final String damage = sa.getParam("NumDmg"); Card source = sa.getHostCard(); int dmg = AbilityUtils.calculateAmount(source, damage, sa); @@ -76,7 +77,7 @@ public class DamageDealAi extends DamageAiBase { if (dmg > energy || dmg < 1) { continue; // in case the calculation gets messed up somewhere } - source.setSVar("EnergyToPay", "Number$" + dmg); + root.setSVar("EnergyToPay", "Number$" + dmg); return true; } } @@ -1075,6 +1076,11 @@ public class DamageDealAi extends DamageAiBase { return null; } + // chaining to this could miscalculate + if (sa.isDividedAsYouChoose()) { + return null; + } + // Try to chain damage/debuff effects if (StringUtils.isNumeric(damage) || (damage.startsWith("-") && StringUtils.isNumeric(damage.substring(1)))) { // currently only works for predictable numeric damage diff --git a/forge-ai/src/main/java/forge/ai/ability/DayTimeAi.java b/forge-ai/src/main/java/forge/ai/ability/DayTimeAi.java index a51e6745b6d..e114d50332e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DayTimeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DayTimeAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.SpellAbilityAi; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -30,7 +32,7 @@ public class DayTimeAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } } 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 2c5eb5dff0f..02a5c9aa557 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -353,12 +353,10 @@ public class DestroyAi extends SpellAbilityAi { // Filter AI-specific targets if provided preferred = ComputerUtil.filterAITgts(sa, ai, preferred, true); - for (final Card c : preferred) { - list.remove(c); - } + list.removeAll(preferred); if (preferred.isEmpty() && !mandatory) { - return false; + return false; } while (sa.canAddMoreTarget()) { 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 9f223ca34ae..33b8dac06a2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigAi.java @@ -209,7 +209,7 @@ public class DigAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { Card topc = player.getZone(ZoneType.Library).get(0); // AI actions for individual cards (until this AI can be generalized) diff --git a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java index 1f838d9172d..4c63daf2d02 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; @@ -94,7 +96,7 @@ public class DigMultipleAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java index ecb26f22112..968b6473ee1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import forge.ai.AiAttackController; import forge.ai.ComputerUtilCost; @@ -122,7 +123,7 @@ public class DigUntilAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { if (sa.hasParam("AILogic")) { final String logic = sa.getParam("AILogic"); if ("OathOfDruids".equals(logic)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java index 010260cfedd..b8ac075c536 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.Collections; import java.util.List; +import java.util.Map; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; @@ -211,11 +212,11 @@ public class DiscardAi extends SpellAbilityAi { return true; } - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { if (mode == PlayerActionConfirmMode.Random) { // TODO For now AI will always discard Random used currently with: Balduvian Horde and similar cards return true; } - return super.confirmAction(player, sa, mode, message); + return super.confirmAction(player, sa, mode, message, params); } } 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 630c6f71c64..b3fc55fb51a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java @@ -18,6 +18,8 @@ */ package forge.ai.ability; +import java.util.Map; + import forge.ai.AiCostDecision; import forge.ai.AiProps; import forge.ai.ComputerUtil; @@ -102,7 +104,7 @@ public class DrawAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkDiscardCost(ai, cost, source,sa)) { + if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) { AiCostDecision aiDecisions = new AiCostDecision(ai, sa, false); for (final CostPart part : cost.getCostParts()) { if (part instanceof CostDiscard) { @@ -541,7 +543,7 @@ public class DrawAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { int numCards = sa.hasParam("NumCards") ? AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumCards"), sa) : 1; // AI shouldn't mill itself if (numCards < player.getZone(ZoneType.Library).size()) 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 6627b3847c0..a12fc2fe440 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java @@ -70,7 +70,7 @@ public final class EncodeAi extends SpellAbilityAi { * forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { // only try to encode if there is a creature it can be used on return chooseCard(player, player.getCreaturesInPlay(), true) != null; } diff --git a/forge-ai/src/main/java/forge/ai/ability/FlipOntoBattlefieldAi.java b/forge-ai/src/main/java/forge/ai/ability/FlipOntoBattlefieldAi.java index ca37e15340a..3fcee8a8341 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FlipOntoBattlefieldAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FlipOntoBattlefieldAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.base.Predicate; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -42,7 +44,7 @@ public class FlipOntoBattlefieldAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java index 6c6ec70808f..9eb125d2db3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java @@ -31,6 +31,11 @@ public class ImmediateTriggerAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + // always add to stack, targeting happens after payment + if (mandatory) { + return true; + } + String logic = sa.getParamOrDefault("AILogic", ""); SpellAbility trigsa = sa.getAdditionalAbility("Execute"); @@ -45,11 +50,7 @@ public class ImmediateTriggerAi extends SpellAbilityAi { AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); trigsa.setActivatingPlayer(ai); - if (!sa.hasParam("OptionalDecider")) { - return aic.doTrigger(trigsa, true); - } else { - return aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You")); - } + return aic.doTrigger(trigsa, !"You".equals(sa.getParamOrDefault("OptionalDecider", "You"))); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java b/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java index e58cf83d66c..7087208f25c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.SpellAbilityAi; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -20,7 +22,7 @@ public class InvestigateAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/LearnAi.java b/forge-ai/src/main/java/forge/ai/ability/LearnAi.java index d155f55aba9..a155b4f77de 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LearnAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LearnAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.ComputerUtilCard; import forge.ai.PlayerControllerAi; import forge.ai.SpellAbilityAi; @@ -32,7 +34,7 @@ public class LearnAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java b/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java index 1f337d1cd0a..b807069b859 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java @@ -43,7 +43,7 @@ public class ManifestAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } 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 55d133adcfe..05c813613e2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MillAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MillAi.java @@ -196,7 +196,7 @@ public class MillAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { if ("TimmerianFiends".equals(sa.getParam("AILogic"))) { return SpecialCardAi.TimmerianFiends.consider(player, sa); } diff --git a/forge-ai/src/main/java/forge/ai/ability/MutateAi.java b/forge-ai/src/main/java/forge/ai/ability/MutateAi.java index 21d3840ac42..00315019dad 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MutateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MutateAi.java @@ -65,7 +65,7 @@ public class MutateAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java b/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java index 417e12775f7..6ec5340a36f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.ai.SpellApiToAi; @@ -70,7 +72,7 @@ public class PeekAndRevealAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { AbilitySub subAb = sa.getSubAbility(); return subAb != null && SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(player, subAb); } 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 6cccbb0de7e..0c03b5a9d08 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java @@ -131,7 +131,7 @@ public class PlayAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } 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 4db53dcaf13..0747688f615 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -24,6 +24,7 @@ import org.apache.commons.lang3.StringUtils; import java.util.Arrays; import java.util.List; +import java.util.Map; public class PumpAi extends PumpAiBase { @@ -731,7 +732,7 @@ public class PumpAi extends PumpAiBase { if (minus > energy || minus < 1) { continue; // in case the calculation gets messed up somewhere } - source.setSVar("EnergyToPay", "Number$" + minus); + root.setSVar("EnergyToPay", "Number$" + minus); return true; } } @@ -781,7 +782,7 @@ public class PumpAi extends PumpAiBase { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { //TODO Add logic here if necessary but I think the AI won't cast //the spell in the first place if it would curse its own creature //and the pump isn't mandatory 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 6bb36200d39..81da8244c57 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.AiCardMemory; import forge.ai.AiController; import forge.ai.AiProps; @@ -92,7 +94,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { // Confirming this action means shuffling the library if asked. // First, let's check if we can play the top card of the library diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java index 8a7a58d4951..3ba08023ec8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import forge.ai.*; @@ -40,7 +42,7 @@ public class RepeatAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { //TODO add logic to have computer make better choice (ArsenalNut) return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java b/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java index fb78fd03bf2..3cb3cedf5ff 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.SpellAbilityAi; import forge.game.Game; import forge.game.card.Card; @@ -43,7 +45,7 @@ public class RollDiceAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java index 87754161cd9..78341e0afbc 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCost; @@ -175,7 +176,7 @@ public class SacrificeAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } 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 e65b97f8e38..c9bf4e7d1d6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import com.google.common.base.Predicates; import forge.ai.ComputerUtilMana; @@ -132,7 +134,7 @@ public class ScryAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } } 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 4b3451a6855..5a8ac20146a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import java.util.List; +import java.util.Map; import com.google.common.base.Predicate; @@ -264,7 +265,7 @@ public class SetStateAi extends SpellAbilityAi { return true; } - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { // TODO: improve the AI for when it may want to transform something that's optional to transform return isSafeToTransformIntoLegendary(player, sa.getHostCard()); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java b/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java index e4101790445..cf66187b55e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.SpellAbilityAi; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -54,7 +56,7 @@ public class ShuffleAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { // ai could analyze parameter denoting the player to shuffle return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/SkipPhaseAi.java b/forge-ai/src/main/java/forge/ai/ability/SkipPhaseAi.java index d5264c0bdac..908308a29c4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SkipPhaseAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SkipPhaseAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.player.Player; @@ -18,7 +20,7 @@ public class SkipPhaseAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } 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 45bc7c9f67c..0d93d65c3a3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import java.util.Map; + import forge.ai.AiCardMemory; import forge.ai.AiProps; import forge.ai.ComputerUtilCost; @@ -119,7 +121,7 @@ public class SurveilAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } } 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 69fc263cb3a..940c667ecf6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java @@ -106,10 +106,8 @@ public abstract class TapAiBase extends SpellAbilityAi { * @return a boolean. */ protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) { - final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Game game = ai.getGame(); - CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); - tapList = CardLists.getTargetableCards(tapList, sa); + CardCollection tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa); tapList = CardLists.filter(tapList, Presets.UNTAPPED); tapList = CardLists.filter(tapList, new Predicate() { @Override @@ -129,8 +127,7 @@ public abstract class TapAiBase extends SpellAbilityAi { //use broader approach when the cost is a positive thing if (tapList.isEmpty() && ComputerUtil.activateForCost(sa, ai)) { - tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); - tapList = CardLists.getTargetableCards(tapList, sa); + tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa); tapList = CardLists.filter(tapList, new Predicate() { @Override public boolean apply(final Card c) { @@ -150,8 +147,10 @@ public abstract class TapAiBase extends SpellAbilityAi { //try to exclude things that will already be tapped due to something on stack or because something is //already targeted in a parent or sub SA - CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, tapList, sa, ApiType.Tap); - tapList.removeAll(toExclude); + if (!sa.isTrigger() || mandatory) { // but if just confirming trigger no need to look for other targets and might still help anyway + CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, tapList, sa, ApiType.Tap); + tapList.removeAll(toExclude); + } if (tapList.isEmpty()) { return false; @@ -176,6 +175,7 @@ public abstract class TapAiBase extends SpellAbilityAi { } PhaseHandler phase = game.getPhaseHandler(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); Card primeTarget = ComputerUtil.getKilledByTargeting(sa, tapList); if (primeTarget != null) { choice = primeTarget; @@ -193,7 +193,7 @@ public abstract class TapAiBase extends SpellAbilityAi { return CombatUtil.canAttack(c, opp); } }); - attackers.remove(sa.getHostCard()); + attackers.remove(source); } Predicate findBlockers = CardPredicates.possibleBlockerForAtLeastOne(attackers); List creatureList = CardLists.filter(tapList, findBlockers); @@ -202,7 +202,7 @@ public abstract class TapAiBase extends SpellAbilityAi { if (!attackers.isEmpty() && !creatureList.isEmpty()) { choice = ComputerUtilCard.getBestCreatureAI(creatureList); - } else if (sa.getRootAbility().isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) { + } else if (sa.isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) { choice = ComputerUtilCard.getMostExpensivePermanentAI(tapList); } } else if (phase.isPlayerTurn(opp) @@ -272,7 +272,7 @@ public abstract class TapAiBase extends SpellAbilityAi { return true; } - // filter by enchantments and planeswalkers, their tapped state doesn't matter. + // filter by enchantments and planeswalkers, their tapped state (usually) doesn't matter. final String[] tappablePermanents = { "Enchantment", "Planeswalker" }; tapList = CardLists.getValidCards(list, tappablePermanents, source.getController(), source, sa); 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 b1ce0ea688a..16af0fbd221 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java @@ -311,7 +311,7 @@ public class TokenAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) */ @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { // TODO: AILogic return true; } 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 2f27d060eab..894f4ae8394 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java @@ -53,7 +53,7 @@ public class UntapAi extends SpellAbilityAi { return false; } - return ComputerUtilCost.checkDiscardCost(ai, cost, sa.getHostCard(), sa); + return ComputerUtilCost.checkDiscardCost(ai, cost, source, sa); } @Override @@ -174,8 +174,10 @@ public class UntapAi extends SpellAbilityAi { //try to exclude things that will already be untapped due to something on stack or because something is //already targeted in a parent or sub SA - CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, untapList, sa, ApiType.Untap); - untapList.removeAll(toExclude); + if (!sa.isTrigger() || mandatory) { // but if just confirming trigger no need to look for other targets and might still help anyway + CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, untapList, sa, ApiType.Untap); + untapList.removeAll(toExclude); + } sa.resetTargets(); while (sa.canAddMoreTarget()) { @@ -199,7 +201,7 @@ public class UntapAi extends SpellAbilityAi { if (choice == null) { if (CardLists.getNotType(untapList, "Creature").isEmpty()) { choice = ComputerUtilCard.getBestCreatureAI(untapList); // if only creatures take the best - } else if (!sa.getPayCosts().hasManaCost() || sa.getRootAbility().isTrigger() + } else if (!sa.getPayCosts().hasManaCost() || sa.isTrigger() || "Always".equals(sa.getParam("AILogic"))) { choice = ComputerUtilCard.getMostExpensivePermanentAI(untapList); } @@ -290,7 +292,7 @@ public class UntapAi extends SpellAbilityAi { choice = ComputerUtilCard.getBestAI(tapList); if (choice == null) { // can't find anything left - if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().size() == 0) { + if (sa.getTargets().size() < tgt.getMinTargets(source, sa) || sa.getTargets().size() == 0) { if (!mandatory) { sa.resetTargets(); } @@ -310,9 +312,7 @@ public class UntapAi extends SpellAbilityAi { @Override 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()); + PlayerCollection pl = ai.getYourTeam(); return ComputerUtilCard.getBestAI(CardLists.filterControlledBy(list, pl)); } diff --git a/forge-ai/src/main/java/forge/ai/ability/VentureAi.java b/forge-ai/src/main/java/forge/ai/ability/VentureAi.java index 0a3197fac13..12ca13cad58 100644 --- a/forge-ai/src/main/java/forge/ai/ability/VentureAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/VentureAi.java @@ -36,7 +36,7 @@ public class VentureAi extends SpellAbilityAi { } @Override - public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { return true; } 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 4e4b73fdb58..0ab07f2b306 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java @@ -91,6 +91,7 @@ public class GameCopier { newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn()); newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn()); newPlayer.setLifeGainedThisTurn(origPlayer.getLifeGainedThisTurn()); + // TODO creatureAttackedThisTurn for (Mana m : origPlayer.getManaPool()) { newPlayer.getManaPool().addMana(m, false); } @@ -207,6 +208,13 @@ public class GameCopier { private void copyGameState(Game newGame) { newGame.setAge(origGame.getAge()); + + // TODO countersAddedThisTurn + + if (origGame.getMonarch() != null) { + newGame.setMonarch(playerMap.get(origGame.getMonarch())); + } + for (ZoneType zone : ZONES) { for (Card card : origGame.getCardsIn(zone)) { addCard(newGame, zone, card); @@ -300,6 +308,7 @@ public class GameCopier { newCard.setPTCharacterDefiningTable(c.getSetPTCharacterDefiningTable()); newCard.setPTBoost(c.getPTBoostTable()); + // TODO copy by map newCard.setDamage(c.getDamage()); newCard.setChangedCardColors(c.getChangedCardColorsTable()); diff --git a/forge-core/pom.xml b/forge-core/pom.xml index c4264bf6a2b..bf760f3a81a 100644 --- a/forge-core/pom.xml +++ b/forge-core/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.50-SNAPSHOT + 1.6.54-SNAPSHOT forge-core diff --git a/forge-core/src/main/java/forge/ImageKeys.java b/forge-core/src/main/java/forge/ImageKeys.java index 3975890cf9b..a1ac75b988b 100644 --- a/forge-core/src/main/java/forge/ImageKeys.java +++ b/forge-core/src/main/java/forge/ImageKeys.java @@ -3,6 +3,7 @@ package forge; import forge.item.PaperCard; import forge.util.FileUtil; import forge.util.TextUtil; +import forge.util.ThreadUtil; import org.apache.commons.lang3.StringUtils; import java.io.File; @@ -171,10 +172,18 @@ public final class ImageKeys { return file; } //setlookup - file = setLookUpFile(filename, fullborderFile); - if (file != null) { - cachedCards.put(filename, file); - return file; + if (hasSetLookup(filename)) { + //delay processing so gui is responsive + ThreadUtil.delay(60, new Runnable() { + @Override + public void run() { + File f = setLookUpFile(filename, fullborderFile); + if (f != null) + cachedCards.put(filename, f); + else //is null + missingCards.add(filename); + } + }); } } //if an image, like phenomenon or planes is missing .full in their filenames but you have an existing images that have .full/.fullborder @@ -250,7 +259,7 @@ public final class ImageKeys { // System.out.println("File not found, no image created: " + key); //add missing cards - disable for desktop version for compatibility reasons with autodownloader - if (isLibGDXPort) + if (isLibGDXPort && !hasSetLookup(filename)) //missing cards with setlookup is handled differently missingCards.add(filename); return null; } @@ -260,6 +269,13 @@ public final class ImageKeys { ? StaticData.instance().getEditions().getCode2ByCode(edition) // by default 2-letter codes from MWS are used : CACHE_CARD_PICS_SUBDIR.get(edition); // may use custom paths though } + static boolean hasSetLookup(String filename) { + if (!StaticData.instance().getSetLookup().isEmpty()) { + return StaticData.instance().getSetLookup().keySet().stream().anyMatch(setKey -> filename.startsWith(setKey)); + } + + return false; + } private static File setLookUpFile(String filename, String fullborderFile) { if (!StaticData.instance().getSetLookup().isEmpty()) { for (String setKey : StaticData.instance().getSetLookup().keySet()) { diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java index c52a9a8bc27..c6369afec49 100644 --- a/forge-core/src/main/java/forge/card/CardRules.java +++ b/forge-core/src/main/java/forge/card/CardRules.java @@ -214,8 +214,15 @@ public final class CardRules implements ICardCharacteristics { } } + public boolean isEnterableDungeon() { + if (mainPart.getOracleText().contains("You can't enter this dungeon unless")) { + return false; + } + return getType().isDungeon(); + } + public boolean canBeCommander() { - if (mainPart.getOracleText().contains("can be your commander")) { + if (mainPart.getOracleText().contains("can be your commander") || canBeBackground()) { return true; } CardType type = mainPart.getType(); @@ -232,8 +239,15 @@ public final class CardRules implements ICardCharacteristics { } public boolean canBePartnerCommander() { + if (canBeBackground()) { + return true; + } return canBeCommander() && (hasKeyword("Partner") || !this.partnerWith.isEmpty() || - hasKeyword("Friends forever")); + hasKeyword("Friends forever") || hasKeyword("Choose a Background")); + } + + public boolean canBeBackground() { + return mainPart.getType().hasSubtype("Background"); } public boolean canBeOathbreaker() { diff --git a/forge-core/src/main/java/forge/card/CardRulesPredicates.java b/forge-core/src/main/java/forge/card/CardRulesPredicates.java index 16b5ed5b974..ea772e340df 100644 --- a/forge-core/src/main/java/forge/card/CardRulesPredicates.java +++ b/forge-core/src/main/java/forge/card/CardRulesPredicates.java @@ -209,6 +209,16 @@ public final class CardRulesPredicates { }; } + public static Predicate deckHasExactly(final DeckHints.Type type, final String has[]) { + return new Predicate() { + @Override + public boolean apply(final CardRules card) { + DeckHints deckHas = card.getAiHints().getDeckHas(); + return deckHas != null && deckHas.isValid() && deckHas.is(type, has); + } + }; + } + /** * Core type. * diff --git a/forge-core/src/main/java/forge/card/CardType.java b/forge-core/src/main/java/forge/card/CardType.java index 3fb3e1b4994..6082b5f8000 100644 --- a/forge-core/src/main/java/forge/card/CardType.java +++ b/forge-core/src/main/java/forge/card/CardType.java @@ -33,6 +33,7 @@ import com.google.common.base.Predicate; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @@ -50,6 +51,7 @@ public final class CardType implements Comparable, CardTypeView { private static final long serialVersionUID = 4629853583167022151L; public static final CardTypeView EMPTY = new CardType(false); + private static final Set multiWordTypes = ImmutableSet.of("Serra's Realm", "Bolas's Meditation Realm", "Dungeon Master"); public enum CoreType { Artifact(true, "artifacts"), @@ -71,6 +73,7 @@ public final class CardType implements Comparable, CardTypeView { public final String pluralName; private static Map stringToCoreType = EnumUtils.getEnumMap(CoreType.class); private static final Set allCoreTypeNames = stringToCoreType.keySet(); + public static final Set spellTypes = ImmutableSet.of(Instant, Sorcery); public static CoreType getEnum(String name) { return stringToCoreType.get(name); @@ -535,7 +538,8 @@ public final class CardType implements Comparable, CardTypeView { newType = new CardType(CardType.this); if (ct.isRemoveCardTypes()) { - newType.coreTypes.clear(); + // 205.1a However, an object with either the instant or sorcery card type retains that type. + newType.coreTypes.retainAll(CoreType.spellTypes); } if (ct.isRemoveSuperTypes()) { newType.supertypes.clear(); @@ -612,6 +616,9 @@ public final class CardType implements Comparable, CardTypeView { if (!isPlaneswalker()) { Iterables.removeIf(subtypes, Predicates.IS_WALKER_TYPE); } + if (!isDungeon()) { + Iterables.removeIf(subtypes, Predicates.IS_DUNGEON_TYPE); + } } @Override @@ -746,12 +753,14 @@ public final class CardType implements Comparable, CardTypeView { while (hasMoreTypes) { final String type = typeText.substring(iTypeStart, iSpace == -1 ? typeText.length() : iSpace); hasMoreTypes = iSpace != -1; - if (!isMultiwordType(type) || !hasMoreTypes) { - iTypeStart = iSpace + 1; - if (!"-".equals(type)) { - result.add(type); - } + final String rest = typeText.substring(iTypeStart); + if (isMultiwordType(rest)) { + result.add(rest); + break; } + + iTypeStart = iSpace + 1; + result.add(type); iSpace = typeText.indexOf(space, iSpace + 1); } return result; @@ -769,13 +778,7 @@ public final class CardType implements Comparable, CardTypeView { } private static boolean isMultiwordType(final String type) { - final String[] multiWordTypes = { "Serra's Realm", "Bolas's Meditation Realm", "Dungeon Master" }; - for (int i = 0; i < multiWordTypes.length; ++i) { - if (multiWordTypes[i].startsWith(type) && !multiWordTypes[i].equals(type)) { - return true; - } - } - return false; + return multiWordTypes.contains(type); } public static class Constant { @@ -787,6 +790,7 @@ public final class CardType implements Comparable, CardTypeView { public static final Set ENCHANTMENT_TYPES = Sets.newHashSet(); public static final Set ARTIFACT_TYPES = Sets.newHashSet(); public static final Set WALKER_TYPES = Sets.newHashSet(); + public static final Set DUNGEON_TYPES = Sets.newHashSet(); // singular -> plural public static final BiMap pluralTypes = HashBiMap.create(); @@ -846,6 +850,12 @@ public final class CardType implements Comparable, CardTypeView { return CardType.isAPlaneswalkerType(input); } }; + public static Predicate IS_DUNGEON_TYPE = new Predicate() { + @Override + public boolean apply(String input) { + return CardType.isADungeonType(input); + } + }; } ///////// Utility methods @@ -878,6 +888,7 @@ public final class CardType implements Comparable, CardTypeView { sortedSubTypes.addAll(Constant.ENCHANTMENT_TYPES); sortedSubTypes.addAll(Constant.ARTIFACT_TYPES); sortedSubTypes.addAll(Constant.WALKER_TYPES); + sortedSubTypes.addAll(Constant.DUNGEON_TYPES); Collections.sort(sortedSubTypes); } return sortedSubTypes; @@ -933,6 +944,9 @@ public final class CardType implements Comparable, CardTypeView { return (Constant.SPELL_TYPES.contains(cardType)); } + public static boolean isADungeonType(final String cardType) { + return (Constant.DUNGEON_TYPES.contains(cardType)); + } /** * If the input is a plural type, return the corresponding singular form. * Otherwise, simply return the input. diff --git a/forge-core/src/main/java/forge/card/DeckHints.java b/forge-core/src/main/java/forge/card/DeckHints.java index 4ede03d9b88..a55426cfd95 100644 --- a/forge-core/src/main/java/forge/card/DeckHints.java +++ b/forge-core/src/main/java/forge/card/DeckHints.java @@ -14,6 +14,7 @@ import com.google.common.collect.Iterables; import forge.item.PaperCard; import forge.util.PredicateString.StringOp; +import forge.util.collect.FCollection; /** * DeckHints provides the ability for a Card to "want" another Card or type of @@ -73,12 +74,26 @@ public class DeckHints { return false; } for (Pair filter : filters) { - if (filter.getLeft() == type && filter.getRight().equals(hint)) { + if (filter.getLeft() == type && filter.getRight().contains(hint)) { return true; } } return false; } + public boolean is(Type type, String hints[]) { + if (filters == null) { + return false; + } + for (String hint : hints) { + for (Pair filter : filters) { + if (filter.getLeft() == type && filter.getRight().equals(hint)) { + continue; + } + } + return false; + } + return true; + } /** * Returns a Map of Cards by Type from the given Iterable that match this @@ -95,6 +110,10 @@ public class DeckHints { String param = pair.getRight(); Iterable cards = getCardsForFilter(cardList, type, param); if (cards != null) { + // if a type is used more than once intersect respective matches + if (ret.get(type) != null) { + Iterables.retainAll(cards, new FCollection<>(ret.get(type))); + } ret.put(type, cards); } } @@ -143,13 +162,16 @@ public class DeckHints { private Iterable getCardsForFilter(Iterable cardList, Type type, String param) { List cards = new ArrayList<>(); + + // this is case ABILITY, but other types can also use this when the implicit parsing would miss + String[] abilities = param.split("\\|"); + for (String ability : abilities) { + Iterables.addAll(cards, getMatchingItems(cardList, CardRulesPredicates.deckHas(type, ability), PaperCard.FN_GET_RULES)); + } + // bonus if a DeckHas can satisfy the type with multiple ones + Iterables.addAll(cards, getMatchingItems(cardList, CardRulesPredicates.deckHasExactly(type, abilities), PaperCard.FN_GET_RULES)); + switch (type) { - case ABILITY: - String[] abilities = param.split("\\|"); - for (String ability : abilities) { - Iterables.addAll(cards, getMatchingItems(cardList, CardRulesPredicates.deckHas(Type.ABILITY, ability), PaperCard.FN_GET_RULES)); - } - break; case COLOR: String[] colors = param.split("\\|"); for (String color : colors) { @@ -187,6 +209,7 @@ public class DeckHints { } break; case NONE: + case ABILITY: // already done above break; } return cards; diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java index 38ea9d2b2cd..4d05053adb4 100644 --- a/forge-core/src/main/java/forge/deck/DeckFormat.java +++ b/forge-core/src/main/java/forge/deck/DeckFormat.java @@ -262,6 +262,9 @@ public enum DeckFormat { } else if (a.getRules().hasKeyword("Friends forever") && b.getRules().hasKeyword("Friends forever")) { // Stranger Things Secret Lair gimmick partner commander + } else if (a.getRules().hasKeyword("Choose a Background") && b.getRules().canBeBackground() + || b.getRules().hasKeyword("Choose a Background") && a.getRules().canBeBackground()) { + // commander with background } else { return "has an illegal commander partnership"; } diff --git a/forge-core/src/main/java/forge/util/Localizer.java b/forge-core/src/main/java/forge/util/Localizer.java index 36acde1432a..173e936ce49 100644 --- a/forge-core/src/main/java/forge/util/Localizer.java +++ b/forge-core/src/main/java/forge/util/Localizer.java @@ -76,13 +76,19 @@ public class Localizer { return defaultValue; } } + public String getEnglishMessage(final String key, final Object... messageArguments) { + return getMessage(true, key, messageArguments); + } //FIXME: localizer should return default value from english locale or it will crash some GUI element like the NewGameMenu->NewGameScreen Popup when returned null... public String getMessage(final String key, final Object... messageArguments) { + return getMessage(false, key, messageArguments); + } + public String getMessage(final boolean forcedEnglish, final String key, final Object... messageArguments) { MessageFormat formatter = null; try { //formatter = new MessageFormat(resourceBundle.getString(key.toLowerCase()), locale); - formatter = new MessageFormat(english ? englishBundle.getString(key) : resourceBundle.getString(key), english ? Locale.ENGLISH : locale); + formatter = new MessageFormat(english || forcedEnglish ? englishBundle.getString(key) : resourceBundle.getString(key), english || forcedEnglish ? Locale.ENGLISH : locale); } catch (final IllegalArgumentException | MissingResourceException e) { if (!silent) e.printStackTrace(); @@ -95,12 +101,12 @@ public class Localizer { silent = false; - formatter.setLocale(english ? Locale.ENGLISH : locale); + formatter.setLocale(english || forcedEnglish ? Locale.ENGLISH : locale); String formattedMessage = "CHAR ENCODING ERROR"; final String[] charsets = { "ISO-8859-1", "UTF-8" }; //Support non-English-standard characters - String detectedCharset = charset(english ? englishBundle.getString(key) : resourceBundle.getString(key), charsets); + String detectedCharset = charset(english || forcedEnglish ? englishBundle.getString(key) : resourceBundle.getString(key), charsets); final int argLength = messageArguments.length; Object[] syncEncodingMessageArguments = new Object[argLength]; @@ -149,7 +155,7 @@ public class Localizer { englishBundle = ResourceBundle.getBundle("en-US", new Locale("en", "US"), loader); } catch (NullPointerException | MissingResourceException e) { //If the language can't be loaded, default to US English - resourceBundle = ResourceBundle.getBundle("en-US", new Locale("en", "US"), loader); + resourceBundle = ResourceBundle.getBundle("en-US", new Locale("en_US"), loader); e.printStackTrace(); } diff --git a/forge-game/pom.xml b/forge-game/pom.xml index c826bb3edaa..0012361fa80 100644 --- a/forge-game/pom.xml +++ b/forge-game/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.50-SNAPSHOT + 1.6.54-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 1fd4d1d2aa7..b393a4d389c 100644 --- a/forge-game/src/main/java/forge/game/CardTraitBase.java +++ b/forge-game/src/main/java/forge/game/CardTraitBase.java @@ -20,6 +20,7 @@ import forge.game.card.CardPredicates; import forge.game.card.CardState; import forge.game.card.CardView; import forge.game.card.IHasCardView; +import forge.game.keyword.KeywordInterface; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; @@ -34,6 +35,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView, /** The host card. */ protected Card hostCard; protected CardState cardState = null; + protected KeywordInterface keyword = null; /** The map params. */ protected Map originalMapParams = Maps.newHashMap(), @@ -55,15 +57,13 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView, /** Keys of descriptive (text) parameters. */ private static final ImmutableList descriptiveKeys = ImmutableList.builder() .add("Description", "SpellDescription", "StackDescription", "TriggerDescription").build(); - /** Keys to be followed as SVar names when changing text. */ - private static final ImmutableList mutableKeys = ImmutableList.builder() - .add("AddAbility").build(); /** * Keys that should not changed */ private static final ImmutableList noChangeKeys = ImmutableList.builder() - .add("TokenScript", "LegacyImage", "TokenImage", "NewName", "ChooseFromList").build(); + .add("TokenScript", "LegacyImage", "TokenImage", "NewName", "ChooseFromList") + .add("AddAbility").build(); /** *

@@ -140,6 +140,14 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView, this.hostCard = c; } + public KeywordInterface getKeyword() { + return this.keyword; + } + + public void setKeyword(final KeywordInterface kw) { + this.keyword = kw; + } + /** *

* isSecondary. @@ -259,6 +267,18 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView, } if (params.containsKey("Revolt")) { if ("True".equalsIgnoreCase(params.get("Revolt")) != hostController.hasRevolt()) return false; + else if ("None".equalsIgnoreCase(params.get("Revolt"))) { + boolean none = true; + for (Player p : game.getRegisteredPlayers()) { + if (p.hasRevolt()) { + none = false; + break; + } + } + if (!none) { + return false; + } + } } if (params.containsKey("Desert")) { if ("True".equalsIgnoreCase(params.get("Desert")) != hostController.hasDesert()) return false; @@ -422,6 +442,16 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView, if (!Expressions.compare(sVar, svarOperator, operandValue)) { return false; } + if (hasParam("CheckSecondSVar")) { + final int sVar2 = AbilityUtils.calculateAmount(this.hostCard, getParam("CheckSecondSVar"), this); + final String comparator2 = getParamOrDefault("SecondSVarCompare", "GE1"); + final String svarOperator2 = comparator2.substring(0, 2); + final String svarOperand2 = comparator2.substring(2); + final int operandValue2 = AbilityUtils.calculateAmount(this.hostCard, svarOperand2, this); + if (!Expressions.compare(sVar2, svarOperator2, operandValue2)) { + return false; + } + } } if (params.containsKey("ManaSpent")) { @@ -489,11 +519,6 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView, } else if (descriptiveKeys.contains(key)) { // change descriptions differently newValue = AbilityUtils.applyDescriptionTextChangeEffects(value, this); - } else if (mutableKeys.contains(key)) { - // follow SVar and change it - final String originalSVarValue = hostCard.getSVar(value); - hostCard.changeSVar(value, AbilityUtils.applyAbilityTextChangeEffects(originalSVarValue, this)); - newValue = null; } else if (this.getHostCard().hasSVar(value)) { // don't change literal SVar names! newValue = null; @@ -652,6 +677,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView, copy.setCardState(cardState); // dont use setHostCard to not trigger the not copied parts yet copy.hostCard = host; + copy.keyword = this.keyword; } abstract public List getTriggerRemembered(); diff --git a/forge-game/src/main/java/forge/game/ForgeScript.java b/forge-game/src/main/java/forge/game/ForgeScript.java index d7a41fd52dc..3568ca5e9ac 100644 --- a/forge-game/src/main/java/forge/game/ForgeScript.java +++ b/forge-game/src/main/java/forge/game/ForgeScript.java @@ -202,6 +202,11 @@ public class ForgeScript { return sa.hasParam("Nightbound"); } else if (property.equals("paidPhyrexianMana")) { return sa.getSpendPhyrexianMana(); + } else if (property.startsWith("ManaSpent")) { + String[] k = property.split(" ", 2); + String comparator = k[1].substring(0, 2); + int y = AbilityUtils.calculateAmount(sa.getHostCard(), k[1].substring(2), sa); + return Expressions.compare(sa.getPayingMana().size(), comparator, y); } else if (property.startsWith("ManaFrom")) { final String fromWhat = property.substring(8); boolean found = false; @@ -247,7 +252,7 @@ public class ForgeScript { } else if (property.startsWith("cmc")) { int y = 0; // spell was on the stack - if (sa.getCardState().getCard().isInZone(ZoneType.Stack)) { + if (sa.getHostCard().isInZone(ZoneType.Stack)) { y = sa.getHostCard().getCMC(); } else { y = sa.getPayCosts().getTotalMana().getCMC(); diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 81d6bc5600f..a8fda242d85 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -47,6 +48,7 @@ import forge.game.ability.AbilityKey; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; +import forge.game.card.CardDamageHistory; import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.card.CardUtil; @@ -78,6 +80,7 @@ import forge.trackable.Tracker; import forge.util.Aggregates; import forge.util.MyRandom; import forge.util.Visitor; +import forge.util.collect.FCollection; /** * Represents the state of a single game, a new instance is created for each game. @@ -119,10 +122,14 @@ public class Game { private Table>> countersAddedThisTurn = HashBasedTable.create(); + private FCollection globalDamageHistory = new FCollection<>(); + private IdentityHashMap, Pair> damageThisTurnLKI = new IdentityHashMap<>(); + private Map topLibsCast = Maps.newHashMap(); private Map facedownWhileCasting = Maps.newHashMap(); private Player monarch = null; + private Player initiative = null; private Player monarchBeginTurn = null; private Player startingPlayer; @@ -173,6 +180,13 @@ public class Game { this.monarchBeginTurn = monarchBeginTurn; } + public Player getHasInitiative() { + return initiative; + } + public void setHasInitiative(final Player p ) { + initiative = p; + } + public CardCollectionView getLastStateBattlefield() { return lastStateBattlefield; } @@ -243,7 +257,7 @@ public class Game { if (c == null) { return null; } - return changeZoneLKIInfo.containsKey(c.getId()) ? changeZoneLKIInfo.get(c.getId()) : c; + return changeZoneLKIInfo.getOrDefault(c.getId(), c); } public final void clearChangeZoneLKIInfo() { changeZoneLKIInfo.clear(); @@ -846,6 +860,18 @@ public class Game { } } + if (p.hasInitiative()) { + // The third way to take the initiative is if the player who currently has the initiative leaves the game. + // When that happens, the player whose turn it is takes the initiative. + // If the player who has the initiative leaves the game on their own turn, + // or the active player left the game at the same time, the next player in turn order takes the initiative. + if (p.equals(getPhaseHandler().getPlayerTurn())) { + getAction().takeInitiative(getNextPlayerAfter(p), null); + } else { + getAction().takeInitiative(getPhaseHandler().getPlayerTurn(), null); + } + } + // Remove leftover items from getStack().removeInstancesControlledBy(p); @@ -1068,6 +1094,7 @@ public class Game { public void onCleanupPhase() { clearCounterAddedThisTurn(); + clearGlobalDamageHistory(); // some cards need this info updated even after a player lost, so don't skip them for (Player player : getRegisteredPlayers()) { player.onCleanupPhase(); @@ -1109,6 +1136,48 @@ public class Game { countersAddedThisTurn.clear(); } + /** + * Gets the damage instances done this turn. + * @param isCombat if true only combat damage matters, pass null for both + * @param anyIsEnough if true returns early once result has an entry + * @param validSourceCard + * @param validTargetEntity + * @param source + * @param sourceController + * @param ctb + * @return List for each source + */ + public List getDamageDoneThisTurn(Boolean isCombat, boolean anyIsEnough, String validSourceCard, String validTargetEntity, Card source, Player sourceController, CardTraitBase ctb) { + final List dmgList = Lists.newArrayList(); + for (CardDamageHistory cdh : globalDamageHistory) { + int dmg = cdh.getDamageDoneThisTurn(isCombat, anyIsEnough, validSourceCard, validTargetEntity, source, sourceController, ctb); + if (dmg == 0) { + continue; + } + + dmgList.add(dmg); + + if (anyIsEnough) { + break; + } + } + + return dmgList; + } + + public void addGlobalDamageHistory(CardDamageHistory cdh, Pair dmg, Card source, GameEntity target) { + globalDamageHistory.add(cdh); + damageThisTurnLKI.put(dmg, Pair.of(source, target)); + } + public void clearGlobalDamageHistory() { + globalDamageHistory.clear(); + damageThisTurnLKI.clear(); + } + + public Pair getDamageLKI(Pair dmg) { + return damageThisTurnLKI.get(dmg); + } + public Card getTopLibForPlayer(Player P) { return topLibsCast.get(P); } diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 57fce01e2a6..257c33cd3f0 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -27,7 +27,6 @@ import java.util.Set; import forge.util.*; import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; import com.google.common.base.Predicate; import com.google.common.collect.ComparisonChain; @@ -37,6 +36,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; +import com.google.common.collect.Table; import forge.GameCommand; import forge.StaticData; @@ -66,6 +66,7 @@ import forge.game.event.GameEventGameStarted; import forge.game.event.GameEventScry; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; +import forge.game.keyword.KeywordsChange; import forge.game.mulligan.MulliganService; import forge.game.player.GameLossReason; import forge.game.player.Player; @@ -161,14 +162,32 @@ public class GameAction { // need to check before it enters if (c.isAura() && !c.isAttachedToEntity() && toBattlefield && (zoneFrom == null || !zoneFrom.is(ZoneType.Stack))) { boolean found = false; - if (Iterables.any(game.getPlayers(), PlayerPredicates.canBeAttached(c, null))) { - found = true; + try { + if (Iterables.any(game.getPlayers(), PlayerPredicates.canBeAttached(c, null))) { + found = true; + } + } catch (Exception e1) { + found = false; } - else if (Iterables.any((CardCollectionView) params.get(AbilityKey.LastStateBattlefield), CardPredicates.canBeAttached(c, null))) { - found = true; + + if (!found) { + try { + if (Iterables.any((CardCollectionView) params.get(AbilityKey.LastStateBattlefield), CardPredicates.canBeAttached(c, null))) { + found = true; + } + } catch (Exception e2) { + found = false; + } } - else if (Iterables.any((CardCollectionView) params.get(AbilityKey.LastStateGraveyard), CardPredicates.canBeAttached(c, null))) { - found = true; + + if (!found) { + try { + if (Iterables.any((CardCollectionView) params.get(AbilityKey.LastStateGraveyard), CardPredicates.canBeAttached(c, null))) { + found = true; + } + } catch (Exception e3) { + found = false; + } } if (!found) { c.clearControllers(); @@ -233,13 +252,6 @@ public class GameAction { } } - // Clean up the temporary Dash/Blitz SVar when the card leaves the battlefield - // Clean up the temporary AtEOT SVar - String endofTurn = c.getSVar("EndOfTurnLeavePlay"); - if (fromBattlefield && (endofTurn.equals("Dash") || endofTurn.equals("Blitz") || endofTurn.equals("AtEOT"))) { - c.removeSVar("EndOfTurnLeavePlay"); - } - if (fromBattlefield && !toBattlefield) { c.getController().setRevolt(true); } @@ -253,10 +265,6 @@ public class GameAction { lastKnownInfo = CardUtil.getLKICopy(c); } - if (!suppress) { - copied.setTimestamp(game.getNextTimestamp()); - } - if (!lastKnownInfo.hasKeyword("Counters remain on CARDNAME as it moves to any zone other than a player's hand or library.")) { copied.clearCounters(); } @@ -281,6 +289,8 @@ public class GameAction { copied = CardFactory.copyCard(c, false); } + copied.setTimestamp(c.getTimestamp()); + if (zoneTo.is(ZoneType.Stack)) { // when moving to stack, copy changed card information copied.setChangedCardColors(c.getChangedCardColorsTable()); @@ -293,7 +303,6 @@ public class GameAction { copied.setDrawnThisTurn(c.getDrawnThisTurn()); copied.copyChangedTextFrom(c); - copied.setTimestamp(c.getTimestamp()); // clean up changes that come from its own static abilities copied.cleanupCopiedChangesFrom(c); @@ -305,15 +314,20 @@ public class GameAction { // copy bestow timestamp copied.setBestowTimestamp(c.getBestowTimestamp()); + + if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) { + copied.setCastSA(cause); + KeywordInterface kw = cause.getKeyword(); + if (kw != null) { + copied.addKeywordForStaticAbility(kw); + } + } } else { // when a card leaves the battlefield, ensure it's in its original state // (we need to do this on the object before copying it, or it won't work correctly e.g. // on Transformed objects) copied.setState(CardStateName.Original, false); copied.setBackSide(false); - - // reset timestamp in changezone effects so they have same timestamp if ETB simultaneously - copied.setTimestamp(game.getNextTimestamp()); } copied.setUnearthed(c.isUnearthed()); @@ -337,7 +351,7 @@ public class GameAction { CardCollectionView comCards = c.getOwner().getCardsIn(ZoneType.Command); for (final Card effCard : comCards) { for (final ReplacementEffect re : effCard.getReplacementEffects()) { - if (re.hasSVar("CommanderMoveReplacement") && effCard.getEffectSource().getName().equals(c.getRealCommander().getName())) { + if (re.hasParam("CommanderMoveReplacement") && c.getMergedCards().contains(effCard.getEffectSource())) { commanderEffect = effCard; break; } @@ -345,8 +359,10 @@ public class GameAction { if (commanderEffect != null) break; } // Disable the commander replacement effect - for (final ReplacementEffect re : commanderEffect.getReplacementEffects()) { - re.setSuppressed(true); + if (commanderEffect != null) { + for (final ReplacementEffect re : commanderEffect.getReplacementEffects()) { + re.setSuppressed(true); + } } } @@ -365,7 +381,7 @@ public class GameAction { } ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.Moved, repParams); - if (repres != ReplacementResult.NotReplaced) { + if (repres != ReplacementResult.NotReplaced && repres != ReplacementResult.Updated) { // reset failed manifested Cards back to original if (c.isManifested() && !c.isInPlay()) { c.forceTurnFaceUp(); @@ -395,6 +411,11 @@ public class GameAction { } } + if (!zoneTo.is(ZoneType.Stack) && !suppress) { + // reset timestamp in changezone effects so they have same timestamp if ETB simultaneously + copied.setTimestamp(game.getNextTimestamp()); + } + copied.getOwner().removeInboundToken(copied); // Aura entering as Copy from stack @@ -489,6 +510,37 @@ public class GameAction { if (!zoneTo.is(ZoneType.Exile) && !zoneTo.is(ZoneType.Stack)) { c.cleanupExiledWith(); } + + // 400.7a Effects from static abilities that give a permanent spell on the stack an ability + // that allows it to be cast for an alternative cost continue to apply to the permanent that spell becomes. + if (zoneFrom.is(ZoneType.Stack) && toBattlefield) { + List newKw = Lists.newArrayList(); + for (Table.Cell cell : c.getChangedCardKeywords().cellSet()) { + // comes from a static ability + if (cell.getColumnKey() == 0) { + continue; + } + for (KeywordInterface ki : cell.getValue().getKeywords()) { + boolean keepKeyword = false; + for (SpellAbility sa : ki.getAbilities()) { + if (!sa.isSpell()) { + continue; + } + if (sa.getAlternativeCost() != null) { + keepKeyword = true; + break; + } + } + if (keepKeyword) { + ki.setHostCard(copied); + newKw.add(ki); + } + } + } + if (!newKw.isEmpty()) { + copied.addChangedCardKeywordsInternal(newKw, null, false, copied.getTimestamp(), 0, true); + } + } } // if an adventureCard is put from Stack somewhere else, need to reset to Original State @@ -498,15 +550,6 @@ public class GameAction { GameEntityCounterTable table = new GameEntityCounterTable(); - // need to suspend cards own replacement effects - if (!suppress) { - if (toBattlefield && !copied.getEtbCounters().isEmpty()) { - for (final ReplacementEffect re : copied.getReplacementEffects()) { - re.setSuppressed(true); - } - } - } - if (mergedCards != null) { // Move components of merged permanent here // Also handle 723.3e and 903.9a @@ -553,14 +596,9 @@ public class GameAction { } // do ETB counters after zone add - if (!suppress) { - if (toBattlefield) { - copied.putEtbCounters(table); - // enable replacement effects again - for (final ReplacementEffect re : copied.getReplacementEffects()) { - re.setSuppressed(false); - } - } + if (!suppress && toBattlefield && !copied.getEtbCounters().isEmpty()) { + game.getTriggerHandler().registerActiveTrigger(copied, false); + copied.putEtbCounters(table); copied.clearEtbCounters(); } @@ -581,7 +619,7 @@ public class GameAction { } } - table.replaceCounterEffect(game, null, true); + table.replaceCounterEffect(game, null, true, true, params); // Need to apply any static effects to produce correct triggers checkStaticAbilities(); @@ -1059,6 +1097,24 @@ public class GameAction { return holdCheckingStaticAbilities; } + // This doesn't check layers or if the ability gets removed by other effects + public boolean hasStaticAbilityAffectingZone(ZoneType zone, StaticAbilityLayer layer) { + for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.getParam("Mode").equals("Continuous") || stAb.isSuppressed() || !stAb.checkConditions()) { + continue; + } + if (layer != null && !stAb.getLayers().contains(layer)) { + continue; + } + if (ZoneType.listValueOf(stAb.getParamOrDefault("AffectedZone", ZoneType.Battlefield.toString())).contains(zone)) { + return true; + } + } + } + return false; + } + public final void checkStaticAbilities() { checkStaticAbilities(true); } @@ -1183,26 +1239,26 @@ public class GameAction { } } - for (Player p : game.getPlayers()) { - for (Card c : p.getCardsIn(ZoneType.Battlefield).threadSafeIterable()) { - if (!c.getController().equals(p)) { - controllerChangeZoneCorrection(c); - affectedCards.add(c); - } - if (c.isCreature() && c.isPaired()) { - Card partner = c.getPairedWith(); - if (!partner.isCreature() || c.getController() != partner.getController() || !c.isInPlay()) { - c.setPairedWith(null); - partner.setPairedWith(null); - affectedCards.add(c); - } - } - } - } - // preList means that this is run by a pre Check with LKI objects // in that case Always trigger should not Run if (preList.isEmpty()) { + for (Player p : game.getPlayers()) { + for (Card c : p.getCardsIn(ZoneType.Battlefield).threadSafeIterable()) { + if (!c.getController().equals(p)) { + controllerChangeZoneCorrection(c); + affectedCards.add(c); + } + if (c.isCreature() && c.isPaired()) { + Card partner = c.getPairedWith(); + if (!partner.isCreature() || c.getController() != partner.getController() || !c.isInPlay()) { + c.setPairedWith(null); + partner.setPairedWith(null); + affectedCards.add(c); + } + } + } + } + final Map runParams = AbilityKey.newMap(); game.getTriggerHandler().runTrigger(TriggerType.Always, runParams, false); @@ -1217,6 +1273,8 @@ public class GameAction { c.updateAbilityTextForView(); // only update keywords and text for view to avoid flickering } + // TODO filter out old copies from zone change + if (runEvents && !affectedCards.isEmpty()) { game.fireEvent(new GameEventCardStatsChanged(affectedCards)); } @@ -1293,33 +1351,13 @@ public class GameAction { noRegCreats.add(c); 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.")) { - // merge entries with same source - List dmgList = Lists.newArrayList(); - List> remainingDamaged = Lists.newArrayList(c.getReceivedDamageFromThisTurn()); - while (!remainingDamaged.isEmpty()) { - Pair damaged = remainingDamaged.get(0); - int sum = damaged.getRight(); - remainingDamaged.remove(damaged); - for (Pair other : Lists.newArrayList(remainingDamaged)) { - if (other.getLeft().equalsWithTimestamp(damaged.getLeft())) { - sum += other.getRight(); - // once it got counted keep it out - remainingDamaged.remove(other); - } - } - dmgList.add(sum); - } - - for (final Integer dmg : dmgList) { - if (c.getLethal() <= dmg.intValue() || c.hasBeenDealtDeathtouchDamage()) { - if (desCreats == null) { - desCreats = new CardCollection(); - } - desCreats.add(c); - c.setHasBeenDealtDeathtouchDamage(false); - checkAgain = true; - break; + if (c.getLethal() <= c.getMaxDamageFromSource() || c.hasBeenDealtDeathtouchDamage()) { + if (desCreats == null) { + desCreats = new CardCollection(); } + desCreats.add(c); + c.setHasBeenDealtDeathtouchDamage(false); + checkAgain = true; } } // Rule 704.5g - Destroy due to lethal damage @@ -1546,7 +1584,7 @@ public class GameAction { c.getGame().getTracker().flush(); c.setMoveToCommandZone(false); - if (c.getOwner().getController().confirmAction(c.getFirstSpellAbility(), PlayerActionConfirmMode.ChangeZoneToAltDestination, c.getName() + ": If a commander is in a graveyard or in exile and that card was put into that zone since the last time state-based actions were checked, its owner may put it into the command zone.")) { + if (c.getOwner().getController().confirmAction(c.getFirstSpellAbility(), PlayerActionConfirmMode.ChangeZoneToAltDestination, c.getName() + ": If a commander is in a graveyard or in exile and that card was put into that zone since the last time state-based actions were checked, its owner may put it into the command zone.", null)) { moveTo(c.getOwner().getZone(ZoneType.Command), c, null); return true; } @@ -2203,6 +2241,32 @@ public class GameAction { game.getTriggerHandler().runTrigger(TriggerType.BecomeMonarch, runParams, false); } + public void takeInitiative(final Player p, final String set) { + final Player previous = game.getHasInitiative(); + if (p == null) { + return; + } + + if (!p.equals(previous)) { + if (previous != null) { + previous.removeInitiativeEffect(); + } + + if (p.hasLost()) { // the person who should take initiative is gone, it goes to next player + takeInitiative(game.getNextPlayerAfter(p), set); + } + + game.setHasInitiative(p); + p.createInitiativeEffect(set); + } + + // You can take the initiative even if you already have it + // Run triggers + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Player, p); + game.getTriggerHandler().runTrigger(TriggerType.TakesInitiative, runParams, false); + } + // Make scry an action function so that it can be used for mulligans (with a null cause) // Assumes that the list of players is in APNAP order, which should be the case // Optional here as well to handle the way that mulligans do the choice @@ -2271,13 +2335,16 @@ public class GameAction { final Player p = e.getKey(); final CardCollection toTop = e.getValue().getLeft(); final CardCollection toBottom = e.getValue().getRight(); + int numLookedAt = 0; if (toTop != null) { + numLookedAt += toTop.size(); Collections.reverse(toTop); // reverse to get the correct order for (Card c : toTop) { moveToLibrary(c, cause, null); } } if (toBottom != null) { + numLookedAt += toBottom.size(); for (Card c : toBottom) { moveToBottomOfLibrary(c, cause, null); } @@ -2287,6 +2354,7 @@ public class GameAction { // set up triggers (but not actually do them until later) final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.Player, p); + runParams.put(AbilityKey.ScryNum, numLookedAt); game.getTriggerHandler().runTrigger(TriggerType.Scry, runParams, false); } } @@ -2308,6 +2376,7 @@ public class GameAction { game.getReplacementHandler().runReplaceDamage(isCombat, damageMap, preventMap, counterTable, cause); Map lethalDamage = Maps.newHashMap(); + Map lkiCache = Maps.newHashMap(); // Actually deal damage according to replaced damage map for (Map.Entry> et : damageMap.rowMap().entrySet()) { @@ -2334,6 +2403,8 @@ public class GameAction { e.setValue(Integer.valueOf(e.getKey().addDamageAfterPrevention(e.getValue(), sourceLKI, isCombat, counterTable))); sum += e.getValue(); + + sourceLKI.getDamageHistory().registerDamage(e.getValue(), isCombat, sourceLKI, e.getKey(), lkiCache); } if (sum > 0 && sourceLKI.hasKeyword(Keyword.LIFELINK)) { diff --git a/forge-game/src/main/java/forge/game/GameActionUtil.java b/forge-game/src/main/java/forge/game/GameActionUtil.java index 3f563ff302a..f7574571163 100644 --- a/forge-game/src/main/java/forge/game/GameActionUtil.java +++ b/forge-game/src/main/java/forge/game/GameActionUtil.java @@ -42,17 +42,12 @@ import forge.game.player.PlayerController; import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementHandler; import forge.game.replacement.ReplacementLayer; -import forge.game.spellability.AbilityManaPart; -import forge.game.spellability.AbilitySub; -import forge.game.spellability.AlternativeCost; -import forge.game.spellability.OptionalCost; -import forge.game.spellability.OptionalCostValue; -import forge.game.spellability.Spell; -import forge.game.spellability.SpellAbility; -import forge.game.spellability.SpellAbilityRestriction; +import forge.game.spellability.*; +import forge.game.staticability.StaticAbilityLayer; 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; import forge.util.Lang; import forge.util.TextUtil; @@ -91,7 +86,11 @@ public final class GameActionUtil { Card source = sa.getHostCard(); final Game game = source.getGame(); - if (sa.isSpell() && !source.isInPlay()) { + if (sa.isSpell() && source.isInPlay()) { + return alternatives; + } + + if (sa.isSpell()) { boolean lkicheck = false; Card newHost = ((Spell)sa).getAlternateHost(source); @@ -179,12 +178,13 @@ public final class GameActionUtil { SpellAbility newSA; if (source.getAlternateState().getType().hasSubtype("Aura")) { - newSA = source.getAlternateState().getFirstAbility().copyWithManaCostReplaced(activator, - disturbCost); + newSA = source.getAlternateState().getFirstAbility().copyWithManaCostReplaced(activator, disturbCost); } else { - newSA = sa.copyWithManaCostReplaced(activator, disturbCost); + newSA = new SpellPermanent(source); + newSA.setCardState(source.getAlternateState()); + newSA.setPayCosts(disturbCost); + newSA.setActivatingPlayer(activator); } - newSA.setActivatingPlayer(activator); newSA.putParam("PrecostDesc", "Disturb —"); newSA.putParam("CostDesc", disturbCost.toString()); @@ -210,7 +210,6 @@ public final class GameActionUtil { final Cost escapeCost = new Cost(k[1], true); final SpellAbility newSA = sa.copyWithManaCostReplaced(activator, escapeCost); - newSA.setActivatingPlayer(activator); newSA.putParam("PrecostDesc", "Escape—"); newSA.putParam("CostDesc", escapeCost.toString()); @@ -290,6 +289,35 @@ public final class GameActionUtil { foretold.putParam("AfterDescription", "(Foretold)"); alternatives.add(foretold); } + + // some needs to check after ability was put on the stack + // Currently this is only checked for Toolbox and that only cares about creature spells + if (source.isCreature() && game.getAction().hasStaticAbilityAffectingZone(ZoneType.Stack, StaticAbilityLayer.ABILITIES)) { + Zone oldZone = source.getLastKnownZone(); + Card blitzCopy = source; + if (!source.isLKI()) { + blitzCopy = CardUtil.getLKICopy(source); + } + blitzCopy.setLastKnownZone(game.getStackZone()); + lkicheck = true; + + blitzCopy.clearStaticChangedCardKeywords(false); + CardCollection preList = new CardCollection(blitzCopy); + game.getAction().checkStaticAbilities(false, Sets.newHashSet(blitzCopy), preList); + + // currently only for Keyword BLitz, but should affect Dash probably too + for (final KeywordInterface inst : blitzCopy.getKeywords(Keyword.BLITZ)) { + // TODO with mana value 4 or greater has blitz. + for (SpellAbility iSa : inst.getAbilities()) { + // do only non intrinsic + if (!iSa.isIntrinsic()) { + alternatives.add(iSa); + } + } + } + // need to reset to Old Zone, or canPlay would fail + blitzCopy.setLastKnownZone(oldZone); + } } // reset static abilities @@ -300,74 +328,73 @@ public final class GameActionUtil { // need to unfreeze tracker game.getTracker().unfreeze(); } - } + } else { + if (sa.isManaAbility() && sa.isActivatedAbility() && activator.hasKeyword("Piracy") && source.isLand() && source.isInPlay() && !activator.equals(source.getController()) && sa.getPayCosts().hasTapCost()) { + SpellAbility newSA = sa.copy(activator); + // to bypass Activator restriction, set Activator to Player + newSA.getRestrictions().setActivator("Player"); - if (sa.isManaAbility() && sa.isActivatedAbility() && activator.hasKeyword("Piracy") && source.isLand() && source.isInPlay() && !activator.equals(source.getController()) && sa.getPayCosts().hasTapCost()) { - SpellAbility newSA = sa.copy(activator); - // to bypass Activator restriction, set Activator to Player - newSA.getRestrictions().setActivator("Player"); - - // extra Mana restriction to only Spells - for (AbilityManaPart mp : newSA.getAllManaParts()) { - mp.setExtraManaRestriction("Spell"); - } - alternatives.add(newSA); - } - - // below are for some special cases of activated abilities - if (sa.isCycling() && activator.hasKeyword("CyclingForZero")) { - for (final KeywordInterface inst : source.getKeywords()) { - // need to find the correct Keyword from which this Ability is from - if (!inst.getAbilities().contains(sa)) { - continue; + // extra Mana restriction to only Spells + for (AbilityManaPart mp : newSA.getAllManaParts()) { + mp.setExtraManaRestriction("Spell"); } - - // set the cost to this directly to bypass non mana cost - final SpellAbility newSA = sa.copyWithDefinedCost("Discard<1/CARDNAME>"); - newSA.setActivatingPlayer(activator); - newSA.putParam("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); } - } - if (sa.isEquip() && activator.hasKeyword("You may pay 0 rather than pay equip costs.")) { - for (final KeywordInterface inst : source.getKeywords()) { - // need to find the correct Keyword from which this Ability is from - if (!inst.getAbilities().contains(sa)) { - continue; + + // below are for some special cases of activated abilities + if (sa.isCycling() && activator.hasKeyword("CyclingForZero")) { + 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 bypass non mana cost + final SpellAbility newSA = sa.copyWithDefinedCost("Discard<1/CARDNAME>"); + newSA.setActivatingPlayer(activator); + newSA.putParam("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); } + } + if (sa.isEquip() && activator.hasKeyword("You may pay 0 rather than pay equip costs.")) { + 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 bypass non mana cost - SpellAbility newSA = sa.copyWithDefinedCost("0"); - newSA.setActivatingPlayer(activator); - newSA.putParam("CostDesc", ManaCostParser.parse("0")); + // set the cost to this directly to bypass non mana cost + SpellAbility newSA = sa.copyWithDefinedCost("0"); + newSA.setActivatingPlayer(activator); + newSA.putParam("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, ":")); + // 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()); + // 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); + alternatives.add(newSA); + } } } - return alternatives; } @@ -677,13 +704,12 @@ public final class GameActionUtil { if (!StringUtils.isNumeric(amount)) { sa.setSVar(amount, sourceCard.getSVar(amount)); } - CardFactoryUtil.setupETBReplacementAbility(sa); String desc = "It enters the battlefield with "; desc += Lang.nounWithNumeral(amount, CounterType.getType(counter).getName() + " counter"); desc += " on it."; - String repeffstr = "Event$ Moved | ValidCard$ Card.IsRemembered | Destination$ Battlefield | Description$ " + desc; + String repeffstr = "Event$ Moved | ValidCard$ Card.IsRemembered | Destination$ Battlefield | ReplacementResult$ Updated | Description$ " + desc; ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true); re.setLayer(ReplacementLayer.Other); diff --git a/forge-game/src/main/java/forge/game/GameEntity.java b/forge-game/src/main/java/forge/game/GameEntity.java index 531bc6e6ecf..f7ba73ef30b 100644 --- a/forge-game/src/main/java/forge/game/GameEntity.java +++ b/forge-game/src/main/java/forge/game/GameEntity.java @@ -17,9 +17,13 @@ */ package forge.game; +import java.util.List; import java.util.Map; +import org.apache.commons.lang3.tuple.Pair; + import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.game.ability.AbilityUtils; @@ -46,6 +50,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { private String name = ""; protected CardCollection attachedCards = new CardCollection(); protected Map counters = Maps.newHashMap(); + protected List> damageReceivedThisTurn = Lists.newArrayList(); protected GameEntity(int id0) { id = id0; @@ -330,6 +335,30 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table); } + public void receiveDamage(Pair dmg) { + damageReceivedThisTurn.add(dmg); + } + + public final int getAssignedDamage() { + return getAssignedDamage(null, null); + } + public final int getAssignedCombatDamage() { + return getAssignedDamage(true, null); + } + public final int getAssignedDamage(Boolean isCombat, final Card source) { + int num = 0; + for (Pair dmg : damageReceivedThisTurn) { + if (isCombat != null && dmg.getRight() != isCombat) { + continue; + } + if (source != null && !getGame().getDamageLKI(dmg).getLeft().equalsWithTimestamp(source)) { + continue; + } + num += dmg.getLeft(); + } + return num; + } + @Override public final boolean equals(Object o) { if (o == null) { return false; } diff --git a/forge-game/src/main/java/forge/game/GameEntityCounterTable.java b/forge-game/src/main/java/forge/game/GameEntityCounterTable.java index 831573e6627..1e81b78a21b 100644 --- a/forge-game/src/main/java/forge/game/GameEntityCounterTable.java +++ b/forge-game/src/main/java/forge/game/GameEntityCounterTable.java @@ -94,7 +94,9 @@ public class GameEntityCounterTable extends ForwardingTable, Ga for (Map cm : gm.getValue().values()) { Integer old = ObjectUtils.firstNonNull(result.get(gm.getKey()), 0); Integer v = ObjectUtils.firstNonNull(cm.get(type), 0); - result.put(gm.getKey(), old + v); + if (old + v > 0) { + result.put(gm.getKey(), old + v); + } } } } @@ -120,8 +122,12 @@ public class GameEntityCounterTable extends ForwardingTable, Ga game.getTriggerHandler().runTrigger(TriggerType.CounterAddedAll, runParams, false); } - @SuppressWarnings("unchecked") public void replaceCounterEffect(final Game game, final SpellAbility cause, final boolean effect) { + replaceCounterEffect(game, cause, effect, false, null); + } + + @SuppressWarnings("unchecked") + public void replaceCounterEffect(final Game game, final SpellAbility cause, final boolean effect, final boolean etb, Map params) { if (isEmpty()) { return; } @@ -133,6 +139,10 @@ public class GameEntityCounterTable extends ForwardingTable, Ga repParams.put(AbilityKey.Cause, cause); repParams.put(AbilityKey.EffectOnly, effect); repParams.put(AbilityKey.CounterMap, values); + repParams.put(AbilityKey.ETB, etb); + if (params != null) { + repParams.putAll(params); + } switch (game.getReplacementHandler().run(ReplacementType.AddCounter, repParams)) { case NotReplaced: diff --git a/forge-game/src/main/java/forge/game/GameFormat.java b/forge-game/src/main/java/forge/game/GameFormat.java index b1749dfe8c1..d27bba2b5fe 100644 --- a/forge-game/src/main/java/forge/game/GameFormat.java +++ b/forge-game/src/main/java/forge/game/GameFormat.java @@ -48,7 +48,7 @@ public class GameFormat implements Comparable { public enum FormatType { SANCTIONED, CASUAL, - HISTORIC, + ARCHIVED, DIGITAL, CUSTOM } @@ -290,7 +290,7 @@ public class GameFormat implements Comparable { if (other.formatSubType != formatSubType){ return formatSubType.compareTo(other.formatSubType); } - if (formatType.equals(FormatType.HISTORIC)){ + if (formatType.equals(FormatType.ARCHIVED)){ int compareDates = this.effectiveDate.compareTo(other.effectiveDate); if (compareDates != 0) return compareDates; @@ -306,7 +306,7 @@ public class GameFormat implements Comparable { public static class Reader extends StorageReaderRecursiveFolderWithUserFolder { List naturallyOrdered = new ArrayList<>(); - boolean includeHistoric; + boolean includeArchived; private List coreFormats = new ArrayList<>(); { coreFormats.add("Standard.txt"); @@ -321,14 +321,14 @@ public class GameFormat implements Comparable { coreFormats.add("Oathbreaker.txt"); } - public Reader(File forgeFormats, File customFormats, boolean includeHistoric) { + public Reader(File forgeFormats, File customFormats, boolean includeArchived) { super(forgeFormats, customFormats, GameFormat.FN_GET_NAME); - this.includeHistoric=includeHistoric; + this.includeArchived=includeArchived; } @Override protected GameFormat read(File file) { - if (!includeHistoric && !coreFormats.contains(file.getName())) { + if (!includeArchived && !coreFormats.contains(file.getName())) { return null; } final Map> contents = FileSection.parseSections(FileUtil.readFile(file)); @@ -348,7 +348,12 @@ public class GameFormat implements Comparable { try { formatType = FormatType.valueOf(section.get("type").toUpperCase()); } catch (Exception e) { - formatType = FormatType.CUSTOM; + if ("HISTORIC".equals(section.get("type").toUpperCase())) { + System.out.println("Historic is no longer used as a format Type. Please update " + file.getAbsolutePath() + " to use 'Archived' instead"); + formatType = FormatType.ARCHIVED; + } else { + formatType = FormatType.CUSTOM; + } } FormatSubType formatsubType; try { @@ -450,7 +455,7 @@ public class GameFormat implements Comparable { public Iterable getFilterList() { List coreList = new ArrayList<>(); for (GameFormat format: naturallyOrdered) { - if (!format.getFormatType().equals(FormatType.HISTORIC) + if (!format.getFormatType().equals(FormatType.ARCHIVED) &&!format.getFormatType().equals(FormatType.DIGITAL)){ coreList.add(format); } @@ -458,10 +463,10 @@ public class GameFormat implements Comparable { return coreList; } - public Iterable getHistoricList() { + public Iterable getArchivedList() { List coreList = new ArrayList<>(); for (GameFormat format: naturallyOrdered) { - if (format.getFormatType().equals(FormatType.HISTORIC)){ + if (format.getFormatType().equals(FormatType.ARCHIVED)){ coreList.add(format); } } @@ -470,7 +475,7 @@ public class GameFormat implements Comparable { public Iterable getBlockList() { List blockFormats = new ArrayList<>(); - for (GameFormat format : this.getHistoricList()){ + for (GameFormat format : this.getArchivedList()){ if (format.getFormatSubType() != GameFormat.FormatSubType.BLOCK) continue; if (!format.getName().endsWith("Block")) @@ -481,10 +486,10 @@ public class GameFormat implements Comparable { return blockFormats; } - public Map> getHistoricMap() { + public Map> getArchivedMap() { Map> coreList = new HashMap<>(); for (GameFormat format: naturallyOrdered){ - if (format.getFormatType().equals(FormatType.HISTORIC)){ + if (format.getFormatType().equals(FormatType.ARCHIVED)){ String alpha = format.getName().substring(0,1); if (!coreList.containsKey(alpha)) { coreList.put(alpha,new ArrayList<>()); @@ -557,9 +562,9 @@ public class GameFormat implements Comparable { //exclude Commander format as other deck checks are not performed here continue; } - if (gf.getFormatType().equals(FormatType.HISTORIC) && coveredTypes.contains(gf.getFormatSubType()) + if (gf.getFormatType().equals(FormatType.ARCHIVED) && coveredTypes.contains(gf.getFormatSubType()) && !exhaustive){ - //exclude duplicate formats - only keep first of e.g. Standard historical + //exclude duplicate formats - only keep first of e.g. Standard archived continue; } if (gf.isPoolLegal(allCards)) { @@ -590,7 +595,7 @@ public class GameFormat implements Comparable { if (gf2.formatSubType != gf1.formatSubType){ return gf1.formatSubType.compareTo(gf2.formatSubType); } - if (gf1.formatType.equals(FormatType.HISTORIC)){ + if (gf1.formatType.equals(FormatType.ARCHIVED)){ if (gf1.effectiveDate!=gf2.effectiveDate) {//for matching dates or default dates default to name sorting return gf1.effectiveDate.compareTo(gf2.effectiveDate); } diff --git a/forge-game/src/main/java/forge/game/GameRules.java b/forge-game/src/main/java/forge/game/GameRules.java index 2ef99fd90ba..16ff8ab2697 100644 --- a/forge-game/src/main/java/forge/game/GameRules.java +++ b/forge-game/src/main/java/forge/game/GameRules.java @@ -81,7 +81,8 @@ public class GameRules { } public void setAppliedVariants(final Set appliedVariants) { - this.appliedVariants.addAll(appliedVariants); + if (appliedVariants != null && !appliedVariants.isEmpty()) + this.appliedVariants.addAll(appliedVariants); } public boolean hasAppliedVariant(final GameType variant) { diff --git a/forge-game/src/main/java/forge/game/GameType.java b/forge-game/src/main/java/forge/game/GameType.java index b1b0c5cf512..4cf55770c9c 100644 --- a/forge-game/src/main/java/forge/game/GameType.java +++ b/forge-game/src/main/java/forge/game/GameType.java @@ -76,7 +76,7 @@ public enum GameType { private final DeckFormat deckFormat; private final boolean isCardPoolLimited, canSideboard, addWonCardsMidGame; - private final String name, description; + private final String name, englishName, description; private final Function deckAutoGenerator; GameType(DeckFormat deckFormat0, boolean isCardPoolLimited0, boolean canSideboard0, boolean addWonCardsMidgame0, String name0, String description0) { @@ -90,6 +90,7 @@ public enum GameType { canSideboard = canSideboard0; addWonCardsMidGame = addWonCardsMidgame0; name = localizer.getMessage(name0); + englishName = localizer.getEnglishMessage(name0); if (description0.length()>0) { description0 = localizer.getMessage(description0); } @@ -148,6 +149,9 @@ public enum GameType { public String toString() { return name; } + public String getEnglishName() { + return englishName; + } public String getDescription() { return description; diff --git a/forge-game/src/main/java/forge/game/GlobalRuleChange.java b/forge-game/src/main/java/forge/game/GlobalRuleChange.java index 3e8312427e0..ef364c1b354 100644 --- a/forge-game/src/main/java/forge/game/GlobalRuleChange.java +++ b/forge-game/src/main/java/forge/game/GlobalRuleChange.java @@ -33,7 +33,6 @@ public enum GlobalRuleChange { onlyOneBlocker ("No more than one creature can block each combat."), onlyOneBlockerPerOpponent ("Each opponent can't block with more than one creature."), onlyTwoBlockers ("No more than two creatures can block each combat."), - toughnessAssignsDamage ("Each creature assigns combat damage equal to its toughness rather than its power."), blankIsChaos("Each blank roll of the planar dice is a {CHAOS} roll."); private final String ruleText; diff --git a/forge-game/src/main/java/forge/game/StaticEffect.java b/forge-game/src/main/java/forge/game/StaticEffect.java index 05bed4adb7a..639f13588cd 100644 --- a/forge-game/src/main/java/forge/game/StaticEffect.java +++ b/forge-game/src/main/java/forge/game/StaticEffect.java @@ -288,6 +288,8 @@ public class StaticEffect { affectedCard.removeCanBlockAdditional(getTimestamp()); } + affectedCard.removeChangedSVars(getTimestamp(), ability.getId()); + affectedCard.updateAbilityTextForView(); // only update keywords and text for view to avoid flickering } return affectedCards; 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 cfb843b4e3f..a34a1019d38 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java @@ -327,8 +327,8 @@ public final class AbilityFactory { } private static final TargetRestrictions readTarget(Map mapParams) { - final String min = mapParams.containsKey("TargetMin") ? mapParams.get("TargetMin") : "1"; - final String max = mapParams.containsKey("TargetMax") ? mapParams.get("TargetMax") : "1"; + final String min = mapParams.getOrDefault("TargetMin", "1"); + final String max = mapParams.getOrDefault("TargetMax", "1"); // TgtPrompt should only be needed for more complicated ValidTgts String tgtWhat = mapParams.get("ValidTgts"); diff --git a/forge-game/src/main/java/forge/game/ability/AbilityKey.java b/forge-game/src/main/java/forge/game/ability/AbilityKey.java index 45b69470128..b59c96e35d3 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityKey.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityKey.java @@ -67,6 +67,7 @@ public enum AbilityKey { Explorer("Explorer"), ExtraTurn("ExtraTurn"), Event("Event"), + ETB("ETB"), Fighter("Fighter"), Fighters("Fighters"), FirstTime("FirstTime"), @@ -112,12 +113,14 @@ public enum AbilityKey { Result("Result"), RoomName("RoomName"), Scheme("Scheme"), + ScryNum("ScryNum"), Sides("Sides"), Source("Source"), Sources("Sources"), SourceSA("SourceSA"), SpellAbility("SpellAbility"), SpellAbilityStackInstance("SpellAbilityStackInstance"), + SpellAbilityTarget("SpellAbilityTarget"), SpellAbilityTargetingCards("SpellAbilityTargetingCards"), StackInstance("StackInstance"), StackSa("StackSa"), 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 6df2d9bc46c..668277ea322 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -14,6 +14,7 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; @@ -146,7 +147,7 @@ public class AbilityUtils { } else if (defined.equals("TopOfGraveyard")) { final CardCollectionView grave = hostCard.getController().getCardsIn(ZoneType.Graveyard); - if (grave.size() > 0) { // TopOfLibrary or BottomOfLibrary + if (grave.size() > 0) { c = grave.getLast(); } else { // we don't want this to fall through and return the "Self" @@ -220,7 +221,7 @@ public class AbilityUtils { final Object crd = root.getReplacingObject(type); if (crd instanceof Card) { - c = game.getCardState((Card) crd); + c = (Card) crd; } else if (crd instanceof Iterable) { cards.addAll(Iterables.filter((Iterable) crd, Card.class)); } @@ -501,7 +502,7 @@ public class AbilityUtils { players.addAll(player.getOpponents()); val = playerXCount(players, calcX[1], card, ability); } else if (hType.equals("RegisteredOpponents")) { - players.addAll(Iterables.filter(game.getRegisteredPlayers(),PlayerPredicates.isOpponentOf(player))); + players.addAll(Iterables.filter(game.getRegisteredPlayers(), PlayerPredicates.isOpponentOf(player))); val = playerXCount(players, calcX[1], card, ability); } else if (hType.equals("Other")) { players.addAll(player.getAllOtherPlayers()); @@ -535,6 +536,9 @@ public class AbilityUtils { } } val = playerXCount(players, calcX[1], card, ability); + } else if (hType.startsWith("Defined")) { + String defined = hType.split("Defined")[1]; + val = playerXCount(getDefinedPlayers(card, defined, ability), calcX[1], card, ability); } else { val = 0; } @@ -1060,17 +1064,16 @@ public class AbilityUtils { final SpellAbility root = ((SpellAbility)sa).getRootAbility(); Object o = null; if (defParsed.endsWith("Controller")) { - String triggeringType = defParsed.substring(9); - triggeringType = triggeringType.substring(0, triggeringType.length() - 10); + final boolean orCont = defParsed.endsWith("OrController"); + String triggeringType = defParsed.substring(9, defParsed.length() - (orCont ? 12 : 10)); final Object c = root.getTriggeringObject(AbilityKey.fromString(triggeringType)); - if (c instanceof Card) { + if (orCont && c instanceof Player) { + o = c; + } else if (c instanceof Card) { o = ((Card) c).getController(); - } - if (c instanceof SpellAbility) { + } else if (c instanceof SpellAbility) { o = ((SpellAbility) c).getActivatingPlayer(); - } - // For merged permanent - if (c instanceof CardCollection) { + } else if (c instanceof CardCollection) { // For merged permanent o = ((CardCollection) c).get(0).getController(); } } @@ -1413,11 +1416,7 @@ public class AbilityUtils { // Needed - Equip an untapped creature with Sword of the Paruns then cast Deadshot on it. Should deal 2 more damage. game.getAction().checkStaticAbilities(); // this will refresh continuous abilities for players and permanents. - if (sa.isReplacementAbility() && abSub.getApi() == ApiType.InternalEtbReplacement) { - game.getTriggerHandler().resetActiveTriggers(false); - } else { - game.getTriggerHandler().resetActiveTriggers(); - } + game.getTriggerHandler().resetActiveTriggers(!sa.isReplacementAbility()); AbilityUtils.resolveApiAbility(abSub, game); } @@ -1448,7 +1447,7 @@ public class AbilityUtils { // The player who has the chance to cancel the ability final String pays = sa.getParamOrDefault("UnlessPayer", "TargetedController"); - final FCollectionView allPayers = getDefinedPlayers(sa.getHostCard(), pays, sa); + final FCollectionView allPayers = getDefinedPlayers(source, pays, sa); final String resolveSubs = sa.getParam("UnlessResolveSubs"); // no value means 'Always' final boolean execSubsWhenPaid = "WhenPaid".equals(resolveSubs) || StringUtils.isBlank(resolveSubs); final boolean execSubsWhenNotPaid = "WhenNotPaid".equals(resolveSubs) || StringUtils.isBlank(resolveSubs); @@ -1485,7 +1484,7 @@ public class AbilityUtils { cost = new Cost(new ManaCost(new ManaCostParser(String.valueOf(source.getChosenNumber()))), true); } else if (unlessCost.startsWith("DefinedCost")) { - CardCollection definedCards = getDefinedCards(sa.getHostCard(), unlessCost.split("_")[1], sa); + CardCollection definedCards = getDefinedCards(source, unlessCost.split("_")[1], sa); if (definedCards.isEmpty()) { sa.resolve(); resolveSubAbilities(sa, game); @@ -1505,7 +1504,7 @@ public class AbilityUtils { cost = new Cost(newCost.toManaCost(), true); } else if (unlessCost.startsWith("DefinedSACost")) { - FCollection definedSAs = getDefinedSpellAbilities(sa.getHostCard(), unlessCost.split("_")[1], sa); + FCollection definedSAs = getDefinedSpellAbilities(source, unlessCost.split("_")[1], sa); if (definedSAs.isEmpty()) { sa.resolve(); resolveSubAbilities(sa, game); @@ -2032,7 +2031,7 @@ public class AbilityUtils { return doXMath(c.getTotalDamageDoneBy(), expr, c, ctb); } if (sq[0].equals("TotalDamageReceivedThisTurn")) { - return doXMath(c.getTotalDamageReceivedThisTurn(), expr, c, ctb); + return doXMath(c.getAssignedDamage(), expr, c, ctb); } if (sq[0].contains("CardPower")) { @@ -2093,13 +2092,6 @@ public class AbilityUtils { return doXMath(c.getTimesMutated(), expr, c, ctb); } - if (sq[0].startsWith("DamageDoneByPlayerThisTurn")) { - int sum = 0; - for (Player p : getDefinedPlayers(c, sq[1], ctb)) { - sum += c.getReceivedDamageByPlayerThisTurn(p); - } - return doXMath(sum, expr, c, ctb); - } if (sq[0].equals("RegeneratedThisTurn")) { return doXMath(c.getRegeneratedThisTurn(), expr, c, ctb); } @@ -2260,6 +2252,9 @@ public class AbilityUtils { if (sq[0].equals("Monarch")) { return doXMath(calculateAmount(c, sq[player.isMonarch() ? 1 : 2], ctb), expr, c, ctb); } + if (sq[0].equals("Initiative")) { + return doXMath(calculateAmount(c, sq[player.hasInitiative() ? 1: 2], ctb), expr, c, ctb); + } if (sq[0].equals("StartingPlayer")) { return doXMath(calculateAmount(c, sq[player.isStartingPlayer() ? 1: 2], ctb), expr, c, ctb); } @@ -2358,20 +2353,28 @@ public class AbilityUtils { return doXMath(player.getOpponentsTotalPoisonCounters(), expr, c, ctb); } - if (sq[0].equals("YourDamageThisTurn")) { - return doXMath(player.getAssignedDamage(), expr, c, ctb); - } - if (sq[0].equals("TotalOppDamageThisTurn")) { - return doXMath(player.getOpponentsAssignedDamage(), expr, c, ctb); - } if (sq[0].equals("MaxOppDamageThisTurn")) { return doXMath(player.getMaxOpponentAssignedDamage(), expr, c, ctb); } - if (sq[0].startsWith("YourDamageSourcesThisTurn")) { - Iterable allSrc = player.getAssignedDamageSources(); - String restriction = sq[0].split(" ")[1]; - return doXMath(CardLists.getValidCardCount(allSrc, restriction, player, c, ctb), expr, c, ctb); + if (sq[0].contains("TotalDamageThisTurn")) { + String[] props = l[0].split(" "); + int sum = 0; + for (Pair p : c.getDamageReceivedThisTurn()) { + if (game.getDamageLKI(p).getLeft().isValid(props[1], player, c, ctb)) { + sum += p.getLeft(); + } + } + return doXMath(sum, expr, c, ctb); + } + + if (sq[0].contains("DamageThisTurn")) { + String[] props = l[0].split(" "); + Boolean isCombat = null; + if (sq[0].contains("CombatDamage")) { + isCombat = true; + } + return doXMath(game.getDamageDoneThisTurn(isCombat, false, props[1], props[2], c, player, ctb).size(), expr, c, ctb); } if (sq[0].equals("YourTurns")) { @@ -3324,12 +3327,6 @@ public class AbilityUtils { totDmg += p.getAssignedDamage(); } return doXMath(totDmg, m, source, ctb); - } else if (sq[0].contains("LifeLostThisTurn")) { - int totDmg = 0; - for (Player p : players) { - totDmg += p.getLifeLostThisTurn(); - } - return doXMath(totDmg, m, source, ctb); } if (players.size() > 0) { @@ -3386,7 +3383,7 @@ public class AbilityUtils { if (value.contains("DomainPlayer")) { int n = 0; - final CardCollectionView someCards = player.getCardsIn(ZoneType.Battlefield); + final CardCollectionView someCards = player.getLandsInPlay(); final List basic = MagicColor.Constant.BASIC_LANDS; for (int i = 0; i < basic.size(); i++) { @@ -3483,6 +3480,10 @@ public class AbilityUtils { return doXMath(opps == null ? 0 : opps.size(), m, source, ctb); } + if (value.equals("OpponentsAttackedThisCombat")) { + return doXMath(game.getCombat().getAttackedOpponents(player).size(), m, source, ctb); + } + if (value.equals("DungeonsCompleted")) { return doXMath(player.getCompletedDungeons().size(), m, source, ctb); } 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 ef64fc75305..1d1e13a525b 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -71,6 +71,7 @@ public enum ApiType { DigUntil (DigUntilEffect.class), Discard (DiscardEffect.class), DrainMana (DrainManaEffect.class), + Draft (DraftEffect.class), Draw (DrawEffect.class), EachDamage (DamageEachEffect.class), Effect (EffectEffect.class), @@ -171,6 +172,7 @@ public enum ApiType { Subgame (SubgameEffect.class), Surveil (SurveilEffect.class), SwitchBlock (SwitchBlockEffect.class), + TakeInitiative (TakeInitiativeEffect.class), Tap (TapEffect.class), TapAll (TapAllEffect.class), TapOrUntap (TapOrUntapEffect.class), @@ -188,7 +190,6 @@ public enum ApiType { DamageResolve (DamageResolveEffect.class), ChangeZoneResolve (ChangeZoneResolveEffect.class), - InternalEtbReplacement (ETBReplacementEffect.class), InternalLegendaryRule (CharmEffect.class), InternalIgnoreEffect (CharmEffect.class), UpdateRemember (UpdateRememberEffect.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 da91b969bef..9cd9c20eade 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java @@ -300,13 +300,12 @@ public abstract class SpellAbilityEffect { delTrig.append("| TriggerDescription$ ").append(desc); final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), CardUtil.getLKICopy(sa.getHostCard()), intrinsic); + long ts = sa.getHostCard().getGame().getNextTimestamp(); for (final Card c : crds) { trig.addRemembered(c); // Svar for AI - if (!c.hasSVar("EndOfTurnLeavePlay")) { - c.setSVar("EndOfTurnLeavePlay", "AtEOT"); - } + c.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "AtEOT"), ts, 0); } String trigSA = ""; if (location.equals("Hand")) { @@ -346,9 +345,7 @@ public abstract class SpellAbilityEffect { card.addTrigger(trig); // Svar for AI - if (!card.hasSVar("EndOfTurnLeavePlay")) { - card.setSVar("EndOfTurnLeavePlay", "AtEOT"); - } + card.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "AtEOT"), card.getGame().getNextTimestamp(), 0); } protected static SpellAbility getForgetSpellAbility(final Card card) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/AbandonEffect.java b/forge-game/src/main/java/forge/game/ability/effects/AbandonEffect.java index 72a88f752c5..7641de47351 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AbandonEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AbandonEffect.java @@ -25,7 +25,7 @@ public class AbandonEffect extends SpellAbilityEffect { Player controller = source.getController(); boolean isOptional = sa.hasParam("Optional"); - if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeAbandonSource", CardTranslation.getTranslatedName(source.getName())))) { + if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeAbandonSource", CardTranslation.getTranslatedName(source.getName())), null)) { return; } diff --git a/forge-game/src/main/java/forge/game/ability/effects/AnimateAllEffect.java b/forge-game/src/main/java/forge/game/ability/effects/AnimateAllEffect.java index efd631c5773..815164b2521 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AnimateAllEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AnimateAllEffect.java @@ -1,10 +1,11 @@ package forge.game.ability.effects; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import forge.GameCommand; import forge.card.CardType; @@ -64,17 +65,17 @@ public class AnimateAllEffect extends AnimateEffectBase { types.add(host.getChosenType2()); } - final List keywords = new ArrayList<>(); + final List keywords = Lists.newArrayList(); if (sa.hasParam("Keywords")) { keywords.addAll(Arrays.asList(sa.getParam("Keywords").split(" & "))); } - final List removeKeywords = new ArrayList<>(); + final List removeKeywords = Lists.newArrayList(); if (sa.hasParam("RemoveKeywords")) { removeKeywords.addAll(Arrays.asList(sa.getParam("RemoveKeywords").split(" & "))); } - final List hiddenKeywords = new ArrayList<>(); + final List hiddenKeywords = Lists.newArrayList(); if (sa.hasParam("HiddenKeywords")) { hiddenKeywords.addAll(Arrays.asList(sa.getParam("HiddenKeywords").split(" & "))); } @@ -99,27 +100,38 @@ public class AnimateAllEffect extends AnimateEffectBase { } // abilities to add to the animated being - final List abilities = new ArrayList<>(); + final List abilities = Lists.newArrayList(); if (sa.hasParam("Abilities")) { abilities.addAll(Arrays.asList(sa.getParam("Abilities").split(","))); } // replacement effects to add to the animated being - final List replacements = new ArrayList<>(); + final List replacements = Lists.newArrayList(); if (sa.hasParam("Replacements")) { replacements.addAll(Arrays.asList(sa.getParam("Replacements").split(","))); } // triggers to add to the animated being - final List triggers = new ArrayList<>(); + final List triggers = Lists.newArrayList(); if (sa.hasParam("Triggers")) { triggers.addAll(Arrays.asList(sa.getParam("Triggers").split(","))); } // sVars to add to the animated being - final List sVars = new ArrayList<>(); + final List sVars = Lists.newArrayList(); if (sa.hasParam("sVars")) { sVars.addAll(Arrays.asList(sa.getParam("sVars").split(","))); } + // static abilities to add to the animated being + final List stAbs = Lists.newArrayList(); + if (sa.hasParam("staticAbilities")) { + stAbs.addAll(Arrays.asList(sa.getParam("staticAbilities").split(","))); + } + + Map sVarsMap = Maps.newHashMap(); + for (final String s : sVars) { + sVarsMap.put(s, AbilityUtils.getSVar(sa, s)); + } + final String valid = sa.getParamOrDefault("ValidCards", ""); CardCollectionView list; @@ -133,15 +145,14 @@ public class AnimateAllEffect extends AnimateEffectBase { list = CardLists.getValidCards(list, valid, host.getController(), host, sa); for (final Card c : list) { - doAnimate(c, sa, power, toughness, types, removeTypes, finalColors, - keywords, removeKeywords, hiddenKeywords, - abilities, triggers, replacements, ImmutableList.of(), - timestamp); + doAnimate(c, sa, power, toughness, types, removeTypes, finalColors, keywords, removeKeywords, + hiddenKeywords, abilities, triggers, replacements, stAbs, timestamp); // give sVars - for (final String s : sVars) { - c.setSVar(s, AbilityUtils.getSVar(sa, s)); + if (!sVarsMap.isEmpty() ) { + c.addChangedSVars(sVarsMap, timestamp, 0); } + game.fireEvent(new GameEventCardStatsChanged(c)); final GameCommand unanimate = new GameCommand() { diff --git a/forge-game/src/main/java/forge/game/ability/effects/AnimateEffect.java b/forge-game/src/main/java/forge/game/ability/effects/AnimateEffect.java index f2c37658ead..5f066d9df5a 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AnimateEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AnimateEffect.java @@ -2,7 +2,9 @@ package forge.game.ability.effects; import java.util.Arrays; import java.util.List; +import java.util.Map; +import com.google.common.collect.Maps; import com.google.common.collect.Lists; import forge.card.CardType; @@ -138,11 +140,22 @@ public class AnimateEffect extends AnimateEffectBase { } // sVars to add to the animated being - final List sVars = Lists.newArrayList(); + Map sVarsMap = Maps.newHashMap(); if (sa.hasParam("sVars")) { - sVars.addAll(Arrays.asList(sa.getParam("sVars").split(","))); + for (final String s : sa.getParam("sVars").split(",")) { + String actualsVar = AbilityUtils.getSVar(sa, s); + String name = s; + if (actualsVar.startsWith("SVar:")) { + actualsVar = actualsVar.split("SVar:")[1]; + name = actualsVar.split(":")[0]; + actualsVar = actualsVar.split(":")[1]; + } + sVarsMap.put(name, actualsVar); + } } + + List tgts = getCardsfromTargets(sa); if (sa.hasParam("Optional")) { @@ -151,7 +164,7 @@ public class AnimateEffect extends AnimateEffectBase { ? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets) : getStackDescription(sa); - if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message)) { + if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message, null)) { return; } } @@ -166,15 +179,8 @@ public class AnimateEffect extends AnimateEffectBase { } // give sVars - for (final String s : sVars) { - String actualsVar = AbilityUtils.getSVar(sa, s); - String name = s; - if (actualsVar.startsWith("SVar:")) { - actualsVar = actualsVar.split("SVar:")[1]; - name = actualsVar.split(":")[0]; - actualsVar = actualsVar.split(":")[1]; - } - c.setSVar(name, actualsVar); + if (!sVarsMap.isEmpty()) { + c.addChangedSVars(sVarsMap, timestamp, 0); } // give Remembered diff --git a/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java b/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java index 70ac1823195..6df27fae809 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java @@ -137,6 +137,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect { public void run() { doUnanimate(c, timestamp); + c.removeChangedSVars(timestamp, 0); c.removeChangedName(timestamp, 0); c.updateStateForView(); 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 566ad27c37a..695eb84895e 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 @@ -134,7 +134,7 @@ public class AttachEffect extends SpellAbilityEffect { // If Cast Targets will be checked on the Stack for (final Card attachment : attachments) { String message = Localizer.getInstance().getMessage("lblDoYouWantAttachSourceToTarget", CardTranslation.getTranslatedName(attachment.getName()), attachToName); - if (sa.hasParam("Optional") && !p.getController().confirmAction(sa, null, message)) + if (sa.hasParam("Optional") && !p.getController().confirmAction(sa, null, message, null)) // TODO add params for message continue; diff --git a/forge-game/src/main/java/forge/game/ability/effects/BecomeMonarchEffect.java b/forge-game/src/main/java/forge/game/ability/effects/BecomeMonarchEffect.java index c1aa9336918..d8ba6c707f3 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/BecomeMonarchEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/BecomeMonarchEffect.java @@ -2,11 +2,10 @@ package forge.game.ability.effects; import java.util.List; -import org.apache.commons.lang3.StringUtils; - import forge.game.ability.SpellAbilityEffect; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.util.Lang; public class BecomeMonarchEffect extends SpellAbilityEffect { @@ -16,8 +15,8 @@ public class BecomeMonarchEffect extends SpellAbilityEffect { final List tgtPlayers = getTargetPlayers(sa); - sb.append(StringUtils.join(tgtPlayers, ", ")); - sb.append(" becomes the Monarch."); + sb.append(Lang.joinHomogenous(tgtPlayers)).append(tgtPlayers.size() == 1 ? " becomes" : " become"); + sb.append(" the monarch."); return sb.toString(); } 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 a62b98dc667..c3d65a43026 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 @@ -21,7 +21,7 @@ import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.TargetRestrictions; import forge.util.CardTranslation; import forge.util.Localizer; -import forge.util.collect.FCollectionView; +import forge.util.collect.FCollection; public class ChangeCombatantsEffect extends SpellAbilityEffect { @@ -47,7 +47,8 @@ public class ChangeCombatantsEffect extends SpellAbilityEffect { if ((tgt == null) || c.canBeTargetedBy(sa)) { final Combat combat = game.getCombat(); final GameEntity originalDefender = combat.getDefenderByAttacker(c); - final FCollectionView defs = combat.getDefenders(); + final FCollection defs = new FCollection<>(); + defs.addAll(sa.hasParam("PlayerOnly") ? combat.getDefendingPlayers() : combat.getDefenders()); String title = Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())); Map params = Maps.newHashMap(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeTargetsEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeTargetsEffect.java index bf481bc5493..56e5b0c5387 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeTargetsEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeTargetsEffect.java @@ -50,7 +50,7 @@ public class ChangeTargetsEffect extends SpellAbilityEffect { // Redirect rules read 'you MAY choose new targets' ... okay! // TODO: Don't even ask to change targets, if the SA and subs don't actually have targets boolean isOptional = sa.hasParam("Optional"); - if (isOptional && !chooser.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantChangeAbilityTargets", tgtSA.getHostCard().toString()))) { + if (isOptional && !chooser.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantChangeAbilityTargets", tgtSA.getHostCard().toString()), null)) { continue; } if (sa.hasParam("ChangeSingleTarget")) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java index 1bbb7308cd8..f204db8466e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java @@ -120,7 +120,7 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect { message = Localizer.getInstance().getMessage("lblMoveTargetFromOriginToDestination", targets, Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME), destination.getTranslatedName()); } - if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message)) { + if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message, null)) { return; } } 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 39c71723aa3..072538ebf2e 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 @@ -196,8 +196,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect { if (destination.equals("Battlefield")) { sb.append("onto the battlefield"); if (tapped) { - sb.append(" tapped"); + sb.append(" tapped").append(attacking ? " and" : ""); } + sb.append(attacking ? " attacking" : ""); if (sa.hasParam("GainControl")) { sb.append(" under ").append(chooserNames).append("'s control"); } @@ -259,14 +260,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect { if (destination.equals("Battlefield")) { sb.append(" onto the battlefield"); if (tapped) { - sb.append(" tapped"); - if (attacking) { - sb.append(" and"); - } - } - if (attacking) { - sb.append(" attacking"); + sb.append(" tapped").append(attacking ? " and" : ""); } + sb.append(attacking ? " attacking" : ""); if (sa.hasParam("GainControl")) { sb.append(" under ").append(chooserNames).append("'s control"); } @@ -366,14 +362,16 @@ public class ChangeZoneEffect extends SpellAbilityEffect { final String fromGraveyard = " from the graveyard"; if (destination.equals(ZoneType.Battlefield)) { + final boolean attacking = (sa.hasParam("Attacking")); if (ZoneType.Graveyard.equals(origin)) { sb.append("Return").append(targetname).append(fromGraveyard).append(" to the battlefield"); } else { sb.append("Put").append(targetname).append(" onto the battlefield"); } if (sa.hasParam("Tapped")) { - sb.append(" tapped"); + sb.append(" tapped").append(attacking ? " and" : ""); } + sb.append(attacking ? " attacking" : ""); if (sa.hasParam("GainControl")) { sb.append(" under your control"); } @@ -494,7 +492,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { PlayerCollection deciders = AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("AlternativeDecider"), sa); alterDecider = deciders.isEmpty() ? null : deciders.get(0); } - if (alterDecider != null && !alterDecider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneToAltDestination, sb.toString())) { + if (alterDecider != null && !alterDecider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneToAltDestination, sb.toString(), null)) { destination = ZoneType.smartValueOf(sa.getParam("DestinationAlternative")); altDest = true; } @@ -513,7 +511,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { continue; } - removeFromStack(tgtSA, sa, si, game, triggerList); + removeFromStack(tgtSA, sa, si, game, triggerList, counterTable); } // End of change from stack final String remember = sa.getParam("RememberChanged"); @@ -566,7 +564,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } final String prompt = TextUtil.concatWithSpace(Localizer.getInstance().getMessage("lblDoYouWantMoveTargetFromOriToDest", CardTranslation.getTranslatedName(gameCard.getName()), Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME), destination.getTranslatedName())); - if (optional && !chooser.getController().confirmAction(sa, null, prompt) ) + if (optional && !chooser.getController().confirmAction(sa, null, prompt, null) ) continue; final Zone originZone = game.getZoneOf(gameCard); @@ -958,7 +956,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { sb.append(sa.getParam("AlternativeMessage")).append(" "); sb.append(altFetchList.size()).append(" " + Localizer.getInstance().getMessage("lblCardMatchSearchingTypeInAlternateZones")); - if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneFromAltSource, sb.toString())) { + if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneFromAltSource, sb.toString(), null)) { origin = alt; } } @@ -970,7 +968,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { final StringBuilder sb = new StringBuilder(); sb.append(sa.getParam("AlternativeDestinationMessage")); - if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneToAltDestination, sb.toString())) { + if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneToAltDestination, sb.toString(), null)) { destination = ZoneType.smartValueOf(sa.getParam("DestinationAlternative")); libraryPos = sa.hasParam("LibraryPositionAlternative") ? Integer.parseInt(sa.getParam("LibraryPositionAlternative")) : 0; } @@ -987,7 +985,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { prompt = Localizer.getInstance().getMessage("lblSearchPlayerZoneConfirm", "{player's}", Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME).toLowerCase()); } String message = MessageUtil.formatMessage(prompt , decider, player); - if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message)) { + if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message, null)) { return; } } @@ -1073,7 +1071,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { continue; } SpellAbility tgtSA = decider.getController().getAbilityToPlay(tgtCard, sas); - if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())))) { + if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())), null)) { continue; } tgtSA.setSVar("IsCastFromPlayEffect", "True"); @@ -1189,7 +1187,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { String message = Localizer.getInstance().getMessage("lblCancelSearchUpToSelectNumCards", String.valueOf(num)); if (fetchList.isEmpty() || sa.hasParam("SkipCancelPrompt") || - decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message)) { + decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message, null)) { break; } i--; @@ -1545,7 +1543,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { * object. * @param game */ - private static void removeFromStack(final SpellAbility tgtSA, final SpellAbility srcSA, final SpellAbilityStackInstance si, final Game game, CardZoneTable triggerList) { + private static void removeFromStack(final SpellAbility tgtSA, final SpellAbility srcSA, final SpellAbilityStackInstance si, final Game game, CardZoneTable triggerList, GameEntityCounterTable counterTable) { final Card tgtHost = tgtSA.getHostCard(); final Zone originZone = tgtHost.getZone(); game.getStack().remove(si); @@ -1557,7 +1555,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect { Card movedCard = null; if (srcSA.hasParam("Destination")) { final boolean remember = srcSA.hasParam("RememberChanged"); - final boolean rememberSpell = srcSA.hasParam("RememberSpell"); final boolean imprint = srcSA.hasParam("Imprint"); if (tgtSA.isAbility()) { // Shouldn't be able to target Abilities but leaving this in for now @@ -1589,13 +1586,20 @@ public class ChangeZoneEffect extends SpellAbilityEffect { + srcSA.getHostCard().getName()); } + if (srcSA.hasParam("WithCountersType")) { + Player placer = srcSA.getActivatingPlayer(); + if (srcSA.hasParam("WithCountersPlacer")) { + placer = AbilityUtils.getDefinedPlayers(srcSA.getHostCard(), srcSA.getParam("WithCountersPlacer"), srcSA).get(0); + } + CounterType cType = CounterType.getType(srcSA.getParam("WithCountersType")); + int cAmount = AbilityUtils.calculateAmount(srcSA.getHostCard(), srcSA.getParamOrDefault("WithCountersAmount", "1"), srcSA); + movedCard.addCounter(cType, cAmount, placer, counterTable); + } + if (remember) { srcSA.getHostCard().addRemembered(tgtHost); // TODO or remember moved? } - if (rememberSpell) { - srcSA.getHostCard().addRemembered(tgtSA); - } if (imprint) { srcSA.getHostCard().addImprintedCard(tgtHost); } 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 0cea936f918..6e484d4a1a7 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 @@ -180,7 +180,7 @@ public class CharmEffect extends SpellAbilityEffect { num = Math.min(num, choices.size()); boolean isOptional = sa.hasParam("Optional"); - if (isOptional && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeCharm", CardTranslation.getTranslatedName(source.getName())))) { + if (isOptional && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeCharm", CardTranslation.getTranslatedName(source.getName())), null)) { return false; } 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 1b708fb097c..d60a1bd5532 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 @@ -131,7 +131,7 @@ public class ChooseCardEffect extends SpellAbilityEffect { Localizer.getInstance().getMessage("lblSelectCreatureWithTotalPowerLessOrEqualTo", (totP - chosenP - negativeNum)) + "\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"))) { + if (p.getController().confirmAction(sa, PlayerActionConfirmMode.OptionalChoose, Localizer.getInstance().getMessage("lblCancelChooseConfirm"), null)) { break; } } else { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseCardNameEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseCardNameEffect.java index c55979fe7aa..a435520bf40 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChooseCardNameEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseCardNameEffect.java @@ -54,7 +54,6 @@ public class ChooseCardNameEffect extends SpellAbilityEffect { } boolean randomChoice = sa.hasParam("AtRandom"); - boolean draft = sa.hasParam("Draft"); //for digital "draft from spellbook" mechanic boolean chooseFromDefined = sa.hasParam("ChooseFromDefinedCards"); boolean chooseFromList = sa.hasParam("ChooseFromList"); boolean chooseFromOneTimeList = sa.hasParam("ChooseFromOneTimeList"); @@ -62,8 +61,6 @@ public class ChooseCardNameEffect extends SpellAbilityEffect { if (!randomChoice) { if (sa.hasParam("SelectPrompt")) { message = sa.getParam("SelectPrompt"); - } else if (draft) { - message = Localizer.getInstance().getMessage("lblChooseCardDraft"); } else if (null == validDesc) { message = Localizer.getInstance().getMessage("lblChooseACardName"); } else { @@ -108,12 +105,6 @@ public class ChooseCardNameEffect extends SpellAbilityEffect { chosen = p.getController().chooseCardName(sa, faces, message); } else if (chooseFromList) { String [] names = sa.getParam("ChooseFromList").split(","); - if (sa.hasParam("Draft")) { - List options = Arrays.asList(names); - Collections.shuffle(options); - List draftChoices = options.subList(0,3); - names = draftChoices.toArray(new String[0]); - } List faces = new ArrayList<>(); for (String name : names) { // Cardnames that include "," must use ";" instead in ChooseFromList$ (i.e. Tovolar; Dire Overlord) @@ -169,9 +160,6 @@ public class ChooseCardNameEffect extends SpellAbilityEffect { host.setNamedCard(chosen); if (!randomChoice) { p.setNamedCard(chosen); - if (!draft) { //drafting is secret - p.getGame().getAction().notifyOfValue(sa, host, Localizer.getInstance().getMessage("lblPlayerPickedChosen", p.getName(), chosen), p); - } } if (sa.hasParam("NoteFor")) { p.addNoteForName(sa.getParam("NoteFor"), "Name:" + chosen); 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 dcbf032c2b1..fffc8ca0680 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 @@ -108,7 +108,7 @@ public class CloneEffect extends SpellAbilityEffect { } final boolean optional = sa.hasParam("Optional"); - if (optional && !host.getController().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantCopy", CardTranslation.getTranslatedName(cardToCopy.getName())))) { + if (optional && !host.getController().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantCopy", CardTranslation.getTranslatedName(cardToCopy.getName())), null)) { return; } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java index 4e7a3b8b0b7..fd15e1b68f5 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java @@ -12,7 +12,10 @@ import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.Lang; +import forge.util.Localizer; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -24,13 +27,14 @@ public class ConniveEffect extends SpellAbilityEffect { */ @Override protected String getStackDescription(SpellAbility sa) { - final StringBuilder sb = new StringBuilder(); - List tgt = getTargetCards(sa); - + if (tgt.size() <= 0) { + return ""; + } else { + final StringBuilder sb = new StringBuilder(); sb.append(Lang.joinHomogenous(tgt)).append(tgt.size() > 1 ? " connive." : " connives."); - return sb.toString(); + } } /* (non-Javadoc) @@ -39,55 +43,70 @@ public class ConniveEffect extends SpellAbilityEffect { @Override public void resolve(SpellAbility sa) { final Card host = sa.getHostCard(); - final Player hostCon = host.getController(); final Game game = host.getGame(); final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa); - GameEntityCounterTable table = new GameEntityCounterTable(); - final CardZoneTable triggerList = new CardZoneTable(); - Map discardedMap = Maps.newHashMap(); - Map moveParams = AbilityKey.newMap(); - moveParams.put(AbilityKey.LastStateBattlefield, sa.getLastStateBattlefield()); - moveParams.put(AbilityKey.LastStateGraveyard, sa.getLastStateGraveyard()); + CardCollection toConnive = getTargetCards(sa); + if (toConnive.isEmpty()) { // if nothing is conniving, we're done + return; + } - for (final Card c : getTargetCards(sa)) { - final Player p = c.getController(); - - p.drawCards(num, sa, moveParams); - - CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand); - dPHand = CardLists.filter(dPHand, CardPredicates.Presets.NON_TOKEN); - if (dPHand.isEmpty()) { // seems unlikely, but just to be safe - continue; // for loop over players - } - - CardCollection validCards = CardLists.getValidCards(dPHand, "Card", hostCon, host, sa); - - if (!p.canDiscardBy(sa, true)) { - continue; - } - - int amt = Math.min(validCards.size(), num); - CardCollectionView toBeDiscarded = amt == 0 ? CardCollection.EMPTY : - p.getController().chooseCardsToDiscardFrom(p, sa, validCards, amt, amt); - - if (toBeDiscarded.size() > 1) { - toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); - } - - discardedMap.put(p, toBeDiscarded); - - int numCntrs = CardLists.getValidCardCount(toBeDiscarded, "Card.nonLand", hostCon, host, sa); - - // need to get newest game state to check if it is still on the battlefield and the timestamp didn't change - Card gamec = game.getCardState(c); - // if the card is not in the game anymore, this might still return true, but it's no problem - if (game.getZoneOf(gamec).is(ZoneType.Battlefield) && gamec.equalsWithTimestamp(c)) { - c.addCounter(CounterEnumType.P1P1, numCntrs, p, table); + List controllers = new ArrayList<>(); + for (Card c : toConnive) { + final Player controller = c.getController(); + if (!controllers.contains(controller)) { + controllers.add(controller); + } + } + //order controllers by APNAP + int indexAP = controllers.indexOf(game.getPhaseHandler().getPlayerTurn()); + if (indexAP != -1) { + Collections.rotate(controllers, - indexAP); + } + + for (final Player p : controllers) { + CardCollection connivers = CardLists.filterControlledBy(toConnive, p); + while (connivers.size() > 0) { + GameEntityCounterTable table = new GameEntityCounterTable(); + final CardZoneTable triggerList = new CardZoneTable(); + Map discardedMap = Maps.newHashMap(); + Map moveParams = AbilityKey.newMap(); + moveParams.put(AbilityKey.LastStateBattlefield, sa.getLastStateBattlefield()); + moveParams.put(AbilityKey.LastStateGraveyard, sa.getLastStateGraveyard()); + + Card conniver = connivers.size() > 1 ? p.getController().chooseSingleEntityForEffect(connivers, sa, + Localizer.getInstance().getMessage("lblChooseConniver"), null) : connivers.get(0); + + p.drawCards(num, sa, moveParams); + + CardCollection validDisards = + CardLists.filter(p.getCardsIn(ZoneType.Hand), CardPredicates.Presets.NON_TOKEN); + if (validDisards.isEmpty() || !p.canDiscardBy(sa, true)) { // hand being empty unlikely, just to be safe + continue; + } + + int amt = Math.min(validDisards.size(), num); + CardCollectionView toBeDiscarded = amt == 0 ? CardCollection.EMPTY : + p.getController().chooseCardsToDiscardFrom(p, sa, validDisards, amt, amt); + + if (toBeDiscarded.size() > 1) { + toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); + } + + int numCntrs = CardLists.getValidCardCount(toBeDiscarded, "Card.nonLand", p, host, sa); + + // need to get newest game state to check if it is still on the battlefield and the timestamp didn't change + Card gamec = game.getCardState(conniver); + // if the card is not in the game anymore, this might still return true, but it's no problem + if (game.getZoneOf(gamec).is(ZoneType.Battlefield) && gamec.equalsWithTimestamp(conniver)) { + conniver.addCounter(CounterEnumType.P1P1, numCntrs, p, table); + } + connivers.remove(conniver); + discardedMap.put(p, CardCollection.getView(toBeDiscarded)); + discard(sa, triggerList, true, discardedMap, moveParams); + table.replaceCounterEffect(game, sa, true); + triggerList.triggerChangesZoneAll(game, sa); } } - discard(sa, triggerList, true, discardedMap, moveParams); - table.replaceCounterEffect(game, sa, true); - triggerList.triggerChangesZoneAll(game, sa); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ControlExchangeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ControlExchangeEffect.java index 078e13a8f2d..9e6a7e87c88 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ControlExchangeEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ControlExchangeEffect.java @@ -89,7 +89,7 @@ public class ControlExchangeEffect extends SpellAbilityEffect { if (sa.hasParam("Optional") && !sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblExchangeControl", CardTranslation.getTranslatedName(object1.getName()), - CardTranslation.getTranslatedName(object2.getName())))) { + CardTranslation.getTranslatedName(object2.getName())), null)) { return; } 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 f9309c3a91a..a7133e10f6a 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 @@ -1,5 +1,9 @@ package forge.game.ability.effects; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import com.google.common.collect.Lists; import forge.GameCommand; @@ -16,9 +20,6 @@ import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.Localizer; -import java.util.Arrays; -import java.util.List; - public class ControlGainEffect extends SpellAbilityEffect { @Override @@ -118,22 +119,24 @@ public class ControlGainEffect extends SpellAbilityEffect { final Game game = newController.getGame(); CardCollectionView tgtCards = null; - if (sa.hasParam("ControlledByTarget")) { - tgtCards = CardLists.filterControlledBy(tgtCards, getTargetPlayers(sa)); - } else if (sa.hasParam("Choices")) { + if (sa.hasParam("Choices")) { Player chooser = sa.hasParam("Chooser") ? AbilityUtils.getDefinedPlayers(source, sa.getParam("Chooser"), sa).get(0) : activator; CardCollectionView choices = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("Choices"), activator, source, sa); if (!choices.isEmpty()) { String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : - Localizer.getInstance().getMessage("lblChooseaCard") +" "; - tgtCards = activator.getController().chooseCardsForEffect(choices, sa, title, 1, 1, false, null); + Localizer.getInstance().getMessage("lblChooseaCard") +" "; + tgtCards = chooser.getController().chooseCardsForEffect(choices, sa, title, 1, 1, false, null); } } else { tgtCards = getDefinedCards(sa); } + if (tgtCards != null & sa.hasParam("ControlledByTarget")) { + tgtCards = CardLists.filterControlledBy(tgtCards, getTargetPlayers(sa)); + } + // in case source was LKI or still resolving if (source.isLKI() || source.getZone().is(ZoneType.Stack)) { source = game.getCardState(source); @@ -208,11 +211,11 @@ public class ControlGainEffect extends SpellAbilityEffect { } if (lose.contains("EOT")) { game.getEndOfTurn().addUntil(loseControl); - tgtC.setSVar("SacMe", "6"); + tgtC.addChangedSVars(Collections.singletonMap("SacMe", "6"), tStamp, 0); } if (lose.contains("EndOfCombat")) { game.getEndOfCombat().addUntil(loseControl); - tgtC.setSVar("SacMe", "6"); + tgtC.addChangedSVars(Collections.singletonMap("SacMe", "6"), tStamp, 0); } if (lose.contains("StaticCommandCheck")) { String leftVar = sa.getSVar(sa.getParam("StaticCommandCheckSVar")); @@ -274,7 +277,7 @@ public class ControlGainEffect extends SpellAbilityEffect { @Override public void run() { doLoseControl(c, hostCard, bTapOnLose, tStamp); - c.removeSVar("SacMe"); + c.removeChangedSVars(tStamp, 0); } }; 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 8c6b04601b5..a13870820f3 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 @@ -119,7 +119,7 @@ public class CopyPermanentEffect extends TokenEffectBase { TokenCreateTable tokenTable = new TokenCreateTable(); if (sa.hasParam("Optional") && !activator.getController().confirmAction(sa, null, - Localizer.getInstance().getMessage("lblCopyPermanentConfirm"))) { + Localizer.getInstance().getMessage("lblCopyPermanentConfirm"), null)) { return; } @@ -208,7 +208,7 @@ public class CopyPermanentEffect extends TokenEffectBase { if (choosen != null) { tgtCards.add(choosen); choices = CardLists.filter(choices, Predicates.not(CardPredicates.sharesNameWith(choosen))); - } else if (chooser.getController().confirmAction(sa, PlayerActionConfirmMode.OptionalChoose, Localizer.getInstance().getMessage("lblCancelChooseConfirm"))) { + } else if (chooser.getController().confirmAction(sa, PlayerActionConfirmMode.OptionalChoose, Localizer.getInstance().getMessage("lblCancelChooseConfirm"), null)) { break; } } @@ -224,6 +224,13 @@ public class CopyPermanentEffect extends TokenEffectBase { } for (final Card c : tgtCards) { + // 111.5. Similarly, if an effect would create a token that is a copy of an instant or sorcery card, no token is created. + // instant and sorcery can't enter the battlefield + // and it can't be replaced by other tokens + if (c.isInstant() || c.isSorcery()) { + continue; + } + // if it only targets player, it already got all needed cards from defined if (sa.usesTargeting() && !sa.getTargetRestrictions().canTgtPlayer() && !c.canBeTargetedBy(sa)) { continue; 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 bb6f226403c..602bf0f185d 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 @@ -26,6 +26,7 @@ import forge.util.Aggregates; import forge.util.CardTranslation; import forge.util.Localizer; import forge.util.Lang; +import forge.util.collect.FCollection; public class CopySpellAbilityEffect extends SpellAbilityEffect { @@ -88,7 +89,7 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect { SpellAbility chosenSA = controller.getController().chooseSingleSpellForEffect(tgtSpells, sa, Localizer.getInstance().getMessage("lblSelectASpellCopy"), ImmutableMap.of()); - if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantCopyTheSpell", CardTranslation.getTranslatedName(chosenSA.getHostCard().getName())))) { + if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantCopyTheSpell", CardTranslation.getTranslatedName(chosenSA.getHostCard().getName())), null)) { continue; } @@ -128,32 +129,38 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect { final Player p = (Player) o; if (p.equals(originalTargetPlayer)) continue; - if (p.isValid(type.split(","), chosenSA.getActivatingPlayer(), chosenSA.getHostCard(), sa)) { + if (p.isValid(type.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa)) { players.add(p); } } } - valid = CardLists.getValidCards(valid, type, chosenSA.getActivatingPlayer(), chosenSA.getHostCard(), sa); + valid = CardLists.getValidCards(valid, type, sa.getActivatingPlayer(), sa.getHostCard(), sa); Card originalTarget = Iterables.getFirst(getTargetCards(chosenSA), null); valid.remove(originalTarget); if (sa.hasParam("ChooseOnlyOne")) { - Card choice = controller.getController().chooseSingleEntityForEffect(valid, sa, Localizer.getInstance().getMessage("lblChooseOne"), null); + FCollection all = new FCollection<>(valid); + all.addAll(players); + GameEntity choice = controller.getController().chooseSingleEntityForEffect(all, sa, + Localizer.getInstance().getMessage("lblChooseOne"), null); if (choice != null) { - valid = new CardCollection(choice); + SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller); + resetFirstTargetOnCopy(copy, choice, targetedSA); + copies.add(copy); + } + } else { + for (final Card c : valid) { + SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller); + resetFirstTargetOnCopy(copy, c, targetedSA); + copies.add(copy); + } + for (final Player p : players) { + SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller); + resetFirstTargetOnCopy(copy, p, targetedSA); + copies.add(copy); } } - for (final Card c : valid) { - SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller); - resetFirstTargetOnCopy(copy, c, targetedSA); - copies.add(copy); - } - for (final Player p : players) { - SpellAbility copy = CardFactory.copySpellAbilityAndPossiblyHost(sa, chosenSA, controller); - resetFirstTargetOnCopy(copy, p, targetedSA); - copies.add(copy); - } } } else { for (int i = 0; i < amount; i++) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java index 4efbf6744cd..c0fe67d0440 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java @@ -123,6 +123,7 @@ public class CounterEffect extends SpellAbilityEffect { for (final SpellAbility tgtSA : sas) { final Card tgtSACard = tgtSA.getHostCard(); // should remember even that spell cannot be countered, e.g. Dovescape + // TODO use LKI in case the spell gets countered before (else X amounts would be missing) if (sa.hasParam("RememberCounteredCMC")) { sa.getHostCard().addRemembered(Integer.valueOf(tgtSACard.getCMC())); } @@ -152,9 +153,7 @@ public class CounterEffect extends SpellAbilityEffect { } if (sa.hasParam("RememberCountered")) { - if (sa.getParam("RememberCountered").equals("True")) { - sa.getHostCard().addRemembered(tgtSACard); - } + sa.getHostCard().addRemembered(tgtSACard); } if (sa.hasParam("RememberSplicedOntoCounteredSpell")) { 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 dfd5a35dcb1..06a38c522e2 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 @@ -20,7 +20,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect { final StringBuilder sb = new StringBuilder(); final CounterType cType = CounterType.getType(sa.getParam("CounterType")); - final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa); + final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParamOrDefault("CounterNum", "1"), sa); final String zone = sa.getParamOrDefault("ValidZone", "Battlefield"); sb.append("Put "); @@ -45,7 +45,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect { final Card host = sa.getHostCard(); final Player activator = sa.getActivatingPlayer(); final CounterType type = CounterType.getType(sa.getParam("CounterType")); - final int counterAmount = AbilityUtils.calculateAmount(host, sa.getParam("CounterNum"), sa); + final int counterAmount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("CounterNum", "1"), sa); final String valid = sa.getParam("ValidCards"); final ZoneType zone = sa.hasParam("ValidZone") ? ZoneType.smartValueOf(sa.getParam("ValidZone")) : ZoneType.Battlefield; final Game game = activator.getGame(); 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 df3ef86ba29..74fe90383b4 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 @@ -174,7 +174,7 @@ public class CountersPutEffect extends SpellAbilityEffect { boolean putOnDefined = sa.hasParam("PutOnDefined"); if (sa.hasParam("Optional") && !pc.confirmAction - (sa, null, Localizer.getInstance().getMessage("lblDoYouWantPutCounter"))) { + (sa, null, Localizer.getInstance().getMessage("lblDoYouWantPutCounter"), null)) { return; } @@ -231,7 +231,9 @@ public class CountersPutEffect extends SpellAbilityEffect { } Map params = Maps.newHashMap(); - params.put("CounterType", counterType); + if (counterType != null) { + params.put("CounterType", counterType); + } if (sa.hasParam("DividedRandomly")) { tgtObjects.addAll(choices); } else { @@ -509,7 +511,7 @@ public class CountersPutEffect extends SpellAbilityEffect { Player chooser = pc.chooseSingleEntityForEffect(activator.getOpponents(), sa, Localizer.getInstance().getMessage("lblChooseAnOpponent"), params); - if (chooser.getController().confirmAction(sa, PlayerActionConfirmMode.Tribute, message)) { + if (chooser.getController().confirmAction(sa, PlayerActionConfirmMode.Tribute, message, null)) { gameCard.setTributed(true); } else { continue; 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 81e00224dec..a55b34a1bc6 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 @@ -76,7 +76,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect { } if (!eachExisting && sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikePutRemoveCounters", ctype.getName(), - CardTranslation.getTranslatedName(gameCard.getName())))) { + CardTranslation.getTranslatedName(gameCard.getName())), null)) { continue; } if (!sa.usesTargeting() || gameCard.canBeTargetedBy(sa)) { 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 203ca93d6a3..302b1059d55 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 @@ -83,7 +83,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect { if (sa.hasParam("Optional")) { String ctrs = cntToRemove > 1 ? Localizer.getInstance().getMessage("lblCounters") : num.equals("All") ? Localizer.getInstance().getMessage("lblAllCounters") : Localizer.getInstance().getMessage("lblACounters"); - if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblRemove") + " " + ctrs + "?")) { + if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblRemove") + " " + ctrs + "?", null)) { return; } } 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 6591ceaeb6e..06d5f9904b5 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 @@ -156,7 +156,7 @@ public class DamageDealEffect extends DamageBaseEffect { List tgts = getTargets(sa); if (sa.hasParam("OptionalDecider")) { Player decider = Iterables.getFirst(AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("OptionalDecider"), sa), null); - if (decider != null && !decider.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantDealTargetDamageToTarget", String.valueOf(dmg), tgts.toString()))) { + if (decider != null && !decider.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantDealTargetDamageToTarget", String.valueOf(dmg), tgts.toString()), null)) { return; } } @@ -299,7 +299,7 @@ public class DamageDealEffect extends DamageBaseEffect { } else { damageMap.put(sourceLKI, c, dmg); if (sa.hasParam("ExcessSVar")) { - hostCard.setSVar(sa.getParam("ExcessSVar"), Integer.toString(excess)); + sa.setSVar(sa.getParam("ExcessSVar"), Integer.toString(excess)); } } } 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 1b815df1edf..f39011e5ceb 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 @@ -72,7 +72,7 @@ public class DelayedTriggerEffect extends SpellAbilityEffect { } if (sa.hasParam("RememberSVarAmount")) { - delTrig.addRemembered(AbilityUtils.calculateAmount(host, host.getSVar(sa.getParam("RememberSVarAmount")), sa)); + delTrig.addRemembered(AbilityUtils.calculateAmount(host, sa.getSVar(sa.getParam("RememberSVarAmount")), sa)); } if (sa.hasAdditionalAbility("Execute")) { 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 1be84f22b2c..1cd26f2d896 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 @@ -34,73 +34,77 @@ public class DigEffect extends SpellAbilityEffect { protected String getStackDescription(SpellAbility sa) { final Card host = sa.getHostCard(); final StringBuilder sb = new StringBuilder(); - final int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa); - final String toChange = sa.getParamOrDefault("ChangeNum", "1"); - final int numToChange = toChange.startsWith("All") ? numToDig : AbilityUtils.calculateAmount(host, sa.getParam("ChangeNum"), sa); final List tgtPlayers = getTargetPlayers(sa); - - String verb = " looks at "; - if (sa.hasParam("Reveal") && sa.getParam("Reveal").equals("True")) { - verb = " reveals "; - } else if (sa.hasParam("DestinationZone") && sa.getParam("DestinationZone").equals("Exile") && - numToDig == numToChange) { - verb = " exiles "; - } - sb.append(host.getController()).append(verb).append("the top "); - sb.append(numToDig == 1 ? "card" : Lang.getNumeral(numToDig) + " cards").append(" of "); - - if (tgtPlayers.contains(host.getController())) { - sb.append("their "); + final String spellDesc = sa.getParamOrDefault("SpellDescription", ""); + if (spellDesc.contains("X card")) { // X value can be changed after this goes to the stack, so use set desc + sb.append("[").append(host.getController()).append("] ").append(spellDesc); } else { - for (final Player p : tgtPlayers) { - sb.append(Lang.getInstance().getPossesive(p.getName())).append(" "); - } - } - sb.append("library."); + final int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa); + final String toChange = sa.getParamOrDefault("ChangeNum", "1"); + final int numToChange = toChange.startsWith("All") ? numToDig : AbilityUtils.calculateAmount(host, sa.getParam("ChangeNum"), sa); - if (numToDig != numToChange) { - String destZone1 = sa.hasParam("DestinationZone") ? - sa.getParam("DestinationZone").toLowerCase() : "hand"; - String destZone2 = sa.hasParam("DestinationZone2") ? - sa.getParam("DestinationZone2").toLowerCase() : "on the bottom of their library in any order."; - if (sa.hasParam("RestRandomOrder")) { - destZone2 = destZone2.replace("any", "a random"); + String verb = " looks at "; + if (sa.hasParam("DestinationZone") && sa.getParam("DestinationZone").equals("Exile") && + numToDig == numToChange) { + verb = " exiles "; + } else if (sa.hasParam("Reveal") && sa.getParam("Reveal").equals("True")) { + verb = " reveals "; } + sb.append(host.getController()).append(verb).append("the top "); + sb.append(numToDig == 1 ? "card" : Lang.getNumeral(numToDig) + " cards").append(" of "); - String verb2 = "put "; - String where = " into their hand "; - if (destZone1.equals("exile")) { - verb2 = "exile "; - where = " "; - } else if (destZone1.equals("battlefield")) { - verb2 = "put "; - where = " onto the battlefield "; - } - - sb.append(" They ").append(sa.hasParam("Optional") ? "may " : "").append(verb2); - if (sa.hasParam("ChangeValid")) { - String what = sa.hasParam("ChangeValidDesc") ? sa.getParam("ChangeValidDesc") : - sa.getParam("ChangeValid"); - if (!StringUtils.containsIgnoreCase(what, "card")) { - what = what + " card"; + if (tgtPlayers.contains(host.getController())) { + sb.append("their "); + } else { + for (final Player p : tgtPlayers) { + sb.append(Lang.getInstance().getPossesive(p.getName())).append(" "); } - sb.append(Lang.nounWithNumeralExceptOne(numToChange, what)).append(" from among them").append(where); - } else { - sb.append(Lang.getNumeral(numToChange)).append(" of them").append(where); } - sb.append(sa.hasParam("ExileFaceDown") ? "face down " : ""); - if (sa.hasParam("WithCounter") || sa.hasParam("ExileWithCounter")) { - String ctr = sa.hasParam("WithCounter") ? sa.getParam("WithCounter") : - sa.getParam("ExileWithCounter"); - sb.append("with a "); - sb.append(CounterType.getType(ctr).getName().toLowerCase()); - sb.append(" counter on it. They "); - } else { - sb.append("and "); - } - sb.append("put the rest ").append(destZone2); - } + sb.append("library."); + if (numToDig != numToChange) { + String destZone1 = sa.hasParam("DestinationZone") ? + sa.getParam("DestinationZone").toLowerCase() : "hand"; + String destZone2 = sa.hasParam("DestinationZone2") ? + sa.getParam("DestinationZone2").toLowerCase() : "on the bottom of their library in any order."; + if (sa.hasParam("RestRandomOrder")) { + destZone2 = destZone2.replace("any", "a random"); + } + + String verb2 = "put "; + String where = " into their hand "; + if (destZone1.equals("exile")) { + verb2 = "exile "; + where = " "; + } else if (destZone1.equals("battlefield")) { + verb2 = "put "; + where = " onto the battlefield "; + } + + sb.append(" They ").append(sa.hasParam("Optional") ? "may " : "").append(verb2); + if (sa.hasParam("ChangeValid")) { + String what = sa.hasParam("ChangeValidDesc") ? sa.getParam("ChangeValidDesc") : + sa.getParam("ChangeValid"); + if (!StringUtils.containsIgnoreCase(what, "card")) { + what = what + " card"; + } + sb.append(Lang.nounWithNumeralExceptOne(numToChange, what)).append(" from among them").append(where); + } else { + sb.append(Lang.getNumeral(numToChange)).append(" of them").append(where); + } + sb.append(sa.hasParam("ExileFaceDown") ? "face down " : ""); + if (sa.hasParam("WithCounter") || sa.hasParam("ExileWithCounter")) { + String ctr = sa.hasParam("WithCounter") ? sa.getParam("WithCounter") : + sa.getParam("ExileWithCounter"); + sb.append("with a "); + sb.append(CounterType.getType(ctr).getName().toLowerCase()); + sb.append(" counter on it. They "); + } else { + sb.append("and "); + } + sb.append("put the rest ").append(destZone2); + } + } return sb.toString(); } @@ -199,7 +203,7 @@ public class DigEffect extends SpellAbilityEffect { else if (sa.hasParam("RevealOptional")) { String question = TextUtil.concatWithSpace(Localizer.getInstance().getMessage("lblReveal") + ":", TextUtil.addSuffix(Lang.joinHomogenous(top),"?")); - hasRevealed = p.getController().confirmAction(sa, null, question); + hasRevealed = p.getController().confirmAction(sa, null, question, null); if (hasRevealed) { game.getAction().reveal(top, p); } @@ -272,7 +276,7 @@ public class DigEffect extends SpellAbilityEffect { // Optional abilities that use a dialog box to prompt the user to skip the ability (e.g. Explorer's Scope, Quest for Ula's Temple) if (optional && mayBeSkipped && !valid.isEmpty()) { String prompt = !optionalAbilityPrompt.isEmpty() ? optionalAbilityPrompt : Localizer.getInstance().getMessage("lblWouldYouLikeProceedWithOptionalAbility") + " " + host + "?\n\n(" + sa.getDescription() + ")"; - if (!p.getController().confirmAction(sa, null, TextUtil.fastReplace(prompt, "CARDNAME", CardTranslation.getTranslatedName(host.getName())))) { + if (!p.getController().confirmAction(sa, null, TextUtil.fastReplace(prompt, "CARDNAME", CardTranslation.getTranslatedName(host.getName())), null)) { return; } } @@ -413,7 +417,9 @@ public class DigEffect extends SpellAbilityEffect { c.setTapped(true); } if (destZone1.equals(ZoneType.Battlefield) && sa.hasParam("WithCounter")) { - c.addEtbCounter(CounterType.getType(sa.getParam("WithCounter")), 1, player); + final int numCtr = AbilityUtils.calculateAmount(host, + sa.getParamOrDefault("WithCounterNum", "1"), sa); + c.addEtbCounter(CounterType.getType(sa.getParam("WithCounter")), numCtr, player); } c = game.getAction().moveTo(zone, c, sa, moveParams); if (destZone1.equals(ZoneType.Battlefield)) { 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 6a2b42a432e..07ca5f7cfe6 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 @@ -120,7 +120,7 @@ public class DigUntilEffect extends SpellAbilityEffect { continue; } if (!sa.usesTargeting() || p.canBeTargetedBy(sa)) { - if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantDigYourLibrary"))) { + if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantDigYourLibrary"), null)) { continue; } CardCollection found = new CardCollection(); @@ -171,7 +171,7 @@ public class DigUntilEffect extends SpellAbilityEffect { final Card c = itr.next(); final ZoneType origin = c.getZone().getZoneType(); if (optionalFound && !p.getController().confirmAction(sa, null, - Localizer.getInstance().getMessage("lblDoYouWantPutCardToZone", foundDest.getTranslatedName()))) { + Localizer.getInstance().getMessage("lblDoYouWantPutCardToZone", foundDest.getTranslatedName()), null)) { continue; } Map moveParams = AbilityKey.newMap(); 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 5522a1e8765..9598a4265c5 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 @@ -153,7 +153,7 @@ public class DiscardEffect extends SpellAbilityEffect { } boolean runDiscard = !sa.hasParam("Optional") - || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, sa.getParam("DiscardMessage")); + || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, sa.getParam("DiscardMessage"), null); if (runDiscard) { toBeDiscarded = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedCards"), sa); @@ -197,7 +197,7 @@ public class DiscardEffect extends SpellAbilityEffect { continue; } String message = Localizer.getInstance().getMessage("lblWouldYouLikeRandomDiscardTargetCard", String.valueOf(numCards)); - boolean runDiscard = !sa.hasParam("Optional") || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message); + boolean runDiscard = !sa.hasParam("Optional") || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message, null); if (runDiscard) { final String valid = sa.getParamOrDefault("DiscardValid", "Card"); diff --git a/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java new file mode 100644 index 00000000000..8ded58eb4e0 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java @@ -0,0 +1,88 @@ +package forge.game.ability.effects; + +import forge.StaticData; +import forge.card.ICardFace; +import forge.game.Game; +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.CardZoneTable; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; +import forge.util.Localizer; + +import java.util.*; + + public class DraftEffect extends SpellAbilityEffect { + @Override + protected String getStackDescription(SpellAbility sa) { + final Card source = sa.getHostCard(); + final Player player = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa).get(0); + final ZoneType zone = ZoneType.smartValueOf(sa.getParamOrDefault("Zone", "Hand")); + + final StringBuilder sb = new StringBuilder(); + + sb.append(player).append(" drafts a card from ").append(source.getName()).append("'s spellbook"); + if (zone.equals("Hand")) { + sb.append("."); + } else if (zone.equals("Battlefield")) { + sb.append(" and puts it onto the battlefield."); + } else if (zone.equals("Exile")) { + sb.append(", then exiles it."); + } + + return sb.toString(); + } + + @Override + public void resolve(SpellAbility sa) { + Map moveParams = AbilityKey.newMap(); + moveParams.put(AbilityKey.LastStateBattlefield, sa.getLastStateBattlefield()); + moveParams.put(AbilityKey.LastStateGraveyard, sa.getLastStateGraveyard()); + final Card source = sa.getHostCard(); + final Player player = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa).get(0); + final Game game = player.getGame(); + final ZoneType zone = ZoneType.smartValueOf(sa.getParamOrDefault("Zone", "Hand")); + List spellbook = Arrays.asList(sa.getParam("Spellbook").split(",")); + final int numToDraft = AbilityUtils.calculateAmount(source, + sa.getParamOrDefault("DraftNum", "1"), sa); + CardCollection drafted = new CardCollection(); + + for (int i = 0; i < numToDraft; i++) { + String chosen = ""; + Collections.shuffle(spellbook); + List faces = new ArrayList<>(); + for (String name : spellbook.subList(0, 3)) { + // Cardnames that include "," must use ";" instead in Spellbook$ (i.e. Tovolar; Dire Overlord) + name = name.replace(";", ","); + faces.add(StaticData.instance().getCommonCards().getFaceByName(name)); + } + chosen = player.getController().chooseCardName(sa, faces, + Localizer.getInstance().getMessage("lblChooseCardDraft")); + if (!chosen.equals("")) { + Card card = Card.fromPaperCard(StaticData.instance().getCommonCards().getUniqueByName(chosen), player); + card.setTokenCard(true); + game.getAction().moveTo(ZoneType.None, card, sa, moveParams); + drafted.add(card); + } + } + + final CardZoneTable triggerList = new CardZoneTable(); + for (final Card c : drafted) { + Card made = game.getAction().moveTo(zone, c, sa, moveParams); + if (c != null) { + triggerList.put(ZoneType.None, made.getZone().getZoneType(), made); + } + if (sa.hasParam("RememberDrafted")) { + source.addRemembered(made); + } + } + triggerList.triggerChangesZoneAll(game, sa); + if (zone.equals(ZoneType.Library)) { + player.shuffle(sa); + } + } + } 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 3031c56f628..5c90c773ed4 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 @@ -72,7 +72,7 @@ public class DrawEffect extends SpellAbilityEffect { continue; } - if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantDrawCards", Lang.nounWithAmount(numCards, " card")))) { + if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantDrawCards", Lang.nounWithAmount(numCards, " card")), null)) { continue; } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ETBReplacementEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ETBReplacementEffect.java deleted file mode 100644 index 77241fecdac..00000000000 --- a/forge-game/src/main/java/forge/game/ability/effects/ETBReplacementEffect.java +++ /dev/null @@ -1,32 +0,0 @@ -package forge.game.ability.effects; - -import java.util.Map; - -import forge.game.Game; -import forge.game.ability.AbilityKey; -import forge.game.ability.SpellAbilityEffect; -import forge.game.card.Card; -import forge.game.spellability.SpellAbility; - -/** - * TODO: Write javadoc for this type. - * - */ -public class ETBReplacementEffect extends SpellAbilityEffect { - @Override - public void resolve(SpellAbility sa) { - final Game game = sa.getActivatingPlayer().getGame(); - final Card card = (Card) sa.getReplacingObject(AbilityKey.Card); - - Map params = AbilityKey.newMap(); - params.put(AbilityKey.CardLKI, sa.getReplacingObject(AbilityKey.CardLKI)); - params.put(AbilityKey.ReplacementEffect, sa.getReplacementEffect()); - params.put(AbilityKey.LastStateBattlefield, sa.getReplacingObject(AbilityKey.LastStateBattlefield)); - params.put(AbilityKey.LastStateGraveyard, sa.getReplacingObject(AbilityKey.LastStateGraveyard)); - - final SpellAbility root = sa.getRootAbility(); - SpellAbility cause = (SpellAbility) root.getReplacingObject(AbilityKey.Cause); - - game.getAction().moveToPlay(card, card.getController(), cause, params); - } -} \ No newline at end of file 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 9feb3afe02f..80b567426df 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 @@ -60,7 +60,7 @@ public class EffectEffect extends SpellAbilityEffect { boolean imprintOnHost = false; final String duration = sa.getParam("Duration"); - if (("UntilHostLeavesPlay".equals(duration) || "UntilLoseControlOfHost".equals(duration)) + if (((duration != null && duration.startsWith("UntilHostLeavesPlay")) || "UntilLoseControlOfHost".equals(duration)) && !(hostCard.isInPlay() || hostCard.isInZone(ZoneType.Stack))) { return; } @@ -88,6 +88,13 @@ public class EffectEffect extends SpellAbilityEffect { effectKeywords = sa.getParam("Keywords").split(","); } + if (sa.hasParam("RememberSpell")) { + rememberList = new FCollection<>(); + for (final String rem : sa.getParam("RememberSpell").split(",")) { + rememberList.addAll(AbilityUtils.getDefinedSpellAbilities(hostCard, rem, sa)); + } + } + if (sa.hasParam("RememberObjects")) { rememberList = new FCollection<>(); for (final String rem : sa.getParam("RememberObjects").split(",")) { @@ -271,7 +278,7 @@ public class EffectEffect extends SpellAbilityEffect { } if (sa.hasParam("CopySVar")) { - eff.setSVar(sa.getParam("CopySVar"), sa.getHostCard().getSVar(sa.getParam("CopySVar"))); + eff.setSVar(sa.getParam("CopySVar"), hostCard.getSVar(sa.getParam("CopySVar"))); } // Copy text changes 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 b1c98c06a8b..02b55a7212b 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 @@ -48,7 +48,7 @@ public class EncodeEffect extends SpellAbilityEffect { } // Handle choice of whether or not to encoded - if (!player.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantExileCardAndEncodeOntoYouCreature", CardTranslation.getTranslatedName(host.getName())))) { + if (!player.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantExileCardAndEncodeOntoYouCreature", CardTranslation.getTranslatedName(host.getName())), null)) { return; } diff --git a/forge-game/src/main/java/forge/game/ability/effects/EndTurnEffect.java b/forge-game/src/main/java/forge/game/ability/effects/EndTurnEffect.java index c19f9b2345c..aabcf131ffc 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/EndTurnEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/EndTurnEffect.java @@ -20,7 +20,7 @@ public class EndTurnEffect extends SpellAbilityEffect { public void resolve(SpellAbility sa) { final List enders = getDefinedPlayersOrTargeted(sa, "Defined"); final Player ender = enders.isEmpty() ? sa.getActivatingPlayer() : enders.get(0); - if (sa.hasParam("Optional") && !ender.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantEndTurn"))) { + if (sa.hasParam("Optional") && !ender.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantEndTurn"), null)) { return; } Game game = ender.getGame(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/FightEffect.java b/forge-game/src/main/java/forge/game/ability/effects/FightEffect.java index 738950a6063..2b96aa56a4d 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/FightEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/FightEffect.java @@ -59,7 +59,7 @@ public class FightEffect extends DamageBaseEffect { Player controller = host.getController(); boolean isOptional = sa.hasParam("Optional"); - if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeFight", CardTranslation.getTranslatedName(fighters.get(0).getName()), CardTranslation.getTranslatedName(fighters.get(1).getName())))) { + if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeFight", CardTranslation.getTranslatedName(fighters.get(0).getName()), CardTranslation.getTranslatedName(fighters.get(1).getName())), null)) { return; } diff --git a/forge-game/src/main/java/forge/game/ability/effects/GoadEffect.java b/forge-game/src/main/java/forge/game/ability/effects/GoadEffect.java index be2e7579c3d..69efb0dfe33 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/GoadEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/GoadEffect.java @@ -7,9 +7,25 @@ import forge.game.card.Card; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; +import forge.util.Lang; + +import java.util.List; public class GoadEffect extends SpellAbilityEffect { + @Override + protected String getStackDescription(SpellAbility sa) { + final Player player = sa.getActivatingPlayer(); + List tgt = getTargetCards(sa); + if (tgt.size() <= 0) { + return ""; + } else { + final StringBuilder sb = new StringBuilder(); + sb.append(player).append(" goads ").append(Lang.joinHomogenous(tgt)).append("."); + return sb.toString(); + } + } + @Override public void resolve(SpellAbility sa) { final Player player = sa.getActivatingPlayer(); 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 a41371981ca..0c5b98cbd78 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 @@ -67,7 +67,7 @@ public class ImmediateTriggerEffect extends SpellAbilityEffect { if (sa.hasParam("RememberSVarAmount")) { immediateTrig.addRemembered(AbilityUtils.calculateAmount(host, - host.getSVar(sa.getParam("RememberSVarAmount")), sa)); + sa.getSVar(sa.getParam("RememberSVarAmount")), sa)); } if (sa.hasAdditionalAbility("Execute")) { 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 index 4eaab4d61d6..48dee299959 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/InvestigateEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/InvestigateEffect.java @@ -40,7 +40,7 @@ public class InvestigateEffect extends TokenEffectBase { for (final Player p : getTargetPlayers(sa)) { for (int i = 0; i < amount; i++) { if (sa.hasParam("Optional") && !p.getController().confirmAction(sa, null, - Localizer.getInstance().getMessage("lblWouldYouLikeInvestigate"))) { + Localizer.getInstance().getMessage("lblWouldYouLikeInvestigate"), null)) { return; } diff --git a/forge-game/src/main/java/forge/game/ability/effects/LifeLoseEffect.java b/forge-game/src/main/java/forge/game/ability/effects/LifeLoseEffect.java index fa1334fffa6..136a70e7c3f 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/LifeLoseEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/LifeLoseEffect.java @@ -2,7 +2,6 @@ package forge.game.ability.effects; import forge.game.ability.AbilityUtils; -import forge.game.ability.ApiType; import forge.game.ability.SpellAbilityEffect; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -41,14 +40,7 @@ public class LifeLoseEffect extends SpellAbilityEffect { lifeLost += p.loseLife(lifeAmount, false, false); } } - sa.getHostCard().setSVar("AFLifeLost", "Number$" + lifeLost); - - // Exceptional case for Extort: must propagate the amount of life lost to subability, - // otherwise the first Extort trigger per game won't work - if (sa.getSubAbility() != null && ApiType.GainLife.equals(sa.getSubAbility().getApi())) { - sa.getSubAbility().setSVar("AFLifeLost", "Number$" + lifeLost); - } - + sa.setSVar("AFLifeLost", "Number$" + lifeLost); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/MakeCardEffect.java b/forge-game/src/main/java/forge/game/ability/effects/MakeCardEffect.java index 1d4a92e7d7e..c6ecc4d7610 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/MakeCardEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/MakeCardEffect.java @@ -9,6 +9,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.CardZoneTable; import forge.game.player.Player; import forge.game.player.PlayerCollection; import forge.game.spellability.SpellAbility; @@ -60,15 +61,18 @@ public class MakeCardEffect extends SpellAbilityEffect { amount--; } + final CardZoneTable triggerList = new CardZoneTable(); for (final Card c : cards) { - game.getAction().moveTo(zone, c, sa, moveParams); + Card made = game.getAction().moveTo(zone, c, sa, moveParams); + triggerList.put(ZoneType.None, made.getZone().getZoneType(), made); if (sa.hasParam("RememberMade")) { - sa.getHostCard().addRemembered(c); + sa.getHostCard().addRemembered(made); } if (sa.hasParam("ImprintMade")) { - sa.getHostCard().addImprintedCard(c); + sa.getHostCard().addImprintedCard(made); } } + triggerList.triggerChangesZoneAll(game, sa); if (zone.equals(ZoneType.Library)) { player.shuffle(sa); } 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 6419c7b1371..56ae7f19819 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 @@ -40,7 +40,7 @@ public class ManaEffect extends SpellAbilityEffect { final boolean optional = sa.hasParam("Optional"); final Game game = sa.getActivatingPlayer().getGame(); - if (optional && !sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantAddMana"))) { + if (optional && !sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantAddMana"), null)) { return; } diff --git a/forge-game/src/main/java/forge/game/ability/effects/MillEffect.java b/forge-game/src/main/java/forge/game/ability/effects/MillEffect.java index eb422e732d7..fc8ae802494 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/MillEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/MillEffect.java @@ -48,7 +48,7 @@ public class MillEffect extends SpellAbilityEffect { if (sa.hasParam("Optional")) { final String prompt = TextUtil.concatWithSpace(Localizer.getInstance().getMessage("lblDoYouWantPutLibraryCardsTo", destination.getTranslatedName())); // CR 701.13b - if (numCards > p.getZone(ZoneType.Library).size() || !p.getController().confirmAction(sa, null, prompt)) { + if (numCards > p.getZone(ZoneType.Library).size() || !p.getController().confirmAction(sa, null, prompt, null)) { continue; } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/PeekAndRevealEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PeekAndRevealEffect.java index 6e760ca602b..68e812f3d85 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PeekAndRevealEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PeekAndRevealEffect.java @@ -87,7 +87,7 @@ public class PeekAndRevealEffect extends SpellAbilityEffect { } if (doReveal && sa.hasParam("RevealOptional")) - doReveal = peekingPlayer.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblRevealCardToOtherPlayers")); + doReveal = peekingPlayer.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblRevealCardToOtherPlayers"), null); if (doReveal) { peekingPlayer.getGame().getAction().reveal(revealableCards, ZoneType.Library, libraryToPeek, !noPeek, diff --git a/forge-game/src/main/java/forge/game/ability/effects/PermanentEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PermanentEffect.java index ddb356c4da1..e985b309554 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PermanentEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PermanentEffect.java @@ -1,5 +1,6 @@ package forge.game.ability.effects; +import java.util.Collections; import java.util.Map; import com.google.common.collect.Lists; @@ -42,12 +43,12 @@ public class PermanentEffect extends SpellAbilityEffect { // some extra for Dashing if (sa.isDash() && c.isInPlay()) { - c.setSVar("EndOfTurnLeavePlay", "Dash"); + c.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "Dash"), c.getGame().getNextTimestamp(), 0); registerDelayedTrigger(sa, "Hand", Lists.newArrayList(c)); } // similar for Blitz keyword if (sa.isBlitz() && c.isInPlay()) { - c.setSVar("EndOfTurnLeavePlay", "Blitz"); + c.addChangedSVars(Collections.singletonMap("EndOfTurnLeavePlay", "Blitz"), c.getGame().getNextTimestamp(), 0); registerDelayedTrigger(sa, "Sacrifice", Lists.newArrayList(c)); } 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 a2915ecabaa..2778b62f446 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 @@ -259,7 +259,7 @@ public class PlayEffect extends SpellAbilityEffect { } if (singleOption && sa.getTargetCard() == null) sa.setPlayEffectCard(tgtCard);// show card to play rather than showing the source card - if (singleOption && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())))) { + if (singleOption && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())), null)) { if (wasFaceDown) { tgtCard.turnFaceDownNoUpdate(); tgtCard.updateStateForView(); @@ -285,6 +285,7 @@ public class PlayEffect extends SpellAbilityEffect { } } + // TODO if cost isn't replaced should include alternative ones // get basic spells (no flashback, etc.) List sas = AbilityUtils.getBasicSpellsFromPlayEffect(tgtCard, controller); if (sa.hasParam("ValidSA")) { @@ -435,8 +436,9 @@ public class PlayEffect extends SpellAbilityEffect { } final Zone currentZone = game.getCardState(tgtCard).getZone(); - if (!originZone.equals(currentZone)) { - triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), game.getCardState(tgtCard)); + if (!currentZone.equals(originZone)) { + //fix Garth One-Eye activated ability and the likes.. + triggerList.put(originZone == null ? null : originZone.getZoneType(), currentZone.getZoneType(), game.getCardState(tgtCard)); } triggerList.triggerChangesZoneAll(game, sa); } 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 ef9342b3b0e..9a2b1e323b3 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 @@ -37,13 +37,15 @@ public class PumpEffect extends SpellAbilityEffect { final long timestamp) { final Card host = sa.getHostCard(); final Game game = host.getGame(); + final String duration = sa.getParam("Duration"); + //if host is not on the battlefield don't apply // Suspend should does Affect the Stack - if (("UntilHostLeavesPlay".equals(sa.getParam("Duration")) || "UntilLoseControlOfHost".equals(sa.getParam("Duration"))) + if (((duration != null && duration.startsWith("UntilHostLeavesPlay")) || "UntilLoseControlOfHost".equals(duration)) && !(host.isInPlay() || host.isInZone(ZoneType.Stack))) { return; } - if ("UntilLoseControlOfHost".equals(sa.getParam("Duration")) && host.getController() != sa.getActivatingPlayer()) { + if ("UntilLoseControlOfHost".equals(duration) && host.getController() != sa.getActivatingPlayer()) { return; } @@ -92,7 +94,7 @@ public class PumpEffect extends SpellAbilityEffect { addLeaveBattlefieldReplacement(gameCard, sa, sa.getParam("LeaveBattlefield")); } - if (!"Permanent".equals(sa.getParam("Duration"))) { + if (!"Permanent".equals(duration)) { // If not Permanent, remove Pumped at EOT final GameCommand untilEOT = new GameCommand() { private static final long serialVersionUID = -42244224L; @@ -123,9 +125,11 @@ public class PumpEffect extends SpellAbilityEffect { private static void applyPump(final SpellAbility sa, final Player p, final List keywords, final long timestamp) { final Card host = sa.getHostCard(); + final String duration = sa.getParam("Duration"); + //if host is not on the battlefield don't apply // Suspend should does Affect the Stack - if (("UntilHostLeavesPlay".equals(sa.getParam("Duration")) || "UntilLoseControlOfHost".equals(sa.getParam("Duration"))) + if (((duration != null && duration.startsWith("UntilHostLeavesPlay")) || "UntilLoseControlOfHost".equals(duration)) && !(host.isInPlay() || host.isInZone(ZoneType.Stack))) { return; } @@ -134,7 +138,7 @@ public class PumpEffect extends SpellAbilityEffect { p.addChangedKeywords(keywords, ImmutableList.of(), timestamp, 0); } - if (!"Permanent".equals(sa.getParam("Duration"))) { + if (!"Permanent".equals(duration)) { // If not Permanent, remove Pumped at EOT final GameCommand untilEOT = new GameCommand() { private static final long serialVersionUID = -32453460L; @@ -348,7 +352,7 @@ public class PumpEffect extends SpellAbilityEffect { ? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets) : Localizer.getInstance().getMessage("lblApplyPumpToTarget", targets); - if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message)) { + if (!sa.getActivatingPlayer().getController().confirmAction(sa, null, message, null)) { return; } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/RearrangeTopOfLibraryEffect.java b/forge-game/src/main/java/forge/game/ability/effects/RearrangeTopOfLibraryEffect.java index 7f16c272931..3d6c7ab50bd 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/RearrangeTopOfLibraryEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/RearrangeTopOfLibraryEffect.java @@ -116,7 +116,7 @@ public class RearrangeTopOfLibraryEffect extends SpellAbilityEffect { Card next = orderedCards.get(i); player.getGame().getAction().moveToLibrary(next, 0, sa); } - if (mayshuffle && activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantShuffleTheLibrary"))) { + if (mayshuffle && activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantShuffleTheLibrary"), null)) { player.shuffle(sa); } } 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 4576d7794ba..2323378939f 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 @@ -47,7 +47,7 @@ public class RepeatEachEffect extends SpellAbilityEffect { final Player player = sa.getActivatingPlayer(); final Game game = player.getGame(); if (sa.hasParam("Optional") && sa.hasParam("OptionPrompt") && //for now, OptionPrompt is needed - !player.getController().confirmAction(sa, null, sa.getParam("OptionPrompt"))) { + !player.getController().confirmAction(sa, null, sa.getParam("OptionPrompt"), null)) { return; } @@ -151,7 +151,7 @@ public class RepeatEachEffect extends SpellAbilityEffect { } } for (final Player p : repeatPlayers) { - if (optional && !p.getController().confirmAction(repeat, null, sa.getParam("RepeatOptionalMessage"))) { + if (optional && !p.getController().confirmAction(repeat, null, sa.getParam("RepeatOptionalMessage"), null)) { continue; } if (nextTurn) { 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 ede9d56820f..3438ee5aa8e 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 @@ -115,7 +115,7 @@ public class RepeatEffect extends SpellAbilityEffect { Player decider = sa.hasParam("RepeatOptionalDecider") ? AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("RepeatOptionalDecider"), sa).get(0) : sa.getActivatingPlayer(); - return decider.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantRepeatProcessAgain")); + return decider.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantRepeatProcessAgain"), null); } return true; diff --git a/forge-game/src/main/java/forge/game/ability/effects/RestartGameEffect.java b/forge-game/src/main/java/forge/game/ability/effects/RestartGameEffect.java index 6b81775dd39..5eeac688763 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/RestartGameEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/RestartGameEffect.java @@ -55,6 +55,7 @@ public class RestartGameEffect extends SpellAbilityEffect { game.getStack().reset(); game.clearCounterAddedThisTurn(); game.setMonarch(null); + game.setHasInitiative(null); game.setDayTime(null); GameAction action = game.getAction(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/RevealHandEffect.java b/forge-game/src/main/java/forge/game/ability/effects/RevealHandEffect.java index c538a17918c..684c3b01d0f 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/RevealHandEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/RevealHandEffect.java @@ -41,7 +41,7 @@ public class RevealHandEffect extends SpellAbilityEffect { for (final Player p : getTargetPlayers(sa)) { if ((tgt == null) || p.canBeTargetedBy(sa)) { - if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantRevealYourHand"))) { + if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantRevealYourHand"), null)) { continue; } CardCollectionView hand = p.getCardsIn(ZoneType.Hand); diff --git a/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java b/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java index 2931da09e9f..fb0a99ace99 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java @@ -126,13 +126,13 @@ public class RollDiceEffect extends SpellAbilityEffect { total += modifier; if (sa.hasParam("ResultSVar")) { - host.setSVar(sa.getParam("ResultSVar"), Integer.toString(total)); + sa.setSVar(sa.getParam("ResultSVar"), Integer.toString(total)); } if (sa.hasParam("ChosenSVar")) { int chosen = player.getController().chooseNumber(sa, Localizer.getInstance().getMessage("lblChooseAResult"), rolls, player); String message = Localizer.getInstance().getMessage("lblPlayerChooseValue", player, chosen); player.getGame().getAction().notifyOfValue(sa, player, message, player); - host.setSVar(sa.getParam("ChosenSVar"), Integer.toString(chosen)); + sa.setSVar(sa.getParam("ChosenSVar"), Integer.toString(chosen)); if (sa.hasParam("OtherSVar")) { int other = rolls.get(0); for (int i = 1; i < rolls.size(); ++i) { @@ -141,7 +141,7 @@ public class RollDiceEffect extends SpellAbilityEffect { break; } } - host.setSVar(sa.getParam("OtherSVar"), Integer.toString(other)); + sa.setSVar(sa.getParam("OtherSVar"), Integer.toString(other)); } } 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 76e8e1c234c..5b512848e43 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 @@ -46,7 +46,7 @@ public class SacrificeEffect extends SpellAbilityEffect { if (sa.hasParam("Echo")) { boolean isPaid; if (activator.hasKeyword("You may pay 0 rather than pay the echo cost for permanents you control.") - && activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPayEcho") + " {0}?")) { + && activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPayEcho") + " {0}?", null)) { isPaid = true; } else { isPaid = activator.getController().payManaOptional(card, new Cost(sa.getParam("Echo"), true), @@ -110,7 +110,7 @@ public class SacrificeEffect extends SpellAbilityEffect { if (valid.equals("Self") && game.getZoneOf(card) != null) { if (game.getZoneOf(card).is(ZoneType.Battlefield)) { if (!optional || activator.getController().confirmAction(sa, null, - Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", card.getName()))) { + Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", card.getName()), null)) { if (game.getAction().sacrifice(card, sa, true, table, params) != null) { if (remSacrificed) { card.addRemembered(card); @@ -153,7 +153,7 @@ public class SacrificeEffect extends SpellAbilityEffect { if (sa.hasParam("Random")) { choosenToSacrifice = Aggregates.random(validTargets, Math.min(amount, validTargets.size()), new CardCollection()); - } else if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantSacrifice"))) { + } else if (optional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantSacrifice"), null)) { choosenToSacrifice = CardCollection.EMPTY; } else { boolean isStrict = sa.hasParam("StrictAmount"); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ScryEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ScryEffect.java index e5d1ac937dc..5d77f6350b1 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ScryEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ScryEffect.java @@ -44,7 +44,7 @@ public class ScryEffect extends SpellAbilityEffect { // Optional here for spells that have optional multi-player scrying for (final Player p : getTargetPlayers(sa)) { if ( (!sa.usesTargeting() || p.canBeTargetedBy(sa)) && - (!isOptional || p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWanttoScry"))) ) { + (!isOptional || p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWanttoScry"), null)) ) { players.add(p); } } 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 34c596406cb..39c3d3eb71a 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 @@ -150,7 +150,7 @@ public class SetStateEffect extends SpellAbilityEffect { if (optional) { String message = TextUtil.concatWithSpace("Transform", gameCard.getName(), "?"); - if (!p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message)) { + if (!p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message, null)) { return; } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ShuffleEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ShuffleEffect.java index 0b438fc57c3..0832d421302 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ShuffleEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ShuffleEffect.java @@ -21,7 +21,7 @@ public class ShuffleEffect extends SpellAbilityEffect { for (final Player p : tgtPlayers) { if ((tgt == null) || p.canBeTargetedBy(sa)) { - boolean mustShuffle = !optional || sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblHaveTargetShuffle", p.getName())); + boolean mustShuffle = !optional || sa.getActivatingPlayer().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblHaveTargetShuffle", p.getName()), null); if (mustShuffle) p.shuffle(sa); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/SurveilEffect.java b/forge-game/src/main/java/forge/game/ability/effects/SurveilEffect.java index b148a44af13..f79c5c9e43e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/SurveilEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/SurveilEffect.java @@ -43,7 +43,7 @@ public class SurveilEffect extends SpellAbilityEffect { for (final Player p : getTargetPlayers(sa)) { if (!sa.usesTargeting() || p.canBeTargetedBy(sa)) { - if (isOptional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantSurveil"))) { + if (isOptional && !p.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantSurveil"), null)) { continue; } diff --git a/forge-game/src/main/java/forge/game/ability/effects/TakeInitiativeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/TakeInitiativeEffect.java new file mode 100644 index 00000000000..9fa95a288a5 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/TakeInitiativeEffect.java @@ -0,0 +1,35 @@ +package forge.game.ability.effects; + +import java.util.List; + +import forge.game.ability.SpellAbilityEffect; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.util.Lang; + +public class TakeInitiativeEffect extends SpellAbilityEffect { + + @Override + protected String getStackDescription(SpellAbility sa) { + final StringBuilder sb = new StringBuilder(); + + final List tgtPlayers = getTargetPlayers(sa); + + sb.append(Lang.joinHomogenous(tgtPlayers)).append(tgtPlayers.size() == 1 ? " takes" : " take"); + sb.append(" the initiative."); + + return sb.toString(); + } + + @Override + public void resolve(SpellAbility sa) { + // TODO: improve ai and fix corner cases + final String set = sa.getHostCard().getSetCode(); + + for (final Player p : getTargetPlayers(sa)) { + if (!sa.usesTargeting() || p.canBeTargetedBy(sa)) { + p.getGame().getAction().takeInitiative(p, set); + } + } + } +} 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 13c4646e42c..2d3658dc705 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 @@ -64,7 +64,8 @@ public class TokenEffect extends TokenEffectBase { if (numTokens != 0) { //0 probably means calculation isn't ready in time for stack if (numTokens != 1) { //if we are making more than one, substitute the numeral for a/an String numeral = " " + Lang.getNumeral(numTokens) + " "; - String target = " " + words.get(words.indexOf(verb) + 1) + " "; + List words2 = Arrays.asList(desc.split(" ")); + String target = " " + words2.get(words2.indexOf(verb) + 1) + " "; desc = desc.replaceFirst(target, numeral); } //try to cut out unneeded description, which would now be confusing diff --git a/forge-game/src/main/java/forge/game/ability/effects/VentureEffect.java b/forge-game/src/main/java/forge/game/ability/effects/VentureEffect.java index 01009a820de..239576a109c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/VentureEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/VentureEffect.java @@ -24,6 +24,7 @@ import forge.game.trigger.WrappedAbility; import forge.game.zone.ZoneType; import forge.item.PaperCard; import forge.util.Localizer; +import forge.util.PredicateString.StringOp; public class VentureEffect extends SpellAbilityEffect { @@ -42,10 +43,19 @@ public class VentureEffect extends SpellAbilityEffect { } } - // Create a new dungeon card chosen by player in command zone. - List dungeonCards = StaticData.instance().getVariantCards().getAllCards( - Predicates.compose(CardRulesPredicates.Presets.IS_DUNGEON, PaperCard.FN_GET_RULES)); - + List dungeonCards = null; + if (sa.hasParam("Dungeon")) { + dungeonCards = StaticData.instance().getVariantCards() + .getAllCards(Predicates.compose( + Predicates.and(CardRulesPredicates.Presets.IS_DUNGEON, + CardRulesPredicates.subType(StringOp.EQUALS, sa.getParam("Dungeon"))), + PaperCard.FN_GET_RULES)); + } else { + // Create a new dungeon card chosen by player in command zone. + dungeonCards = StaticData.instance().getVariantCards().getAllCards( + Predicates.compose(CardRulesPredicates.Presets.IS_DUNGEON, PaperCard.FN_GET_RULES)); + dungeonCards.removeIf(c -> !c.getRules().isEnterableDungeon()); + } String message = Localizer.getInstance().getMessage("lblChooseDungeon"); Card dungeon = player.getController().chooseDungeon(player, dungeonCards, message); 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 5ef63f3ba3b..2305418b22a 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 @@ -126,7 +126,7 @@ public class VoteEffect extends SpellAbilityEffect { } if (sa.hasParam("StoreVoteNum")) { for (final Object type : voteType) { - host.setSVar("VoteNum" + type, "Number$" + votes.get(type).size()); + sa.setSVar("VoteNum" + type, "Number$" + votes.get(type).size()); } } else { for (final String subAb : subAbs) { 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 f5295311b61..ef63ba39d97 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -54,13 +54,7 @@ import forge.game.replacement.ReplacementHandler; import forge.game.replacement.ReplacementResult; import forge.game.replacement.ReplacementType; import forge.game.spellability.*; -import forge.game.staticability.StaticAbility; -import forge.game.staticability.StaticAbilityCantAttackBlock; -import forge.game.staticability.StaticAbilityCantPutCounter; -import forge.game.staticability.StaticAbilityCantSacrifice; -import forge.game.staticability.StaticAbilityCantTarget; -import forge.game.staticability.StaticAbilityCantTransform; -import forge.game.staticability.StaticAbilityIgnoreLegendRule; +import forge.game.staticability.*; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerType; @@ -165,6 +159,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private final NavigableMap clonedStates = Maps.newTreeMap(); // Layer 1 + private final Table> changedSVars = TreeBasedTable.create(); + private final Map mayLook = Maps.newHashMap(); private final PlayerCollection mayLookFaceDownExile = new PlayerCollection(); private final PlayerCollection mayLookTemp = new PlayerCollection(); @@ -181,15 +177,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private final CardChangedWords changedTextColors = new CardChangedWords(); private final CardChangedWords changedTextTypes = new CardChangedWords(); - /** Original values of SVars changed by text changes. */ - private Map originalSVars = Maps.newHashMap(); - private final Set rememberedObjects = Sets.newLinkedHashSet(); private Map flipResult; - private List> receivedDamageFromThisTurn = Lists.newArrayList(); - private Map receivedDamageFromPlayerThisTurn = Maps.newHashMap(); - private final Map assignedDamageMap = Maps.newTreeMap(); private boolean isCommander = false; @@ -264,7 +254,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private String oracleText = ""; - private int damage; + private Map damage = Maps.newHashMap(); private boolean hasBeenDealtDeathtouchDamage; // regeneration @@ -510,7 +500,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { if (game != null) { // update Type, color and keywords again if they have changed if (!changedCardTypes.isEmpty()) { - currentState.getView().updateType(currentState); + updateTypesForView(); } updateColorForView(); @@ -958,7 +948,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } devouredCards.add(c); } - public final void clearDevoured() { devouredCards = null; } @@ -989,7 +978,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { delvedCards = null; } - public final CardCollectionView getConvoked() { return CardCollection.getView(convokedCards); } @@ -1592,10 +1580,20 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } public final String getSVar(final String var) { + for (Map map : changedSVars.values()) { + if (map.containsKey(var)) { + return map.get(var); + } + } return currentState.getSVar(var); } public final boolean hasSVar(final String var) { + for (Map map : changedSVars.values()) { + if (map.containsKey(var)) { + return true; + } + } return currentState.hasSVar(var); } @@ -1621,6 +1619,13 @@ public class Card extends GameEntity implements Comparable, IHasSVars { currentState.removeSVar(var); } + public final void addChangedSVars(Map map, long timestamp, long staticId) { + this.changedSVars.put(timestamp, staticId, map); + } + public final void removeChangedSVars(long timestamp, long staticId) { + this.changedSVars.remove(timestamp, staticId); + } + public final int getTurnInZone() { return turnInZone; } @@ -2091,7 +2096,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { || keyword.equals("Ascend") || keyword.equals("Totem armor") || keyword.equals("Battle cry") || keyword.equals("Devoid") || keyword.equals("Riot") || keyword.equals("Daybound") || keyword.equals("Nightbound") - || keyword.equals("Friends forever")) { + || keyword.equals("Friends forever") || keyword.equals("Choose a Background")) { sbLong.append(keyword).append(" (").append(inst.getReminderText()).append(")"); } else if (keyword.startsWith("Partner:")) { final String[] k = keyword.split(":"); @@ -2583,7 +2588,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { || keyword.equals("Devoid") || keyword.equals("Lifelink") || keyword.equals("Split second")) { sbBefore.append(keyword).append(" (").append(inst.getReminderText()).append(")"); - sbBefore.append("\r\n"); + sbBefore.append("\r\n\r\n"); } else if (keyword.equals("Conspire") || keyword.equals("Epic") || keyword.equals("Suspend") || keyword.equals("Jump-start") || keyword.equals("Fuse")) { @@ -3306,8 +3311,14 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } public final void removeTempController(final Player player) { + boolean changed = false; // Remove each key that yields this player - this.tempControllers.values().remove(player); + while (tempControllers.values().remove(player)) { + changed = true; + } + if (changed) { + view.updateController(this); + } } public final void clearTempControllers() { @@ -4124,8 +4135,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } public final boolean toughnessAssignsDamage() { - return getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.toughnessAssignsDamage) - || hasKeyword("CARDNAME assigns combat damage equal to its toughness rather than its power"); + return StaticAbilityCombatDamageToughness.combatDamageToughness(this); } // How much combat damage does the card deal @@ -4448,6 +4458,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { KeywordInterface result; if (staticId < 1 || !storedKeywords.contains(staticId, kw)) { result = Keyword.getInstance(kw); + result.setStaticId(staticId); result.createTraits(this, false); if (staticId > 0) { storedKeywords.put(staticId, kw, result); @@ -4457,6 +4468,12 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } return result; } + + public final void addKeywordForStaticAbility(KeywordInterface kw) { + if (kw.getStaticId() > 0) { + storedKeywords.put(kw.getStaticId(), kw.getOriginal(), kw); + } + } public final void addChangedCardKeywordsByText(final List keywords, final long timestamp, final long staticId, final boolean updateView) { // keywords should already created for Card, so no addKeywordsToCard @@ -4597,7 +4614,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { * Update the changed text of the intrinsic spell abilities and keywords. */ public void updateChangedText() { - resetChangedSVars(); // update type List toAdd = Lists.newArrayList(); @@ -4665,26 +4681,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { changedTextTypes.copyFrom(other.changedTextTypes); } - /** - * Change a SVar due to a text change effect. Change is volatile and will be - * reverted upon refreshing text changes (unless it is changed again at that - * time). - * - * @param key the SVar name. - * @param value the new SVar value. - */ - public final void changeSVar(final String key, final String value) { - originalSVars.put(key, getSVar(key)); - setSVar(key, value); - } - - private void resetChangedSVars() { - for (final Entry svar : originalSVars.entrySet()) { - setSVar(svar.getKey(), svar.getValue()); - } - originalSVars.clear(); - } - public final KeywordInterface addIntrinsicKeyword(final String s) { KeywordInterface inst = currentState.addIntrinsicKeyword(s, true); if (inst != null) { @@ -5298,62 +5294,15 @@ public class Card extends GameEntity implements Comparable, IHasSVars { damageHistory = history; } - public final List> getReceivedDamageFromThisTurn() { - return receivedDamageFromThisTurn; + public List> getDamageReceivedThisTurn() { + return damageReceivedThisTurn; } - public final void setReceivedDamageFromThisTurn(final List> receivedDamageList) { - receivedDamageFromThisTurn = Lists.newArrayList(receivedDamageList); - } - public final Map getReceivedDamageFromPlayerThisTurn() { - return receivedDamageFromPlayerThisTurn; - } - public final void setReceivedDamageFromPlayerThisTurn(final Map receivedDamageMap) { - receivedDamageFromPlayerThisTurn = Maps.newHashMap(receivedDamageMap); - } - - public int getReceivedDamageByPlayerThisTurn(final Player p) { - if (receivedDamageFromPlayerThisTurn.containsKey(p)) { - return receivedDamageFromPlayerThisTurn.get(p); - } - return 0; - } - - public final void addReceivedDamageFromThisTurn(Card c, final int damage) { - int currentDamage = 0; - // because Aegar cares about the past state we need to keep all LKI instances - receivedDamageFromThisTurn.add(Pair.of(c.isLKI() ? c : CardUtil.getLKICopy(c), damage)); - - Player p = c.getController(); - if (p != null) { - if (receivedDamageFromPlayerThisTurn.containsKey(p)) { - currentDamage = receivedDamageFromPlayerThisTurn.get(p); - } - receivedDamageFromPlayerThisTurn.put(p, damage+currentDamage); - } - } - public final void resetReceivedDamageFromThisTurn() { - receivedDamageFromThisTurn.clear(); - receivedDamageFromPlayerThisTurn.clear(); - } - - public final int getTotalDamageReceivedThisTurn() { - int total = 0; - for (Integer i : receivedDamageFromPlayerThisTurn.values()) { - total += i; - } - return total; + public void setDamageReceivedThisTurn(List> dmg) { + damageReceivedThisTurn.addAll(dmg); } public final boolean hasDealtDamageToOpponentThisTurn() { - for (final GameEntity e : getDamageHistory().getThisTurnDamaged().keySet()) { - if (e instanceof Player) { - final Player p = (Player) e; - if (getController().isOpponentOf(p)) { - return true; - } - } - } - return false; + return getDamageHistory().getDamageDoneThisTurn(null, true, null, "Player.Opponent", this, getController(), null) > 0; } /** @@ -5362,11 +5311,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { * @return the damage done to player p this turn */ public final int getTotalDamageDoneBy() { - int sum = 0; - for (final GameEntity e : getDamageHistory().getThisTurnDamaged().keySet()) { - sum += getDamageHistory().getThisTurnDamaged().get(e); - } - return sum; + return getDamageHistory().getDamageDoneThisTurn(null, false, null, null, this, getController(), null); } // this is the amount of damage a creature needs to receive before it dies @@ -5389,15 +5334,26 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } public final int getDamage() { - return damage; + int sum = 0; + for (int i : damage.values()) { + sum += i; + } + return sum; } public final void setDamage(int damage0) { - if (damage == damage0) { return; } - damage = damage0; + if (getDamage() == damage0) { return; } + damage.clear(); + if (damage0 != 0) { + damage.put(0, damage0); + } view.updateDamage(this); getGame().fireEvent(new GameEventCardStatsChanged(this)); } + public int getMaxDamageFromSource() { + return damage.isEmpty() ? 0 : Collections.max(damage.values()); + } + public final boolean hasBeenDealtDeathtouchDamage() { return hasBeenDealtDeathtouchDamage; } @@ -5548,14 +5504,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { // Run replacement effects getGame().getReplacementHandler().run(ReplacementType.DealtDamage, AbilityKey.mapFromAffected(this)); - addReceivedDamageFromThisTurn(source, damageIn); - source.getDamageHistory().registerDamage(this, damageIn); - if (isCombat) { - source.getDamageHistory().registerCombatDamage(this, damageIn); - } else { - getDamageHistory().setHasBeenDealtNonCombatDamageThisTurn(true); - } - // Run triggers Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.DamageSource, source); @@ -5582,7 +5530,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars { damageType = DamageType.M1M1Counters; } else { // 120.3e - damage += damageIn; + int old = damage.getOrDefault(Objects.hash(source.getId(), source.getTimestamp()), 0); + damage.put(Objects.hash(source.getId(), source.getTimestamp()), old + damageIn); view.updateDamage(this); } @@ -5720,7 +5669,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } return false; } - public final void setForetold(final boolean foretold) { this.foretold = foretold; } @@ -5738,7 +5686,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public final void setForetoldThisTurn(final boolean foretoldThisTurn) { this.foretoldThisTurn = foretoldThisTurn; } - public void resetForetoldThisTurn() { foretoldThisTurn = false; } @@ -5809,7 +5756,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public final long getBestowTimestamp() { return bestowTimestamp; } - public final void setBestowTimestamp(final long t) { bestowTimestamp = t; } @@ -6212,7 +6158,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { setDamage(0); } setHasBeenDealtDeathtouchDamage(false); - resetReceivedDamageFromThisTurn(); setRegeneratedThisTurn(0); resetShield(); setBecameTargetThisTurn(false); @@ -6220,6 +6165,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { clearMustBlockCards(); getDamageHistory().setCreatureAttackedLastTurnOf(turn, getDamageHistory().getCreatureAttacksThisTurn() > 0); getDamageHistory().newTurn(); + damageReceivedThisTurn.clear(); clearBlockedByThisTurn(); clearBlockedThisTurn(); resetMayPlayTurn(); diff --git a/forge-game/src/main/java/forge/game/card/CardDamageHistory.java b/forge-game/src/main/java/forge/game/card/CardDamageHistory.java index ef53f011c6e..105bb992bc8 100644 --- a/forge-game/src/main/java/forge/game/card/CardDamageHistory.java +++ b/forge-game/src/main/java/forge/game/card/CardDamageHistory.java @@ -4,11 +4,15 @@ package forge.game.card; import java.util.List; import java.util.Map; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; +import org.apache.commons.lang3.tuple.Pair; +import com.google.common.collect.Lists; + +import forge.game.CardTraitBase; import forge.game.GameEntity; import forge.game.player.Player; +import forge.game.zone.ZoneType; +import forge.util.collect.FCollection; /** * TODO: Write javadoc for this type. @@ -19,7 +23,7 @@ public class CardDamageHistory { private boolean creatureAttackedThisCombat = false; private boolean creatureBlockedThisCombat = false; private boolean creatureGotBlockedThisCombat = false; - private boolean receivedNonCombatDamageThisTurn = false; + private List attackedThisTurn = Lists.newArrayList(); private final List creatureAttackedLastTurnOf = Lists.newArrayList(); @@ -27,13 +31,16 @@ public class CardDamageHistory { private final List NotBlockedSinceLastUpkeepOf = Lists.newArrayList(); private final List NotBeenBlockedSinceLastUpkeepOf = Lists.newArrayList(); - private final Map damagedThisCombat = Maps.newHashMap(); - private final Map damagedThisTurn = Maps.newHashMap(); - private final Map damagedThisTurnInCombat = Maps.newHashMap(); - private final Map damagedThisGame = Maps.newHashMap(); + private List> damageDoneThisTurn = Lists.newArrayList(); + + // only needed for Glen Elendra (Plane) + private final List damagedThisCombat = Lists.newArrayList(); + // only needed for The Fallen + private final FCollection damagedThisGame = new FCollection<>(); + boolean hasdealtDamagetoAny = false; public final boolean getHasdealtDamagetoAny() { - return !damagedThisGame.isEmpty(); + return hasdealtDamagetoAny; } // used to see if an attacking creature with a triggering attack ability @@ -218,87 +225,68 @@ public class CardDamageHistory { return this.creatureGotBlockedThisCombat; } - /** - *

- * Getter for the field receivedNonCombatDamageThisTurn. - *

- * - * @return a boolean. - */ - public boolean hasBeenDealtNonCombatDamageThisTurn() { - return this.receivedNonCombatDamageThisTurn; - } - - /** - *

- * Setter for the field receivedNonCombatDamageThisTurn. - *

- * - * @param b - * a boolean. - */ - public void setHasBeenDealtNonCombatDamageThisTurn(boolean b) { - this.receivedNonCombatDamageThisTurn = b; - } - - public final Map getThisCombatDamaged() { + public final List getThisCombatDamaged() { return damagedThisCombat; } - public final Map getThisTurnDamaged() { - return damagedThisTurn; - } - public final Map getThisTurnCombatDamaged() { - return damagedThisTurnInCombat; - } - public final Map getThisGameDamaged() { + public final FCollection getThisGameDamaged() { return damagedThisGame; } /** * TODO: Write javadoc for this method. * @param player */ - public void registerCombatDamage(GameEntity entity, int amount) { - int old = 0; - if (damagedThisCombat.containsKey(entity)) { - old = damagedThisCombat.get(entity); + public void registerDamage(int damage, boolean isCombat, Card sourceLKI, GameEntity target, Map lkiCache) { + damagedThisGame.add(target); + hasdealtDamagetoAny = true; + if (isCombat && target instanceof Player) { + damagedThisCombat.add((Player) target); } - damagedThisCombat.put(entity, old + amount); - old = 0; - if (damagedThisTurnInCombat.containsKey(entity)) { - old = damagedThisTurnInCombat.get(entity); - } - damagedThisTurnInCombat.put(entity, old + amount); + Pair dmg = Pair.of(damage, isCombat); + damageDoneThisTurn.add(dmg); + target.receiveDamage(dmg); + sourceLKI.getGame().addGlobalDamageHistory(this, dmg, sourceLKI.isLKI() ? sourceLKI : CardUtil.getLKICopy(sourceLKI, lkiCache), CardUtil.getLKICopy(target, lkiCache)); } - /** - * TODO: Write javadoc for this method. - */ + + public int getDamageDoneThisTurn(Boolean isCombat, boolean anyIsEnough, String validSourceCard, String validTargetEntity, Card source, Player sourceController, CardTraitBase ctb) { + int sum = 0; + for (Pair damage : damageDoneThisTurn) { + Pair sourceToTarget = sourceController.getGame().getDamageLKI(damage); + + if (isCombat != null && damage.getRight() != isCombat) { + continue; + } + if (validSourceCard != null && !sourceToTarget.getLeft().isValid(validSourceCard.split(","), sourceController, source == null ? sourceToTarget.getLeft() : source, ctb)) { + continue; + } + if (validTargetEntity != null && !sourceToTarget.getRight().isValid(validTargetEntity.split(","), sourceController, source, ctb)) { + continue; + } + sum += damage.getLeft(); + if (anyIsEnough) { + break; + } + } + return sum; + } + public void newTurn() { - damagedThisCombat.clear(); - damagedThisTurnInCombat.clear(); - damagedThisTurn.clear(); attackedThisTurn.clear(); - setHasBeenDealtNonCombatDamageThisTurn(false); + damagedThisCombat.clear(); + damageDoneThisTurn.clear(); + + // if card already LTB we can safely dereference (allows quite a few objects to be cleaned up earlier for bigger boardstates) + CardCollection toRemove = new CardCollection(); + for (GameEntity e : damagedThisGame) { + if (e instanceof Card) { + if (((Card) e).getZone().getZoneType() != ZoneType.Battlefield) { + toRemove.add((Card)e); + } + } + } + damagedThisGame.removeAll(toRemove); } public void endCombat() { damagedThisCombat.clear(); } - - /** - * TODO: Write javadoc for this method. - * @param player - */ - public void registerDamage(GameEntity entity, int amount) { - int old = 0; - if (damagedThisTurn.containsKey(entity)) { - old = damagedThisTurn.get(entity); - } - damagedThisTurn.put(entity, old + amount); - old = 0; - if (damagedThisGame.containsKey(entity)) { - old = damagedThisGame.get(entity); - } - damagedThisGame.put(entity, old + amount); - } - } 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 34947967c04..ad93f326739 100644 --- a/forge-game/src/main/java/forge/game/card/CardDamageMap.java +++ b/forge-game/src/main/java/forge/game/card/CardDamageMap.java @@ -19,6 +19,8 @@ import forge.game.Game; import forge.game.GameEntity; import forge.game.GameObjectPredicates; import forge.game.ability.AbilityKey; +import forge.game.player.Player; +import forge.game.player.PlayerCollection; import forge.game.trigger.TriggerType; public class CardDamageMap extends ForwardingTable { @@ -66,11 +68,15 @@ public class CardDamageMap extends ForwardingTable { game.getTriggerHandler().runTrigger(TriggerType.DamageDealtOnce, runParams, false); } } + // Targets -> Source for (Map.Entry> e : columnMap().entrySet()) { int sum = 0; - for (final int i : e.getValue().values()) { - sum += i; + // controller list + PlayerCollection controllers = new PlayerCollection(); + for (Entry ec : e.getValue().entrySet()) { + sum += ec.getValue(); + controllers.add(ec.getKey().getController()); } if (sum > 0) { final GameEntity ge = e.getKey(); @@ -81,6 +87,15 @@ public class CardDamageMap extends ForwardingTable { game.getTriggerHandler().runTrigger(TriggerType.DamageDoneOnce, runParams, false); } + for (Player p : controllers) { + final GameEntity ge = e.getKey(); + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.DamageTarget, ge); + runParams.put(AbilityKey.DamageSource, p); + runParams.put(AbilityKey.IsCombatDamage, isCombat); + + game.getTriggerHandler().runTrigger(TriggerType.DamageDoneOnceByController, runParams, false); + } } final Map runParams = AbilityKey.newMap(); 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 062c48892ca..257d9c0c3c4 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -29,6 +29,7 @@ import forge.game.Game; import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.cost.Cost; +import forge.game.keyword.KeywordInterface; import forge.game.player.Player; import forge.game.replacement.ReplacementHandler; import forge.game.spellability.*; @@ -203,12 +204,25 @@ public class CardFactory { } else { copySA = targetSA.copy(c, controller, false); c.setCastSA(copySA); + // need to copy keyword + if (targetSA.getKeyword() != null) { + KeywordInterface kw = targetSA.getKeyword().copy(c, false); + copySA.setKeyword(kw); + // need to add the keyword to so static doesn't make new keyword + c.addKeywordForStaticAbility(kw); + } } copySA.setCopied(true); // 707.10b copySA.setOriginalAbility(targetSA); + // Copied spell is not cast face down + if (copySA instanceof Spell) { + Spell spell = (Spell) copySA; + spell.setCastFaceDown(false); + } + if (targetSA.usesTargeting()) { // do for SubAbilities too? copySA.setTargets(targetSA.getTargets().clone()); 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 48244a37e8a..3f454c027cf 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -52,7 +52,6 @@ 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.cost.Cost; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; @@ -651,14 +650,13 @@ public class CardFactoryUtil { final boolean intrinsic, final String valid, final String zone) { Card host = card.getCard(); String desc = repAb.getDescription(); - setupETBReplacementAbility(repAb); if (!intrinsic) { repAb.setIntrinsic(false); } StringBuilder repEffsb = new StringBuilder(); repEffsb.append("Event$ Moved | ValidCard$ ").append(valid); - repEffsb.append(" | Destination$ Battlefield | Description$ ").append(desc); + repEffsb.append(" | Destination$ Battlefield | ReplacementResult$ Updated | Description$ ").append(desc); if (optional) { repEffsb.append(" | Optional$ True"); } @@ -751,13 +749,12 @@ public class CardFactoryUtil { } SpellAbility sa = AbilityFactory.getAbility(abStr, card); - setupETBReplacementAbility(sa); if (!intrinsic) { sa.setIntrinsic(false); } String repeffstr = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield " - + "| Secondary$ True | Description$ " + desc + (!extraparams.equals("") ? " | " + extraparams : ""); + + "| Secondary$ True | ReplacementResult$ Updated | Description$ " + desc + (!extraparams.equals("") ? " | " + extraparams : ""); ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, card.getCard(), intrinsic, card); @@ -866,7 +863,7 @@ public class CardFactoryUtil { inst.addTrigger(bushidoTrigger1); inst.addTrigger(bushidoTrigger2); } else if (keyword.equals("Cascade")) { - final StringBuilder trigScript = new StringBuilder("Mode$ SpellCast | ValidCard$ Card.Self" + + final StringBuilder trigScript = new StringBuilder("Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack" + " | Secondary$ True | TriggerDescription$ Cascade - CARDNAME"); final String abString = "DB$ DigUntil | Defined$ You | Amount$ 1 | Valid$ Card.nonLand+cmcLTCascadeX" + @@ -959,7 +956,7 @@ public class CardFactoryUtil { inst.addTrigger(parsedTrigger); inst.addTrigger(parsedTrigReturn); } else if (keyword.startsWith("Casualty")) { - final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | CheckSVar$ Casualty | Secondary$ True"; + final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | CheckSVar$ Casualty | TriggerZones$ Stack | Secondary$ True"; String abString = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ 1 | MayChooseTarget$ True"; String[] k = keyword.split(":"); if (k.length > 2) { @@ -972,7 +969,7 @@ public class CardFactoryUtil { inst.addTrigger(casualtyTrigger); } else if (keyword.equals("Conspire")) { - final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | CheckSVar$ Conspire | Secondary$ True | TriggerDescription$ Copy CARDNAME if its conspire cost was paid"; + final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | CheckSVar$ Conspire | TriggerZones$ Stack | Secondary$ True | TriggerDescription$ Copy CARDNAME if its conspire cost was paid"; final String abString = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ 1 | MayChooseTarget$ True"; final Trigger conspireTrigger = TriggerHandler.parseTrigger(trigScript, card, intrinsic); @@ -1037,7 +1034,7 @@ public class CardFactoryUtil { parsedTrigger.setOverridingAbility(delayTrigSA); inst.addTrigger(parsedTrigger); } else if (keyword.equals("Demonstrate")) { - final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerDescription$ Demonstrate (" + inst.getReminderText() + ")"; + final String trigScript = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack | TriggerDescription$ Demonstrate (" + inst.getReminderText() + ")"; final String youCopyStr = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | MayChooseTarget$ True | Optional$ True | RememberCopies$ True | IgnoreFreeze$ True"; final String chooseOppStr = "DB$ ChoosePlayer | Defined$ You | Choices$ Player.Opponent | ConditionDefined$ Remembered | ConditionPresent$ Spell"; final String oppCopyStr = "DB$ CopySpellAbility | Controller$ ChosenPlayer | Defined$ TriggeredSpellAbility | MayChooseTarget$ True | ConditionDefined$ Remembered | ConditionPresent$ Spell"; @@ -1226,7 +1223,7 @@ public class CardFactoryUtil { inst.addTrigger(trigger); } else if (keyword.startsWith("Gravestorm")) { - String trigStr = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerDescription$ Gravestorm (" + inst.getReminderText() + ")"; + String trigStr = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack | TriggerDescription$ Gravestorm (" + inst.getReminderText() + ")"; String copyStr = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ GravestormCount | MayChooseTarget$ True"; SpellAbility copySa = AbilityFactory.getAbility(copyStr, card); @@ -1683,7 +1680,7 @@ public class CardFactoryUtil { final String abString = "DB$ PeekAndReveal | PeekAmount$ " + num + " | RememberRevealed$ True"; - final String dbCast = "DB$ Play | Valid$ Card.IsRemembered+sameName | " + + final String dbCast = "DB$ Play | Valid$ Card.IsRemembered+sameName | ValidSA$ Spell | " + "ValidZone$ Library | WithoutManaCost$ True | Optional$ True | " + "Amount$ All"; @@ -1784,7 +1781,7 @@ public class CardFactoryUtil { inst.addTrigger(parsedTrigger); } else if (keyword.equals("Storm")) { - final String actualTrigger = "Mode$ SpellCast | ValidCard$ Card.Self | Secondary$ True" + final String actualTrigger = "Mode$ SpellCast | ValidCard$ Card.Self | TriggerZones$ Stack | Secondary$ True" + "| TriggerDescription$ Storm (" + inst.getReminderText() + ")"; String effect = "DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | Amount$ StormCount | MayChooseTarget$ True"; @@ -1896,7 +1893,7 @@ public class CardFactoryUtil { String upkeepTrig = "Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | " + "TriggerDescription$ " + sb.toString(); - String effect = "DB$ Sacrifice | SacValid$ Self | UnlessPayer$ You | UnlessCost$ " + k[1]; + String effect = "DB$ SacrificeAll | Defined$ Self | Controller$ You | UnlessPayer$ You | UnlessCost$ " + k[1]; final Trigger parsedTrigger = TriggerHandler.parseTrigger(upkeepTrig, card, intrinsic); parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card)); @@ -2055,7 +2052,7 @@ public class CardFactoryUtil { // Setup ETB replacement effects final String actualRep = "Event$ Moved | Destination$ Battlefield | ValidCard$ Card.Self |" - + " | Description$ Amplify " + amplifyMagnitude + " (" + + " | ReplacementResult$ Updated | Description$ Amplify " + amplifyMagnitude + " (" + inst.getReminderText() + ")"; final String abString = "DB$ Reveal | AnyNumber$ True | RevealValid$ " @@ -2075,7 +2072,6 @@ public class CardFactoryUtil { AbilitySub saCleanup = (AbilitySub) AbilityFactory.getAbility(dbClean, card); saPut.setSubAbility(saCleanup); - setupETBReplacementAbility(saCleanup); saReveal.setSubAbility(saPut); @@ -2168,13 +2164,12 @@ public class CardFactoryUtil { inst.addReplacement(re); } else if (keyword.equals("Daybound")) { - final String actualRep = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | DayTime$ Night | Secondary$ True | Layer$ Transform | Description$ If it is night, this permanent enters the battlefield transformed."; + final String actualRep = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | DayTime$ Night | Secondary$ True | Layer$ Transform | ReplacementResult$ Updated | Description$ If it is night, this permanent enters the battlefield transformed."; final String abTransform = "DB$ SetState | Defined$ ReplacedCard | Mode$ Transform | ETB$ True | Daybound$ True"; ReplacementEffect re = ReplacementHandler.parseReplacement(actualRep, host, intrinsic, card); SpellAbility saTransform = AbilityFactory.getAbility(abTransform, card); - setupETBReplacementAbility(saTransform); re.setOverridingAbility(saTransform); inst.addReplacement(re); @@ -2206,11 +2201,10 @@ public class CardFactoryUtil { AbilitySub cleanupSA = (AbilitySub) AbilityFactory.getAbility(cleanupStr, card); counterSA.setSubAbility(cleanupSA); - String repeffstr = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | Secondary$ True | Description$ Devour " + magnitude + " ("+ inst.getReminderText() + ")"; + String repeffstr = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | Secondary$ True | ReplacementResult$ Updated | Description$ Devour " + magnitude + " ("+ inst.getReminderText() + ")"; ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, host, intrinsic, card); - setupETBReplacementAbility(cleanupSA); re.setOverridingAbility(sacrificeSA); inst.addReplacement(re); @@ -2329,12 +2323,11 @@ public class CardFactoryUtil { + " | Origin$ Stack | Destination$ Graveyard | Fizzle$ False " + " | Description$ Rebound (" + inst.getReminderText() + ")"; - String abExile = "DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Stack | Destination$ Exile"; + String abExile = "DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Stack | Destination$ Exile | RememberChanged$ True"; String delTrig = "DB$ DelayedTrigger | Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You " + - " | OptionalDecider$ You | RememberObjects$ ReplacedCard | TriggerDescription$" + " | OptionalDecider$ You | RememberObjects$ Remembered | TriggerDescription$" + " At the beginning of your next upkeep, you may cast " + card.toString() + " without paying its mana cost."; - // TODO add check for still in exile - String abPlay = "DB$ Play | Defined$ DelayTriggerRemembered | WithoutManaCost$ True | Optional$ True"; + String abPlay = "DB$ Play | Defined$ DelayTriggerRememberedLKI | WithoutManaCost$ True | Optional$ True"; SpellAbility saExile = AbilityFactory.getAbility(abExile, card); @@ -2686,7 +2679,11 @@ public class CardFactoryUtil { final String[] k = keyword.split(":"); final Cost blitzCost = new Cost(k[1], false); - final SpellAbility newSA = card.getFirstSpellAbility().copyWithDefinedCost(blitzCost); + final SpellAbility newSA = card.getFirstSpellAbility().copyWithManaCostReplaced(host.getController(), blitzCost); + + if (k.length > 2) { + newSA.getMapParams().put("ValidAfterStack", k[2]); + } final StringBuilder desc = new StringBuilder(); desc.append("Blitz ").append(blitzCost.toSimpleString()).append(" ("); @@ -2797,7 +2794,18 @@ public class CardFactoryUtil { abilityStr.append(equipCost); abilityStr.append("| ValidTgts$ ").append(valid); abilityStr.append(" | TgtPrompt$ Select target ").append(vstr).append(" you control "); - abilityStr.append("| SorcerySpeed$ True | Equip$ True | AILogic$ Pump | IsPresent$ Equipment.Self+nonCreature "); + if (card.hasKeyword(Keyword.RECONFIGURE)) { + /* + * 301.5c An Equipment that’s also a creature can’t equip a creature unless that Equipment has reconfigure (see rule 702.151, “Reconfigure”). + * An Equipment that loses the subtype “Equipment” can’t equip a creature. An Equipment can’t equip itself. An Equipment that equips an illegal + * or nonexistent permanent becomes unattached from that permanent but remains on the battlefield. (This is a state-based action. See rule 704.) + * An Equipment can’t equip more than one creature. If a spell or ability would cause an Equipment to equip more than one creature, + * the Equipment’s controller chooses which creature it equips. + */ + abilityStr.append("| SorcerySpeed$ True | Equip$ True | AILogic$ Pump | IsPresent$ Equipment.Self "); + } else { + abilityStr.append("| SorcerySpeed$ True | Equip$ True | AILogic$ Pump | IsPresent$ Equipment.Self+nonCreature "); + } // add AttachAi for some special cards if (card.hasSVar("AttachAi")) { abilityStr.append("| ").append(card.getSVar("AttachAi")); @@ -3440,9 +3448,21 @@ public class CardFactoryUtil { st.setSVar("AffinityX", "Count$Valid " + t + ".YouCtrl"); inst.addStaticAbility(st); } else if (keyword.startsWith("Blitz")) { - String effect = "Mode$ Continuous | Affected$ Card.Self+blitzed | AddKeyword$ Haste | AddTrigger$ Dies"; - String trig = "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | " + - "Execute$ TrigDraw | TriggerDescription$ When this creature dies, draw a card."; + final String[] k = keyword.split(":"); + final String manacost = k[1]; + final Cost cost = new Cost(manacost, false); + + StringBuilder sb = new StringBuilder("Blitz"); + if (!cost.isOnlyManaCost()) { + sb.append("—"); + } else { + sb.append(" "); + } + sb.append(cost.toSimpleString()); + String effect = "Mode$ Continuous | Affected$ Card.Self+blitzed+castKeyword | AddKeyword$ Haste | AddTrigger$ Dies" + + " | Secondary$ True | Description$ " + sb.toString() + " (" + inst.getReminderText() + ")"; + String trig = "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self" + + " | Execute$ TrigDraw | Secondary$ True | TriggerDescription$ When this creature dies, draw a card."; String ab = "DB$ Draw | NumCards$ 1"; StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic); @@ -3516,7 +3536,7 @@ public class CardFactoryUtil { inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.startsWith("Dash")) { - String effect = "Mode$ Continuous | Affected$ Card.Self+dashed | AddKeyword$ Haste"; + String effect = "Mode$ Continuous | Affected$ Card.Self+dashed+castKeyword | AddKeyword$ Haste"; inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Daybound")) { String effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Daybound | Secondary$ True | Description$ This permanent can't be transformed except by its daybound ability."; @@ -3693,14 +3713,6 @@ public class CardFactoryUtil { return altCostSA; } - private static final Map emptyMap = Maps.newTreeMap(); - public static SpellAbility setupETBReplacementAbility(SpellAbility sa) { - AbilitySub as = new AbilitySub(ApiType.InternalEtbReplacement, sa.getHostCard(), null, emptyMap); - sa.appendSubAbility(as); - return as; - // ETBReplacementMove(sa.getHostCard(), null)); - } - public static void setupAdventureAbility(Card card) { if (card.getCurrentStateName() != CardStateName.Adventure) { return; 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 7e3449a65b5..cc63af4777c 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -13,6 +13,7 @@ import forge.game.*; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.card.CardPredicates.Presets; +import forge.game.combat.AttackRequirement; import forge.game.combat.AttackingBand; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; @@ -34,6 +35,7 @@ import org.apache.commons.lang3.tuple.Pair; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; public class CardProperty { @@ -109,8 +111,13 @@ public class CardProperty { if (card.getId() != Integer.parseInt(property.split("CardUID_")[1])) { return false; } - } else if (property.equals("ChosenCard")) { - if (!source.hasChosenCard(card)) { + } else if (property.startsWith("ChosenCard")) { + CardCollectionView chosen = source.getChosenCards(); + int i = chosen.indexOf(card); + if (i == -1) { + return false; + } + if (property.contains("Strict") && !chosen.get(i).equalsWithTimestamp(card)) { return false; } } else if (property.equals("nonChosenCard")) { @@ -250,21 +257,6 @@ public class CardProperty { if (!AbilityUtils.getDefinedPlayers(source, "TargetedPlayer", spellAbility).contains(controller)) { return false; } - } else if (property.equals("TargetedControllerCtrl")) { - final CardCollectionView cards = AbilityUtils.getDefinedCards(source, "Targeted", spellAbility); - final List sas = AbilityUtils.getDefinedSpellAbilities(source, "Targeted", spellAbility); - for (final Card c : cards) { - final Player p = c.getController(); - if (!controller.equals(p)) { - return false; - } - } - for (final SpellAbility s : sas) { - final Player p = s.getHostCard().getController(); - if (!controller.equals(p)) { - return false; - } - } } else if (property.startsWith("ActivePlayerCtrl")) { if (!game.getPhaseHandler().isPlayerTurn(controller)) { return false; @@ -618,35 +610,6 @@ public class CardProperty { if ((card.getCloneOrigin() == null) || !card.getCloneOrigin().equals(source)) { return false; } - } else if (property.startsWith("DamagedBy")) { - List damaged = Lists.newArrayList(); - for (Pair pair : card.getReceivedDamageFromThisTurn()) { - damaged.add(pair.getLeft()); - } - if (property.endsWith("Source") || property.equals("DamagedBy")) { - if (!damaged.contains(source)) { - return false; - } - } else { - String prop = property.substring("DamagedBy".length()); - boolean found = Iterables.any(damaged, CardPredicates.restriction(prop, sourceController, source, spellAbility)); - - if (!found) { - for (Card d : AbilityUtils.getDefinedCards(source, prop, spellAbility)) { - if (damaged.contains(d)) { - found = true; - break; - } - } - } - if (!found) { - return false; - } - } - } else if (property.startsWith("Damaged")) { - if (!card.getDamageHistory().getThisTurnDamaged().containsKey(source)) { - return false; - } } else if (property.startsWith("SharesCMCWith")) { if (property.equals("SharesCMCWith")) { if (!card.sharesCMCWith(source)) { @@ -1143,38 +1106,87 @@ public class CardProperty { && !card.getDamageHistory().hasBlockedSinceLastUpkeepOf(sourceController)) { return false; } + } else if (property.startsWith("DamagedBy")) { + String prop = property.substring("DamagedBy".length()); + CardCollection def = null; + if (prop.startsWith(" ")) { + def = AbilityUtils.getDefinedCards(source, prop.substring(1), spellAbility); + } + boolean found = false; + for (Pair p : card.getDamageReceivedThisTurn()) { + Card dmgSource = game.getDamageLKI(p).getLeft(); + if (def != null) { + for (Card c : def) { + if (dmgSource.equalsWithTimestamp(c)) { + found = true; + } + } + } + else if (prop.isEmpty() && dmgSource.equalsWithTimestamp(source)) { + found = true; + } else if (dmgSource.isValid(prop.split(","), sourceController, source, spellAbility)) { + found = true; + } + if (found) { + break; + } + } + if (!found) { + return false; + } + } else if (property.startsWith("Damaged")) { + for (Pair p : source.getDamageReceivedThisTurn()) { + boolean found = false; + if (game.getDamageLKI(p).getLeft().equalsWithTimestamp(card)) { + found = true; + break; + } + if (!found) { + return false; + } + } + } else if (property.startsWith("dealtCombatDamageThisCombat")) { + if (card.getDamageHistory().getThisCombatDamaged().isEmpty()) { + return false; + } } else if (property.startsWith("dealtDamageToYouThisTurn")) { - if (!card.getDamageHistory().getThisTurnDamaged().containsKey(sourceController)) { + if (card.getDamageHistory().getDamageDoneThisTurn(null, true, null, "You", card, sourceController, spellAbility) == 0) { return false; } } else if (property.startsWith("dealtDamageToOppThisTurn")) { if (!card.hasDealtDamageToOpponentThisTurn()) { return false; } - //dealtCombatDamageThisCombat , dealtCombatDamageThisTurn , and notDealt versions } else if (property.startsWith("dealtCombatDamage") || property.startsWith("notDealtCombatDamage")) { - final String[] v = property.split(" ")[1].split(","); - final Iterable list = property.contains("ThisCombat") ? - card.getDamageHistory().getThisCombatDamaged().keySet() : - card.getDamageHistory().getThisTurnCombatDamaged().keySet(); - boolean found = Iterables.any(list, GameObjectPredicates.restriction(v, sourceController, source, spellAbility)); + final String v = property.split(" ")[1]; + boolean found = card.getDamageHistory().getDamageDoneThisTurn(true, true, null, v, card, sourceController, spellAbility) > 0; + if (found == property.startsWith("not")) { return false; } } else if (property.startsWith("controllerWasDealtCombatDamageByThisTurn")) { - if (!source.getDamageHistory().getThisTurnCombatDamaged().containsKey(controller)) { + if (source.getDamageHistory().getDamageDoneThisTurn(true, true, null, "You", card, controller, spellAbility) == 0) { return false; } } else if (property.startsWith("controllerWasDealtDamageByThisTurn")) { - if (!source.getDamageHistory().getThisTurnDamaged().containsKey(controller)) { + if (source.getDamageHistory().getDamageDoneThisTurn(null, true, null, "You", card, controller, spellAbility) == 0) { return false; } } else if (property.startsWith("wasDealtDamageThisTurn")) { - if (card.getReceivedDamageFromPlayerThisTurn().isEmpty()) { + if (card.getAssignedDamage() == 0) { return false; } } else if (property.equals("wasDealtNonCombatDamageThisTurn")) { - if (!card.getDamageHistory().hasBeenDealtNonCombatDamageThisTurn()) { + if (card.getAssignedDamage(false, null) == 0) { + return false; + } + } else if (property.startsWith("wasDealtDamageByThisGame")) { + int idx = source.getDamageHistory().getThisGameDamaged().indexOf(card); + if (idx == -1) { + return false; + } + Card c = (Card) source.getDamageHistory().getThisGameDamaged().get(idx); + if (!c.equalsWithTimestamp(game.getCardState(card))) { return false; } } else if (property.startsWith("dealtDamageThisTurn")) { @@ -1343,6 +1355,10 @@ public class CardProperty { if (!card.isToken() && !card.isTokenCard()) { return false; } + // copied spell don't count + if (property.contains("Created") && card.getCastSA() != null) { + return false; + } } else if (property.startsWith("nonToken")) { if (card.isToken() || card.isTokenCard()) { return false; @@ -1674,6 +1690,11 @@ public class CardProperty { if (band == null || !band.getAttackers().contains(card)) { return false; } + } else if (property.equals("hadToAttackThisCombat")) { + AttackRequirement e = game.getCombat().getAttackConstraints().getRequirements().get(card); + if (e == null || !e.hasCreatureRequirement() || !e.getAttacker().equalsWithTimestamp(card)) { + return false; + } } else if (property.equals("couldAttackButNotAttacking")) { if (!game.getPhaseHandler().isPlayerTurn(controller)) return false; return CombatUtil.couldAttackButNotAttacking(combat, card); @@ -1794,6 +1815,21 @@ public class CardProperty { if (card.hasCounters()) { return false; } + } else if (property.equals("castKeyword")) { + SpellAbility castSA = card.getCastSA(); + if (castSA == null) { + return false; + } + // intrinsic keyword might be a new one when the zone changes + if (castSA.isIntrinsic()) { + // so just check if the static is intrinsic too + if (!spellAbility.isIntrinsic()) { + return false; + } + } else { + // otherwise check for keyword object + return Objects.equals(castSA.getKeyword(), spellAbility.getKeyword()); + } } else if (property.startsWith("CastSa")) { SpellAbility castSA = card.getCastSA(); if (castSA == null) { 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 abd1fe3a069..7699689d798 100644 --- a/forge-game/src/main/java/forge/game/card/CardUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardUtil.java @@ -250,9 +250,8 @@ public final class CardUtil { newCopy.setColor(in.getColor().getColor()); newCopy.setPhasedOut(in.isPhasedOut()); - newCopy.setReceivedDamageFromThisTurn(in.getReceivedDamageFromThisTurn()); - newCopy.setReceivedDamageFromPlayerThisTurn(in.getReceivedDamageFromPlayerThisTurn()); newCopy.setDamageHistory(in.getDamageHistory()); + newCopy.setDamageReceivedThisTurn(in.getDamageReceivedThisTurn()); for (Card c : in.getBlockedThisTurn()) { newCopy.addBlockedThisTurn(c); } 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 27b508b1231..4628a64e4e8 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackConstraints.java +++ b/forge-game/src/main/java/forge/game/combat/AttackConstraints.java @@ -160,7 +160,7 @@ public class AttackConstraints { } } } - myPossibleAttackers.removeAll((Iterable) attackersToRemove); + myPossibleAttackers.removeAll(attackersToRemove); for (final Card toRemove : attackersToRemove) { reqs.removeAll(findAll(reqs, toRemove)); } @@ -186,8 +186,9 @@ public class AttackConstraints { // Now try all others (plus empty attack) and count their violations final FCollection> legalAttackers = collectLegalAttackers(reqs, myMax); possible.putAll(Maps.asMap(legalAttackers.asSet(), FN_COUNT_VIOLATIONS)); - if (countViolations(Collections.emptyMap()) != -1) { - possible.put(Collections.emptyMap(), countViolations(Collections.emptyMap())); + int empty = countViolations(Collections.emptyMap()); + if (empty != -1) { + possible.put(Collections.emptyMap(), empty); } // take the case with the fewest violations @@ -392,7 +393,7 @@ public class AttackConstraints { * restriction is violated. */ public final int countViolations(final Map attackers) { - if (!globalRestrictions.isLegal(attackers, possibleAttackers)) { + if (!globalRestrictions.isLegal(attackers)) { return -1; } for (final Entry attacker : attackers.entrySet()) { diff --git a/forge-game/src/main/java/forge/game/combat/AttackRequirement.java b/forge-game/src/main/java/forge/game/combat/AttackRequirement.java index 234453a2fa7..bbd1afdbb89 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackRequirement.java +++ b/forge-game/src/main/java/forge/game/combat/AttackRequirement.java @@ -107,8 +107,17 @@ public class AttackRequirement { } } + public Card getAttacker() { + return attacker; + } + public boolean hasRequirement() { - return !defenderSpecific.isEmpty() || !causesToAttack.isEmpty() || !defenderOrPWSpecific.isEmpty(); + return defenderSpecific.countAll() > 0 || causesToAttack.countAll() > 0 || defenderOrPWSpecific.countAll() > 0; + } + + // according to Firkraag ruling Trove of Temptation applies to players, not creatures + public boolean hasCreatureRequirement() { + return defenderSpecific.countAll() > 0 || causesToAttack.countAll() > 0; } public final MapToAmount getCausesToAttack() { @@ -124,29 +133,24 @@ public class AttackRequirement { int violations = 0; // first. check to see if "must attack X or Y with at least one creature" requirements are satisfied - //List toRemoveFromDefSpecific = Lists.newArrayList(); - if (!defenderOrPWSpecific.isEmpty()) { - for (GameEntity def : defenderOrPWSpecific.keySet()) { - if (defenderSpecificAlternatives.containsKey(def)) { - boolean isAttackingDefender = false; - outer: for (Card atk : attackers.keySet()) { - // is anyone attacking this defender or any of the alternative defenders? - if (attackers.get(atk).equals(def)) { - isAttackingDefender = true; - break; - } - for (GameEntity altDef : defenderSpecificAlternatives.get(def)) { - if (attackers.get(atk).equals(altDef)) { - isAttackingDefender = true; - break outer; - } - } - } - if (!isAttackingDefender && CombatUtil.getAttackCost(attacker.getGame(), attacker, def) == null) { - violations++; // no one is attacking that defender or any of his PWs + for (GameEntity def : defenderOrPWSpecific.keySet()) { + boolean isAttackingDefender = false; + outer: for (Card atk : attackers.keySet()) { + // is anyone attacking this defender or any of the alternative defenders? + if (attackers.get(atk).equals(def)) { + isAttackingDefender = true; + break; + } + for (GameEntity altDef : defenderSpecificAlternatives.get(def)) { + if (attackers.get(atk).equals(altDef)) { + isAttackingDefender = true; + break outer; } } } + if (!isAttackingDefender) { + violations++; // no one is attacking that defender or any of his PWs + } } // now, count everything else 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 7fac51009c9..119396cab27 100644 --- a/forge-game/src/main/java/forge/game/combat/Combat.java +++ b/forge-game/src/main/java/forge/game/combat/Combat.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked; import org.apache.commons.lang3.tuple.Pair; import com.google.common.base.Function; @@ -200,6 +201,19 @@ public class Combat { return attackableEntries; } + //gets attacked player opponents (ignores planeswalkers) + public final FCollection getAttackedOpponents(Player atk) { + FCollection attackedOpps = new FCollection(); + if (atk == playerWhoAttacks) { + for (Player defender : getDefendingPlayers()) { + if (!getAttackersOf(defender).isEmpty()) { + attackedOpps.add(defender); + } + } + } + return attackedOpps; + } + public final FCollection getDefendersControlledBy(Player who) { FCollection res = new FCollection<>(); for (GameEntity ge : attackableEntries) { @@ -711,7 +725,7 @@ public class Combat { "defending player and/or any number of creatures they control.") && blocker.getController().getController().confirmAction(null, null, Localizer.getInstance().getMessage("lblAssignCombatDamageAsChoose", - CardTranslation.getTranslatedName(blocker.getName()))); + CardTranslation.getTranslatedName(blocker.getName())), null); // choose defending player if (divideCombatDamageAsChoose) { defender = blocker.getController().getController().chooseSingleEntityForEffect(attackingPlayer.getOpponents(), null, Localizer.getInstance().getMessage("lblChoosePlayer"), null); @@ -784,13 +798,14 @@ public class Combat { final SpellAbility emptySA = new SpellAbility.EmptySa(attacker); boolean assignToPlayer = false; - if (attacker.hasKeyword("CARDNAME assigns its combat damage as though it weren't blocked.")) { + if (StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker, false)) { assignToPlayer = true; } - if (!assignToPlayer && attacker.getGame().getCombat().isBlocked(attacker) && attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) { + if (!assignToPlayer && attacker.getGame().getCombat().isBlocked(attacker) + && StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) { assignToPlayer = assigningPlayer.getController().confirmAction(emptySA, PlayerActionConfirmMode.AlternativeDamageAssignment, Localizer.getInstance().getMessage("lblAssignCombatDamageWerentBlocked", - CardTranslation.getTranslatedName(attacker.getName()))); + CardTranslation.getTranslatedName(attacker.getName())), null); } boolean divideCombatDamageAsChoose = false; @@ -802,7 +817,7 @@ public class Combat { "defending player and/or any number of creatures they control.") && assigningPlayer.getController().confirmAction(emptySA, PlayerActionConfirmMode.AlternativeDamageAssignment, Localizer.getInstance().getMessage("lblAssignCombatDamageAsChoose", - CardTranslation.getTranslatedName(attacker.getName()))); + CardTranslation.getTranslatedName(attacker.getName())), null); if (defender instanceof Card && divideCombatDamageAsChoose) { defender = getDefenderPlayerByAttacker(attacker); } @@ -813,7 +828,7 @@ public class Combat { "a creature defending player controls.") && assigningPlayer.getController().confirmAction(emptySA, PlayerActionConfirmMode.AlternativeDamageAssignment, Localizer.getInstance().getMessage("lblAssignCombatDamageToCreature", - CardTranslation.getTranslatedName(attacker.getName()))); + CardTranslation.getTranslatedName(attacker.getName())), null); if (divideCombatDamageAsChoose) { if (orderedBlockers == null || orderedBlockers.isEmpty()) { orderedBlockers = getDefendersCreatures(); diff --git a/forge-game/src/main/java/forge/game/combat/CombatUtil.java b/forge-game/src/main/java/forge/game/combat/CombatUtil.java index 72431a7e0f9..9dad2411453 100644 --- a/forge-game/src/main/java/forge/game/combat/CombatUtil.java +++ b/forge-game/src/main/java/forge/game/combat/CombatUtil.java @@ -408,7 +408,7 @@ public class CombatUtil { c.getDamageHistory().setCreatureAttackedThisCombat(defender); c.getDamageHistory().clearNotAttackedSinceLastUpkeepOf(); c.getController().addCreaturesAttackedThisTurn(CardUtil.getLKICopy(c)); - if (combat.getDefenderByAttacker(c) instanceof Player) { + if (defender instanceof Player) { c.getController().addAttackedPlayersMyTurn(combat.getDefenderPlayerByAttacker(c)); } } diff --git a/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java b/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java index 46e24fbfe70..f62bb00a819 100644 --- a/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java +++ b/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java @@ -9,9 +9,7 @@ import forge.game.Game; import forge.game.GameEntity; import forge.game.GlobalRuleChange; import forge.game.card.Card; -import forge.game.card.CardCollection; import forge.game.player.Player; -import forge.game.player.PlayerCollection; import forge.game.zone.ZoneType; import forge.util.collect.FCollectionView; import forge.util.maps.LinkedHashMapToAmount; @@ -22,11 +20,9 @@ public class GlobalAttackRestrictions { private final int max; private final MapToAmount defenderMax; - private final PlayerCollection mustBeAttackedByEachOpp; - private GlobalAttackRestrictions(final int max, final MapToAmount defenderMax, PlayerCollection mustBeAttackedByEachOpp) { + private GlobalAttackRestrictions(final int max, final MapToAmount defenderMax) { this.max = max; this.defenderMax = defenderMax; - this.mustBeAttackedByEachOpp = mustBeAttackedByEachOpp; } public int getMax() { @@ -36,17 +32,17 @@ public class GlobalAttackRestrictions { return defenderMax; } - public boolean isLegal(final Map attackers, final CardCollection possibleAttackers) { - return !getViolations(attackers, possibleAttackers, true).isViolated(); + public boolean isLegal(final Map attackers) { + return !getViolations(attackers, true).isViolated(); } - public GlobalAttackRestrictionViolations getViolations(final Map attackers, final CardCollection possibleAttackers) { - return getViolations(attackers, possibleAttackers, false); + public GlobalAttackRestrictionViolations getViolations(final Map attackers) { + return getViolations(attackers, false); } - private GlobalAttackRestrictionViolations getViolations(final Map attackers, final CardCollection possibleAttackers, final boolean returnQuickly) { + private GlobalAttackRestrictionViolations getViolations(final Map attackers, final boolean returnQuickly) { final int nTooMany = max < 0 ? 0 : attackers.size() - max; if (returnQuickly && nTooMany > 0) { - return new GlobalAttackRestrictionViolations(nTooMany, MapToAmountUtil.emptyMap(), MapToAmountUtil.emptyMap()); + return new GlobalAttackRestrictionViolations(nTooMany, MapToAmountUtil.emptyMap()); } final MapToAmount defenderTooMany = new LinkedHashMapToAmount<>(defenderMax.size()); @@ -77,49 +73,18 @@ public class GlobalAttackRestrictions { } } - final MapToAmount defenderTooFew = new LinkedHashMapToAmount<>(defenderMax.size()); - for (final GameEntity mandatoryDef : mustBeAttackedByEachOpp) { - // check to ensure that this defender can even legally be attacked in the first place - boolean canAttackThisDef = false; - for (Card c : possibleAttackers) { - if (CombatUtil.canAttack(c, mandatoryDef) && null == CombatUtil.getAttackCost(c.getGame(), c, mandatoryDef)) { - canAttackThisDef = true; - break; - } - } - if (!canAttackThisDef) { - continue; - } - - boolean isAttacked = false; - for (final GameEntity defender : attackers.values()) { - if (defender.equals(mandatoryDef)) { - isAttacked = true; - break; - } else if (defender instanceof Card && ((Card)defender).getController().equals(mandatoryDef)) { - isAttacked = true; - break; - } - } - if (!isAttacked) { - defenderTooFew.add(mandatoryDef); - } - } - - return new GlobalAttackRestrictionViolations(nTooMany, defenderTooMany, defenderTooFew); + return new GlobalAttackRestrictionViolations(nTooMany, defenderTooMany); } final class GlobalAttackRestrictionViolations { private final boolean isViolated; private final int globalTooMany; private final MapToAmount defenderTooMany; - private final MapToAmount defenderTooFew; - public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount defenderTooMany, final MapToAmount defenderTooFew) { - this.isViolated = globalTooMany > 0 || !defenderTooMany.isEmpty() || !defenderTooFew.isEmpty(); + public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount defenderTooMany) { + this.isViolated = globalTooMany > 0 || !defenderTooMany.isEmpty(); this.globalTooMany = globalTooMany; this.defenderTooMany = defenderTooMany; - this.defenderTooFew = defenderTooFew; } public boolean isViolated() { return isViolated; @@ -127,9 +92,6 @@ public class GlobalAttackRestrictions { public int getGlobalTooMany() { return globalTooMany; } - public MapToAmount getDefenderTooFew() { - return defenderTooFew; - } public MapToAmount getDefenderTooMany() { return defenderTooMany; } @@ -147,7 +109,6 @@ public class GlobalAttackRestrictions { public static GlobalAttackRestrictions getGlobalRestrictions(final Player attackingPlayer, final FCollectionView possibleDefenders) { int max = -1; final MapToAmount defenderMax = new LinkedHashMapToAmount<>(possibleDefenders.size()); - final PlayerCollection mustBeAttacked = new PlayerCollection(); final Game game = attackingPlayer.getGame(); /* if (game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.onlyOneAttackerATurn)) { @@ -171,14 +132,6 @@ public class GlobalAttackRestrictions { } } - for (final Card card : game.getCardsIn(ZoneType.Battlefield)) { - if (card.hasKeyword("Each opponent must attack you or a planeswalker you control with at least one creature each combat if able.")) { - if (attackingPlayer.isOpponentOf(card.getController())) { - mustBeAttacked.add(card.getController()); - } - } - } - for (final GameEntity defender : possibleDefenders) { final int defMax = getMaxAttackTo(defender); if (defMax != -1) { @@ -190,7 +143,7 @@ public class GlobalAttackRestrictions { max = Ints.min(max, defenderMax.countAll()); } - return new GlobalAttackRestrictions(max, defenderMax, mustBeAttacked); + return new GlobalAttackRestrictions(max, defenderMax); } /** diff --git a/forge-game/src/main/java/forge/game/cost/CostTap.java b/forge-game/src/main/java/forge/game/cost/CostTap.java index 2f57d20589d..bf2d35cef5a 100644 --- a/forge-game/src/main/java/forge/game/cost/CostTap.java +++ b/forge-game/src/main/java/forge/game/cost/CostTap.java @@ -20,6 +20,7 @@ package forge.game.cost; import forge.game.card.Card; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.game.staticability.StaticAbilityActivateAbilityAsIfHaste; /** * The Class CostTap. @@ -61,7 +62,7 @@ public class CostTap extends CostPart { @Override public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) { final Card source = ability.getHostCard(); - return source.isUntapped() && (!source.isSick() || source.hasKeyword("CARDNAME may activate abilities as though it has haste.")); + return source.isUntapped() && !isAbilitySick(source); } @Override @@ -74,4 +75,10 @@ public class CostTap extends CostPart { return visitor.visit(this); } + public boolean isAbilitySick(final Card source) { + if (!source.isSick()) { + return false; + } + return !StaticAbilityActivateAbilityAsIfHaste.canActivate(source); + } } diff --git a/forge-game/src/main/java/forge/game/cost/CostUntap.java b/forge-game/src/main/java/forge/game/cost/CostUntap.java index cbaff72257a..877e6730d44 100644 --- a/forge-game/src/main/java/forge/game/cost/CostUntap.java +++ b/forge-game/src/main/java/forge/game/cost/CostUntap.java @@ -20,6 +20,7 @@ package forge.game.cost; import forge.game.card.Card; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.game.staticability.StaticAbilityActivateAbilityAsIfHaste; /** * The Class CostUntap. @@ -72,7 +73,7 @@ public class CostUntap extends CostPart { @Override public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) { final Card source = ability.getHostCard(); - return source.isTapped() && (!source.isSick() || source.hasKeyword("CARDNAME may activate abilities as though it has haste.")); + return source.isTapped() && !isAbilitySick(source); } @Override @@ -85,4 +86,10 @@ public class CostUntap extends CostPart { return visitor.visit(this); } + public boolean isAbilitySick(final Card source) { + if (!source.isSick()) { + return false; + } + return !StaticAbilityActivateAbilityAsIfHaste.canActivate(source); + } } 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 2a348100ff7..c92bcd4d4c7 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -39,6 +39,7 @@ public enum Keyword { CASUALTY("Casualty", KeywordWithAmount.class, false, "As you cast this spell, you may sacrifice a creature with power %1$d or greater. When you do, copy this spell."), 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."), + CHOOSE_A_BACKGROUND("Choose a Background", Partner.class, true, "You can have a Background as a second commander."), 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."), COMPLEATED("Compleated", SimpleKeyword.class, true, "{$0} can be paid with {$1}, {$2}, or 2 life. If life was paid, this planeswalker enters with two fewer loyalty counters."), diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordInstance.java b/forge-game/src/main/java/forge/game/keyword/KeywordInstance.java index 8bc119f91f3..7b37656fb64 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordInstance.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordInstance.java @@ -22,6 +22,7 @@ import io.sentry.Sentry; public abstract class KeywordInstance> implements KeywordInterface { private Keyword keyword; private String original; + private long staticId = 0; private List triggers = Lists.newArrayList(); private List replacements = Lists.newArrayList(); @@ -181,6 +182,7 @@ public abstract class KeywordInstance> implements K * @see forge.game.keyword.KeywordInterface#addTrigger(forge.game.trigger.Trigger) */ public final void addTrigger(final Trigger trg) { + trg.setKeyword(this); triggers.add(trg); } @@ -189,6 +191,7 @@ public abstract class KeywordInstance> implements K * @see forge.game.keyword.KeywordInterface#addReplacement(forge.game.replacement.ReplacementEffect) */ public final void addReplacement(final ReplacementEffect trg) { + trg.setKeyword(this); replacements.add(trg); } @@ -197,6 +200,7 @@ public abstract class KeywordInstance> implements K * @see forge.game.keyword.KeywordInterface#addSpellAbility(forge.game.spellability.SpellAbility) */ public final void addSpellAbility(final SpellAbility s) { + s.setKeyword(this); abilities.add(s); } @@ -205,6 +209,7 @@ public abstract class KeywordInstance> implements K * @see forge.game.keyword.KeywordInterface#addStaticAbility(forge.game.staticability.StaticAbility) */ public final void addStaticAbility(final StaticAbility st) { + st.setKeyword(this); staticAbilities.add(st); } @@ -247,22 +252,30 @@ public abstract class KeywordInstance> implements K result.abilities = Lists.newArrayList(); for (SpellAbility sa : this.abilities) { - result.abilities.add(sa.copy(host, lki)); + SpellAbility copy = sa.copy(host, lki); + copy.setKeyword(result); + result.abilities.add(copy); } result.triggers = Lists.newArrayList(); for (Trigger tr : this.triggers) { - result.triggers.add(tr.copy(host, lki)); + Trigger copy = tr.copy(host, lki); + copy.setKeyword(result); + result.triggers.add(copy); } result.replacements = Lists.newArrayList(); for (ReplacementEffect re : this.replacements) { - result.replacements.add(re.copy(host, lki)); + ReplacementEffect copy = re.copy(host, lki); + copy.setKeyword(result); + result.replacements.add(copy); } result.staticAbilities = Lists.newArrayList(); for (StaticAbility sa : this.staticAbilities) { - result.staticAbilities.add(sa.copy(host, lki)); + StaticAbility copy = sa.copy(host, lki); + copy.setKeyword(result); + result.staticAbilities.add(copy); } return result; @@ -327,4 +340,11 @@ public abstract class KeywordInstance> implements K sa.setIntrinsic(value); } } + + public long getStaticId() { + return this.staticId; + } + public void setStaticId(long v) { + this.staticId = v; + } } diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordInterface.java b/forge-game/src/main/java/forge/game/keyword/KeywordInterface.java index a46a555fd3c..43620f225ca 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordInterface.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordInterface.java @@ -18,6 +18,8 @@ public interface KeywordInterface extends Cloneable { String getReminderText(); int getAmount(); + long getStaticId(); + void setStaticId(long v); void createTraits(final Card host, final boolean intrinsic); void createTraits(final Card host, final boolean intrinsic, final boolean clear); 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 88310daba2b..8ab507529ac 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -641,6 +641,7 @@ public class PhaseHandler implements java.io.Serializable { game.getTriggerHandler().runTrigger(TriggerType.AttackersDeclared, runParams, false); } + playerTurn.clearAttackedPlayersMyCombat(); for (final Card c : combat.getAttackers()) { CombatUtil.checkDeclaredAttacker(game, c, combat, true); } @@ -1063,7 +1064,7 @@ public class PhaseHandler implements java.io.Serializable { // currently there can be only one Spell put on the Stack at once, or Land Abilities be played final CardZoneTable triggerList = new CardZoneTable(); triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost); - triggerList.triggerChangesZoneAll(game, null); + triggerList.triggerChangesZoneAll(game, sa); } } 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 3ada083dc12..3e3a90e74f0 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -98,6 +98,7 @@ import forge.game.keyword.KeywordsChange; import forge.game.mana.ManaPool; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; +import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementHandler; import forge.game.replacement.ReplacementResult; import forge.game.replacement.ReplacementType; @@ -146,8 +147,6 @@ public class Player extends GameEntity implements Comparable { private int life = 20; private int startingLife = 20; private int lifeStartedThisTurnWith = startingLife; - private final Map assignedDamage = Maps.newHashMap(); - private final Map assignedCombatDamage = Maps.newHashMap(); private int spellsCastThisTurn; private int spellsCastThisGame; private int spellsCastLastTurn; @@ -206,6 +205,8 @@ public class Player extends GameEntity implements Comparable { private List creatureAttackedThisTurn = new ArrayList<>(); private List attackedPlayersThisTurn = new ArrayList<>(); private List attackedPlayersLastTurn = new ArrayList<>(); + private List attackedPlayersThisCombat = new ArrayList<>(); + private boolean activateLoyaltyAbilityThisTurn = false; private boolean tappedLandForManaThisTurn = false; private List completedDungeons = new ArrayList<>(); @@ -216,7 +217,6 @@ public class Player extends GameEntity implements Comparable { private Map maingameCardsMap = Maps.newHashMap(); private CardCollection currentPlanes = new CardCollection(); - private Set prowl = Sets.newHashSet(); private PlayerStatistics stats = new PlayerStatistics(); private PlayerController controller; @@ -238,6 +238,7 @@ public class Player extends GameEntity implements Comparable { private Deque paidForStack = new ArrayDeque<>(); private Card monarchEffect; + private Card initiativeEffect; private Card blessingEffect; private Card keywordEffect; @@ -700,19 +701,6 @@ public class Player extends GameEntity implements Comparable { } } - int old = assignedDamage.containsKey(source) ? assignedDamage.get(source) : 0; - assignedDamage.put(source, old + amount); - source.getDamageHistory().registerDamage(this, amount); - - if (isCombat) { - old = assignedCombatDamage.containsKey(source) ? assignedCombatDamage.get(source) : 0; - assignedCombatDamage.put(source, old + amount); - for (final String type : source.getType().getCreatureTypes()) { - source.getController().addProwlType(type); - } - source.getDamageHistory().registerCombatDamage(this, amount); - } - // Run triggers final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.DamageSource, source); @@ -826,49 +814,6 @@ public class Player extends GameEntity implements Comparable { simultaneousDamage = 0; } - public final void clearAssignedDamage() { - assignedDamage.clear(); - assignedCombatDamage.clear(); - } - - public final int getAssignedDamage() { - int num = 0; - for (final Integer value : assignedDamage.values()) { - num += value; - } - return num; - } - - public final int getAssignedCombatDamage() { - int num = 0; - for (final Integer value : assignedCombatDamage.values()) { - num += value; - } - return num; - } - - public final Iterable getAssignedDamageSources() { - return assignedDamage.keySet(); - } - - public final int getAssignedDamage(final Card c) { - return assignedDamage.get(c); - } - - public final int getAssignedDamage(final String type) { - final Map valueMap = Maps.newHashMap(); - for (final Card c : assignedDamage.keySet()) { - if (c.getType().hasStringType(type)) { - valueMap.put(c, assignedDamage.get(c)); - } - } - int num = 0; - for (final Integer value : valueMap.values()) { - num += value; - } - return num; - } - /** * Get the total damage assigned to this player's opponents this turn. */ @@ -1886,6 +1831,7 @@ public class Player extends GameEntity implements Comparable { public final void addAttackedPlayersMyTurn(final Player p) { if (!attackedPlayersThisTurn.contains(p)) { + attackedPlayersThisCombat.add(p); attackedPlayersThisTurn.add(p); } } @@ -1903,6 +1849,13 @@ public class Player extends GameEntity implements Comparable { attackedPlayersLastTurn.addAll(players); } + public final List getAttackedPlayersMyCombat() { + return attackedPlayersThisTurn; + } + public final void clearAttackedPlayersMyCombat() { + attackedPlayersThisCombat.clear(); + } + public final int getVenturedThisTurn() { return venturedThisTurn; } @@ -2061,27 +2014,27 @@ public class Player extends GameEntity implements Comparable { return Iterables.any(getZone(ZoneType.Battlefield).getCardsAddedThisTurn(null), CardPredicates.Presets.LANDS); } + public boolean hasFerocious() { + return !CardLists.filterPower(getCreaturesInPlay(), 4).isEmpty(); + } + + public final boolean hasSurge() { + return !CardLists.filterControlledBy(game.getStack().getSpellsCastThisTurn(), getYourTeam()).isEmpty(); + } + public final boolean hasBloodthirst() { - for (Player p : game.getRegisteredPlayers()) { - if (p.isOpponentOf(this) && p.getAssignedDamage() > 0) { + for (Player p : getRegisteredOpponents()) { + if (p.getAssignedDamage() > 0) { return true; } } return false; } - public boolean hasFerocious() { - return !CardLists.filterPower(getCreaturesInPlay(), 4).isEmpty(); - } - public final int getBloodthirstAmount() { return Aggregates.sum(getRegisteredOpponents(), Accessors.FN_GET_ASSIGNED_DAMAGE); } - public final boolean hasSurge() { - return !CardLists.filterControlledBy(game.getStack().getSpellsCastThisTurn(), getYourTeam()).isEmpty(); - } - public final int getOpponentLostLifeThisTurn() { int lost = 0; for (Player opp : getRegisteredOpponents()) { @@ -2090,14 +2043,12 @@ public class Player extends GameEntity implements Comparable { return lost; } - public final boolean hasProwl(final String type) { - return prowl.contains(type); - } - public final void addProwlType(final String type) { - prowl.add(type); - } - public final void resetProwl() { - prowl.clear(); + public final boolean hasProwl(final Set types) { + StringBuilder sb = new StringBuilder(); + for (String type : types) { + sb.append("Card.YouCtrl+").append(type).append(","); + } + return !game.getDamageDoneThisTurn(true, true, sb.toString(), "Player", null, this, null).isEmpty(); } public final void setLibrarySearched(final int l) { @@ -2138,7 +2089,7 @@ public class Player extends GameEntity implements Comparable { } } else if (incR[0].equals("EnchantedController")) { final GameEntity enchanted = source.getEntityAttachedTo(); - if ((enchanted == null) || !(enchanted instanceof Card)) { + if (enchanted == null || !(enchanted instanceof Card)) { return false; } final Card enchantedCard = (Card) enchanted; @@ -2227,10 +2178,6 @@ public class Player extends GameEntity implements Comparable { investigatedThisTurn = 0; } - public final List getSacrificedThisTurn() { - return sacrificedThisTurn; - } - public final void addSacrificedThisTurn(final Card c, final SpellAbility source) { // Play the Sacrifice sound game.fireEvent(new GameEventCardSacrificed()); @@ -2248,6 +2195,9 @@ public class Player extends GameEntity implements Comparable { game.getTriggerHandler().runTrigger(TriggerType.Sacrificed, runParams, false); } + public final List getSacrificedThisTurn() { + return sacrificedThisTurn; + } public final void resetSacrificedThisTurn() { sacrificedThisTurn.clear(); } @@ -2440,10 +2390,8 @@ public class Player extends GameEntity implements Comparable { resetCycledThisTurn(); resetEquippedThisTurn(); resetSacrificedThisTurn(); - clearAssignedDamage(); resetVenturedThisTurn(); setRevolt(false); - resetProwl(); setSpellsCastLastTurn(getSpellsCastThisTurn()); resetSpellsCastThisTurn(); setLifeLostLastTurn(getLifeLostThisTurn()); @@ -2455,6 +2403,8 @@ public class Player extends GameEntity implements Comparable { setLibrarySearched(0); setNumManaConversion(0); + damageReceivedThisTurn.clear(); + // set last turn nr if (game.getPhaseHandler().isPlayerTurn(this)) { setAttackedPlayersMyLastTurn(attackedPlayersThisTurn); @@ -3021,8 +2971,10 @@ public class Player extends GameEntity implements Comparable { 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 | SorcerySpeed$ True | ActivationZone$ Command | SpellDescription$ Companion - Put CARDNAME in to your hand"; - eff.setSVar("MoveToHand", moveToHand); - eff.addStaticAbility(addToHandAbility); + + StaticAbility stAb = StaticAbility.create(addToHandAbility, eff, eff.getCurrentState(), true); + stAb.setSVar("MoveToHand", moveToHand); + eff.addStaticAbility(stAb); return eff; } @@ -3033,11 +2985,13 @@ public class Player extends GameEntity implements Comparable { if (game.getRules().hasAppliedVariant(GameType.Oathbreaker) && commander.getRules().canBeSignatureSpell()) { //signature spells can only reside on the stack or in the command zone - eff.setSVar("SignatureSpellMoveReplacement", "DB$ ChangeZone | Origin$ Stack | Destination$ Command | Defined$ ReplacedCard"); + String effStr = "DB$ ChangeZone | Origin$ Stack | Destination$ Command | Defined$ ReplacedCard"; - String moved = "Event$ Moved | ValidCard$ Card.EffectSource+YouOwn | Secondary$ True | ReplaceWith$ SignatureSpellMoveReplacement | Destination$ Graveyard,Exile,Hand,Library | " + + String moved = "Event$ Moved | ValidCard$ Card.EffectSource+YouOwn | Secondary$ True | Destination$ Graveyard,Exile,Hand,Library | " + "Description$ If a signature spell would be put into another zone from the stack, put it into the command zone instead."; - eff.addReplacementEffect(ReplacementHandler.parseReplacement(moved, eff, true)); + ReplacementEffect re = ReplacementHandler.parseReplacement(moved, eff, true); + re.setOverridingAbility(AbilityFactory.getAbility(effStr, eff)); + eff.addReplacementEffect(re); //signature spells can only be cast if your oathbreaker is in on the battlefield under your control String castRestriction = "Mode$ CantBeCast | ValidCard$ Card.EffectSource+YouOwn | EffectZone$ Command | IsPresent$ Card.IsCommander+YouOwn+YouCtrl | PresentZone$ Battlefield | PresentCompare$ EQ0 | " + @@ -3045,9 +2999,9 @@ public class Player extends GameEntity implements Comparable { eff.addStaticAbility(castRestriction); } else { - eff.setSVar("CommanderMoveReplacement", "DB$ ChangeZone | Origin$ Battlefield,Graveyard,Exile,Library,Hand | Destination$ Command | Defined$ ReplacedCard"); + String effStr = "DB$ ChangeZone | Origin$ Battlefield,Graveyard,Exile,Library,Hand | Destination$ Command | Defined$ ReplacedCard"; - String moved = "Event$ Moved | ValidCard$ Card.EffectSource+YouOwn | Secondary$ True | Optional$ True | OptionalDecider$ You | ReplaceWith$ CommanderMoveReplacement "; + String moved = "Event$ Moved | ValidCard$ Card.EffectSource+YouOwn | Secondary$ True | Optional$ True | OptionalDecider$ You | CommanderMoveReplacement$ True "; if (game.getRules().hasAppliedVariant(GameType.TinyLeaders)) { moved += " | Destination$ Graveyard,Exile | Description$ If a commander would be put into its owner's graveyard or exile from anywhere, that player may put it into the command zone instead."; } @@ -3057,7 +3011,9 @@ public class Player extends GameEntity implements Comparable { // rule 903.9b moved += " | Destination$ Hand,Library | Description$ If a commander would be put into its owner's hand or library from anywhere, its owner may put it into the command zone instead."; } - eff.addReplacementEffect(ReplacementHandler.parseReplacement(moved, eff, true)); + ReplacementEffect re = ReplacementHandler.parseReplacement(moved, eff, true); + re.setOverridingAbility(AbilityFactory.getAbility(effStr, eff)); + eff.addReplacementEffect(re); } String mayBePlayedAbility = "Mode$ Continuous | EffectZone$ Command | MayPlay$ True | Affected$ Card.YouOwn+EffectSource | AffectedZone$ Command"; @@ -3177,6 +3133,77 @@ public class Player extends GameEntity implements Comparable { return !StaticAbilityCantBecomeMonarch.anyCantBecomeMonarch(this); } + public void createInitiativeEffect(final String set) { + final PlayerZone com = getZone(ZoneType.Command); + if (initiativeEffect == null) { + initiativeEffect = new Card(game.nextCardId(), null, game); + initiativeEffect.setOwner(this); + initiativeEffect.setImmutable(true); + if (set != null) { + initiativeEffect.setImageKey("t:initiative_" + set.toLowerCase()); + initiativeEffect.setSetCode(set); + } else { + initiativeEffect.setImageKey("t:initiative"); + } + initiativeEffect.setName("The Initiative"); + + //Set up damage trigger + final String damageTrig = "Mode$ DamageDoneOnceByController | ValidSource$ Player | ValidTarget$ You | " + + "CombatDamage$ True | TriggerZones$ Command | TriggerDescription$ Whenever one or more " + + "creatures a player controls deal combat damage to you, that player takes the initiative."; + final String damageEff = "DB$ TakeInitiative | Defined$ TriggeredSource"; + + final Trigger damageTrigger = TriggerHandler.parseTrigger(damageTrig, initiativeEffect, true); + + damageTrigger.setOverridingAbility(AbilityFactory.getAbility(damageEff, initiativeEffect)); + initiativeEffect.addTrigger(damageTrigger); + + //Set up triggers to venture into Undercity + final String ventureTakeTrig = "Mode$ TakesInitiative | ValidPlayer$ You | TriggerZones$ Command | " + + "TriggerDescription$ Whenever you take the initiative and at the beginning of your upkeep, " + + "venture into Undercity. (If you're in a dungeon, advance to the next room. If not, enter " + + "Undercity. You can take the initiative even if you already have it.)"; + + final String ventureUpkpTrig = "Mode$ Phase | Phase$ Upkeep | TriggerZones$ Command | ValidPlayer$ You " + + "| TriggerDescription$ Whenever you take the initiative and at the beginning of your upkeep, " + + "venture into Undercity. (If you're in a dungeon, advance to the next room. If not, enter " + + "Undercity. You can take the initiative even if you already have it.) | Secondary$ True"; + + final String ventureEff = "DB$ Venture | Dungeon$ Undercity"; + + final Trigger ventureUTrigger = TriggerHandler.parseTrigger(ventureUpkpTrig, initiativeEffect, true); + ventureUTrigger.setOverridingAbility(AbilityFactory.getAbility(ventureEff, initiativeEffect)); + initiativeEffect.addTrigger(ventureUTrigger); + + final Trigger ventureTTrigger = TriggerHandler.parseTrigger(ventureTakeTrig, initiativeEffect, true); + ventureTTrigger.setOverridingAbility(AbilityFactory.getAbility(ventureEff, initiativeEffect)); + initiativeEffect.addTrigger(ventureTTrigger); + + initiativeEffect.updateStateForView(); + } + + final TriggerHandler triggerHandler = game.getTriggerHandler(); + triggerHandler.suppressMode(TriggerType.ChangesZone); + game.getAction().moveTo(ZoneType.Command, initiativeEffect, null, null); + triggerHandler.clearSuppression(TriggerType.ChangesZone); + triggerHandler.clearActiveTriggers(initiativeEffect, null); + triggerHandler.registerActiveTrigger(initiativeEffect, false); + + this.updateZoneForView(com); + } + + public boolean hasInitiative() { + return equals(game.getHasInitiative()); + } + + public void removeInitiativeEffect() { + final PlayerZone com = getZone(ZoneType.Command); + if (initiativeEffect != null) { + com.remove(initiativeEffect); + this.updateZoneForView(com); + } + } + public void updateKeywordCardAbilityText() { if (getKeywordCard() == null) return; 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 137bcc5b8a3..4a0dac41aa8 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -134,7 +134,7 @@ public abstract class PlayerController { 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 confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params); public abstract boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode bidlife, String string, int bid, Player winner); public abstract boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question); public abstract boolean confirmStaticApplication(Card hostCard, GameEntity affected, String logic, String message); 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 9f682f65cd7..9d14fe11f54 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerProperty.java +++ b/forge-game/src/main/java/forge/game/player/PlayerProperty.java @@ -1,7 +1,10 @@ package forge.game.player; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import forge.game.CardTraitBase; import forge.game.Game; @@ -78,85 +81,74 @@ public class PlayerProperty { if (player.isMonarch()) { return false; } + } else if (property.equals("hasInitiative")) { + if (!player.hasInitiative()) { + return false; + } } else if (property.equals("hasBlessing")) { if (!player.hasBlessing()) { return false; } + } else if (property.startsWith("damageDoneSingleSource")) { + String props = property.split(" ")[1]; + List sourceDmg = game.getDamageDoneThisTurn(null, false, "Card.YouCtrl", null, source, sourceController, spellAbility); + int maxDmg = sourceDmg.isEmpty() ? 0 : Collections.max(sourceDmg); + if (!Expressions.compare(maxDmg, props.substring(0, 2), AbilityUtils.calculateAmount(source, props.substring(2), spellAbility))) { + return false; + } } else if (property.startsWith("wasDealtCombatDamageThisCombatBy ")) { String v = property.split(" ")[1]; - - int count = 1; - if (v.contains("_AtLeast")) { - count = Integer.parseInt(v.substring(v.indexOf("_AtLeast") + 8)); - v = v.substring(0, v.indexOf("_AtLeast")).replace("Valid:", "Valid "); - } + boolean found = true; final List cards = AbilityUtils.getDefinedCards(source, v, spellAbility); - int found = 0; for (final Card card : cards) { - if (card.getDamageHistory().getThisCombatDamaged().containsKey(player)) { - found++; + if (card.getDamageHistory().getThisCombatDamaged().contains(player)) { + found = true; } } - if (found < count) { + if (!found) { return false; } } else if (property.startsWith("wasDealtDamageThisGameBy ")) { String v = property.split(" ")[1]; - - int count = 1; - if (v.contains("_AtLeast")) { - count = Integer.parseInt(v.substring(v.indexOf("_AtLeast") + 8)); - v = TextUtil.fastReplace(v.substring(0, v.indexOf("_AtLeast")), "Valid:", "Valid "); - } + boolean found = true; final List cards = AbilityUtils.getDefinedCards(source, v, spellAbility); - int found = 0; for (final Card card : cards) { - if (card.getDamageHistory().getThisGameDamaged().containsKey(player)) { - found++; + if (card.getDamageHistory().getThisGameDamaged().contains(player)) { + found = true; } } - if (found < count) { + if (!found) { return false; } - } else if (property.startsWith("wasDealtDamageThisTurnBy ")) { - String v = property.split(" ")[1]; - int count = 1; - - if (v.contains("_AtLeast")) { - count = Integer.parseInt(v.substring(v.indexOf("_AtLeast") + 8)); - v = TextUtil.fastReplace(v.substring(0, v.indexOf("_AtLeast")), "Valid:", "Valid "); + } else if (property.startsWith("wasDealt")) { + boolean found = false; + String validCard = null; + Boolean combat = null; + if (property.contains("CombatDamage")) { + combat = true; } + if (property.contains("ThisTurnBySource")) { + found = source.getDamageHistory().getDamageDoneThisTurn(combat, validCard == null, validCard, "You", source, player, spellAbility) > 0; + } else { + String comp = "GE"; + int right = 1; + int numValid = 0; - final List cards = AbilityUtils.getDefinedCards(source, v, spellAbility); - int found = 0; - for (final Card card : cards) { - if (card.getDamageHistory().getThisTurnDamaged().containsKey(player)) { - found++; + if (property.contains("ThisTurnBy")) { + String[] props = property.split(" "); + validCard = props[1]; + if (props.length > 2) { + comp = props[2].substring(0, 2); + right = AbilityUtils.calculateAmount(source, props[2].substring(2), spellAbility); + } } - } - if (found < count) { - return false; - } - } else if (property.startsWith("wasDealtCombatDamageThisTurnBy ")) { - String v = property.split(" ")[1]; - int count = 1; - if (v.contains("_AtLeast")) { - count = Integer.parseInt(v.substring(v.indexOf("_AtLeast") + 8)); - v = TextUtil.fastReplace(v.substring(0, v.indexOf("_AtLeast")), "Valid:", "Valid "); + numValid = game.getDamageDoneThisTurn(combat, validCard == null, validCard, "You", source, player, spellAbility).size(); + found = Expressions.compare(numValid, comp, right); } - - final List cards = AbilityUtils.getDefinedCards(source, v, spellAbility); - - int found = 0; - for (final Card card : cards) { - if (card.getDamageHistory().getThisTurnCombatDamaged().containsKey(player)) { - found++; - } - } - if (found < count) { + if (!found) { return false; } } else if (property.equals("attackedBySourceThisCombat")) { @@ -167,18 +159,10 @@ public class PlayerProperty { if (!source.getDamageHistory().hasAttackedThisTurn(player)) { return false; } - } else if (property.equals("wasDealtDamageThisTurn")) { - if (player.getAssignedDamage() == 0) { - return false; - } - } else if (property.equals("Defending")) { + } else if (property.equals("Defending")) { if (!game.getCombat().getAttackersAndDefenders().values().contains(player)) { return false; } - } else if (property.equals("wasDealtCombatDamageThisTurn")) { - if (player.getAssignedCombatDamage() == 0) { - return false; - } } else if (property.equals("LostLifeThisTurn")) { if (player.getLifeLostThisTurn() <= 0) { return false; @@ -239,10 +223,16 @@ public class PlayerProperty { return false; } } else if (property.startsWith("controls")) { - final String[] type = property.substring(8).split("_"); - final CardCollectionView list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type[0], sourceController, source, spellAbility); - String comparator = type.length > 1 ? type[1] : "GE"; - int y = type.length > 1 ? AbilityUtils.calculateAmount(source, comparator.substring(2), spellAbility) : 1; + // this allows escaping _ with \ in case of complex restrictions (used on Turf War) + List type = new ArrayList<>(); + Pattern regex = Pattern.compile("(?:\\\\.|[^_\\\\]++)+"); + Matcher regexMatcher = regex.matcher(property.substring(8)); + while (regexMatcher.find()) { + type.add(regexMatcher.group()); + } + final CardCollectionView list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.get(0).replace("\\_", "_"), sourceController, source, spellAbility); + String comparator = type.size() > 1 ? type.get(1) : "GE"; + int y = type.size() > 1 ? AbilityUtils.calculateAmount(source, comparator.substring(2), spellAbility) : 1; if (!Expressions.compare(list.size(), comparator, y)) { return false; } @@ -395,6 +385,13 @@ public class PlayerProperty { if (!player.getAttackedPlayersMyLastTurn().contains(sourceController)) { return false; } + } else if (property.equals("BeenAttackedThisCombat")) { + for (Player p : game.getRegisteredPlayers()) { + if (p.getAttackedPlayersMyCombat().contains(sourceController)) { + return true; + } + } + return false; } else if (property.equals("VenturedThisTurn")) { if (player.getVenturedThisTurn() < 1) { return false; diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceAddCounter.java b/forge-game/src/main/java/forge/game/replacement/ReplaceAddCounter.java index 82747d2c755..d1ba490ca1f 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplaceAddCounter.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceAddCounter.java @@ -8,9 +8,11 @@ import com.google.common.base.Optional; import forge.game.ability.AbilityKey; import forge.game.card.Card; +import forge.game.card.CardCollectionView; import forge.game.card.CounterType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; /** * TODO: Write javadoc for this type. @@ -58,6 +60,23 @@ public class ReplaceAddCounter extends ReplacementEffect { return false; } + if (runParams.containsKey(AbilityKey.ETB) && (Boolean)runParams.get(AbilityKey.ETB)) { + // if Card does affect something other than itself + if (!hasParam("ValidCard") || !getParam("ValidCard").equals("Card.Self")) { + // and it self is entering, skip + if (getHostCard().equals(runParams.get(AbilityKey.Affected))) { + return false; + } + // and it wasn't already on the field, skip + if (getActiveZone().contains(ZoneType.Battlefield) && runParams.containsKey(AbilityKey.LastStateBattlefield)) { + CardCollectionView lastBattlefield = (CardCollectionView) runParams.get(AbilityKey.LastStateBattlefield); + if (!lastBattlefield.contains(getHostCard())) { + return false; + } + } + } + } + return true; } diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceMoved.java b/forge-game/src/main/java/forge/game/replacement/ReplaceMoved.java index ce068800cca..e370ca9635e 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplaceMoved.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceMoved.java @@ -96,7 +96,7 @@ public class ReplaceMoved extends ReplacementEffect { @Override public void setReplacingObjects(Map runParams, SpellAbility sa) { sa.setReplacingObject(AbilityKey.Card, runParams.get(AbilityKey.Affected)); - sa.setReplacingObjectsFrom(runParams, AbilityKey.CardLKI, AbilityKey.Cause, AbilityKey.LastStateBattlefield, AbilityKey.LastStateGraveyard); + sa.setReplacingObjectsFrom(runParams, AbilityKey.NewCard, AbilityKey.CardLKI, AbilityKey.Cause, AbilityKey.LastStateBattlefield, AbilityKey.LastStateGraveyard); } } 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 1e72eb846ca..7ce788fd936 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java @@ -211,6 +211,7 @@ public class ReplacementHandler { re.setHostCard(affectedCard); } runParams.put(AbilityKey.Affected, affectedCard); + runParams.put(AbilityKey.NewCard, CardUtil.getLKICopy(affectedLKI)); } game.getAction().checkStaticAbilities(false); } @@ -418,8 +419,8 @@ public class ReplacementHandler { } } - if ("Replaced".equals(replacementEffect.getParam("ReplacementResult"))) { - return ReplacementResult.Replaced; // Event is replaced without SA. + if (replacementEffect.hasParam("ReplacementResult")) { + return ReplacementResult.valueOf(replacementEffect.getParam("ReplacementResult")); // Event is replaced without SA. } // if the spellability is a replace effect then its some new logic 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 4a618be3e2c..c1f5dc6e9d7 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java @@ -42,6 +42,8 @@ import forge.game.replacement.ReplacementType; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerType; +import forge.game.zone.ZoneType; +import forge.game.zone.Zone; import forge.util.TextUtil; /** @@ -87,11 +89,11 @@ public class AbilityManaPart implements java.io.Serializable { public AbilityManaPart(final Card sourceCard, final Map params) { this.sourceCard = sourceCard; - origProduced = params.containsKey("Produced") ? params.get("Produced") : "1"; - this.manaRestrictions = params.containsKey("RestrictValid") ? params.get("RestrictValid") : ""; + origProduced = params.getOrDefault("Produced", "1"); + this.manaRestrictions = params.getOrDefault("RestrictValid", ""); this.cannotCounterSpell = params.get("AddsNoCounter"); this.addsKeywords = params.get("AddsKeywords"); - this.addsKeywordsType = params.get("AddsKeywordsType"); + this.addsKeywordsType = params.get("AddsKeywordsValid"); this.addsKeywordsUntil = params.get("AddsKeywordsUntil"); this.addsCounters = params.get("AddsCounters"); this.triggersWhenSpent = params.get("TriggersWhenSpent"); @@ -126,20 +128,22 @@ public class AbilityManaPart implements java.io.Serializable { SpellAbility root = sa == null ? null : sa.getRootAbility(); - final Map repParams = AbilityKey.mapFromAffected(source); - repParams.put(AbilityKey.Mana, afterReplace); - repParams.put(AbilityKey.Player, player); - repParams.put(AbilityKey.AbilityMana, root); - repParams.put(AbilityKey.Activator, root == null ? null : root.getActivatingPlayer()); + if (root != null && root.isManaAbility()) { + final Map repParams = AbilityKey.mapFromAffected(source); + repParams.put(AbilityKey.Mana, afterReplace); + repParams.put(AbilityKey.Player, player); + repParams.put(AbilityKey.AbilityMana, root); + repParams.put(AbilityKey.Activator, root.getActivatingPlayer()); - switch (player.getGame().getReplacementHandler().run(ReplacementType.ProduceMana, repParams)) { - case NotReplaced: - break; - case Updated: - afterReplace = (String) repParams.get(AbilityKey.Mana); - break; - default: - return; + switch (player.getGame().getReplacementHandler().run(ReplacementType.ProduceMana, repParams)) { + case NotReplaced: + break; + case Updated: + afterReplace = (String) repParams.get(AbilityKey.Mana); + break; + default: + return; + } } //clear lastProduced @@ -172,7 +176,7 @@ public class AbilityManaPart implements java.io.Serializable { runParams.put(AbilityKey.Activator, root == null ? null : root.getActivatingPlayer()); player.getGame().getTriggerHandler().runTrigger(TriggerType.TapsForMana, runParams, false); - if (source.isLand() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) { + if (source.isLand() && root.isManaAbility() && root.getPayCosts() != null && root.getPayCosts().hasTapCost()) { player.setTappedLandForManaThisTurn(true); } } // end produceMana(String) @@ -337,10 +341,26 @@ public class AbilityManaPart implements java.io.Serializable { continue; } + //handled in meetsManaShardRestrictions if (restriction.equals("CantPayGenericCosts")) { return true; } + // "can't" zone restriction – shouldn't be mixed with other restrictions + if (restriction.startsWith("CantCastSpellFrom")) { + if (!sa.isSpell()) { // + return true; + } + final ZoneType badZone = ZoneType.smartValueOf(restriction.substring(17)); + final Card host = sa.getHostCard(); + final Zone castFrom = host.getCastFrom(); + //ComputerUtilMana looks at this to see if AI can cast things, so need a fallback zone + final ZoneType zone = castFrom == null ? host.getZone().getZoneType() : castFrom.getZoneType(); + if (!badZone.equals(zone)) { + return true; + } + } + // the payment is for a resolving SA, currently no other restrictions would allow that if (getSourceCard().getGame().getStack().getInstanceFromSpellAbility(sa.getRootAbility()) != null) { return false; 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 7940ae92aa0..ab9d7fc1426 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -638,7 +638,8 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit if (isSpell() && host != null) { if (mana.addsKeywords(this) && mana.addsKeywordsType() - && host.getType().hasStringType(mana.getManaAbility().getAddsKeywordsType())) { + && this.isValid(mana.getManaAbility().getAddsKeywordsType(), + mana.getSourceCard().getController(), mana.getSourceCard(), null)) { final long timestamp = host.getGame().getNextTimestamp(); final List kws = Arrays.asList(mana.getAddedKeywords().split(" & ")); host.addChangedCardKeywords(kws, null, false, timestamp, 0); @@ -1096,12 +1097,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit // always set this to false, it is only set in CopyEffect clone.mayChooseNewTargets = false; - // Copied spell is not cast face down - if (clone instanceof Spell) { - Spell spell = (Spell) clone; - spell.setCastFaceDown(false); - } - clone.triggeringObjects = AbilityKey.newMap(this.triggeringObjects); clone.setPayCosts(getPayCosts().copy()); @@ -1161,14 +1156,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } newSA.setDescription(newSA.getDescription() + " (without paying its mana cost)"); - //Normal copied spell will not copy castFaceDown flag - //But copyWithNoManaCost is used to get SA without mana cost - //So it need to copy the castFaceDown flag too - if (newSA instanceof Spell) { - Spell spell = (Spell) newSA; - spell.setCastFaceDown(this.isCastFaceDown()); - } - return newSA; } @@ -2058,41 +2045,52 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit final String[] incR = restriction.split("\\.", 2); SpellAbility root = getRootAbility(); + boolean testFailed = false; + if (incR[0].startsWith("!")) { + testFailed = true; // a bit counterintuitive + incR[0] = incR[0].substring(1); // consume negation sign + } + if (incR[0].equals("Spell")) { if (!root.isSpell()) { - return false; + return testFailed; } } else if (incR[0].equals("Instant")) { if (!root.getCardState().getType().isInstant()) { - return false; + return testFailed; } } else if (incR[0].equals("Sorcery")) { if (!root.getCardState().getType().isSorcery()) { - return false; + return testFailed; } } else if (incR[0].equals("Triggered")) { if (!root.isTrigger()) { - return false; + return testFailed; } } else if (incR[0].equals("Activated")) { if (!root.isActivatedAbility()) { - return false; + return testFailed; } } else if (incR[0].equals("Static")) { if (!(root instanceof AbilityStatic)) { - return false; + return testFailed; + } + } + else if (incR[0].contains("LandAbility")) { + if (!(root instanceof LandAbility)) { + return testFailed; } } else if (incR[0].equals("SpellAbility")) { // Match anything } else { //not a spell/ability type - return false; + return testFailed; } if (incR.length > 1) { @@ -2100,11 +2098,11 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit final String[] exR = excR.split("\\+"); // Exclusive Restrictions are ... for (int j = 0; j < exR.length; j++) { if (!hasProperty(exR[j], sourceController, source, spellAbility)) { - return false; + return testFailed; } } } - return true; + return !testFailed; } // Takes arguments like Blue or withFlying @@ -2462,4 +2460,12 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } return hidden; } + + public boolean isLegalAfterStack() { + if (!matchesValidParam("ValidAfterStack", this)) { + return false; + } + // TODO add checks for Lurrus + return true; + } } 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 25a41f579c4..8f72ba69673 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java @@ -434,13 +434,7 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { } } if (sa.isProwl()) { - boolean prowlFlag = false; - for (final String type : c.getType().getCreatureTypes()) { - if (activator.hasProwl(type)) { - prowlFlag = true; - } - } - if (!prowlFlag) { + if (!activator.hasProwl(c.getType().getCreatureTypes())) { return false; } } @@ -499,6 +493,7 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { // check static abilities game.getTracker().freeze(); + cp.clearStaticChangedCardKeywords(false); CardCollection preList = new CardCollection(cp); game.getAction().checkStaticAbilities(false, Sets.newHashSet(cp), preList); 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 dde359b29ab..e0a45949555 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -211,7 +211,10 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone } String desc = CardTranslation.translateSingleDescriptionText(getParam("Description"), currentName); desc = TextUtil.fastReplace(desc, "CARDNAME", CardTranslation.getTranslatedName(currentName)); - desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(CardTranslation.getTranslatedName(currentName))); + desc = TextUtil.fastReplace(desc, "NICKNAME", + Lang.getInstance().getNickName(CardTranslation.getTranslatedName(currentName))); + desc = TextUtil.fastReplace(desc, "INSERT", + Integer.toString(AbilityUtils.calculateAmount(getHostCard(), getParam("Insert"), this))); return desc; } else { diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityActivateAbilityAsIfHaste.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityActivateAbilityAsIfHaste.java new file mode 100644 index 00000000000..c6781262d4f --- /dev/null +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityActivateAbilityAsIfHaste.java @@ -0,0 +1,54 @@ +/* + * 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.staticability; + +import forge.game.card.Card; +import forge.game.zone.ZoneType; + +/** + * The Class StaticAbility_ActivateAbilityAsIfHaste. + * - used to allow cards to activate abilities as if they had haste + */ +public class StaticAbilityActivateAbilityAsIfHaste { + + static String MODE = "ActivateAbilityAsIfHaste"; + + public static boolean canActivate(final Card card) { + for (final Card ca : card.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.getParam("Mode").equals(MODE) || stAb.isSuppressed() || !stAb.checkConditions()) { + continue; + } + + if (applyCanActivateAbility(stAb, card)) { + return true; + } + } + } + return false; + } + + public static boolean applyCanActivateAbility(final StaticAbility stAb, final Card card) { + + if (!stAb.matchesValidParam("ValidCard", card)) { + return false; + } + + return true; + } +} diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityAssignCombatDamageAsUnblocked.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAssignCombatDamageAsUnblocked.java new file mode 100644 index 00000000000..490da6e38b2 --- /dev/null +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAssignCombatDamageAsUnblocked.java @@ -0,0 +1,47 @@ +package forge.game.staticability; + +import forge.game.Game; +import forge.game.card.Card; +import forge.game.zone.ZoneType; + +public class StaticAbilityAssignCombatDamageAsUnblocked { + + static String MODE = "AssignCombatDamageAsUnblocked"; + + public static boolean assignCombatDamageAsUnblocked(final Card card) { + return assignCombatDamageAsUnblocked(card, true); + } + + public static boolean assignCombatDamageAsUnblocked(final Card card, final boolean optional) { + final Game game = card.getGame(); + for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.getParam("Mode").equals(MODE) || stAb.isSuppressed() || !stAb.checkConditions()) { + continue; + } + + if (stAb.hasParam("Optional")) { + if (!optional) { + continue; + } + } else { + if (optional) { + continue; + } + } + + if (applyAssignCombatDamageAsUnblocked(stAb, card)) { + return true; + } + } + } + return false; + } + + public static boolean applyAssignCombatDamageAsUnblocked(final StaticAbility stAb, final Card card) { + if (!stAb.matchesValidParam("ValidCard", card)) { + return false; + } + return true; + } +} diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantBeCast.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantBeCast.java index c158cac8b62..ed95f9ce562 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantBeCast.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantBeCast.java @@ -109,7 +109,7 @@ public class StaticAbilityCantBeCast { return false; } - if (stAb.hasParam("OnlySorcerySpeed") && (activator != null) && activator.canCastSorcery()) { + if (stAb.hasParam("OnlySorcerySpeed") && activator != null && activator.canCastSorcery()) { return false; } @@ -120,12 +120,12 @@ public class StaticAbilityCantBeCast { } } - if (stAb.hasParam("NonCasterTurn") && (activator != null) + if (stAb.hasParam("NonCasterTurn") && activator != null && activator.getGame().getPhaseHandler().isPlayerTurn(activator)) { return false; } - if (stAb.hasParam("cmcGT") && (activator != null)) { + if (stAb.hasParam("cmcGT") && activator != null) { if (stAb.getParam("cmcGT").equals("Turns")) { if (card.getCMC() <= activator.getTurn()) { return false; @@ -176,7 +176,7 @@ public class StaticAbilityCantBeCast { return false; } - if (stAb.hasParam("NonActivatorTurn") && (activator != null) + if (stAb.hasParam("NonActivatorTurn") && activator != null && activator.getGame().getPhaseHandler().isPlayerTurn(activator)) { return false; } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCombatDamageToughness.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCombatDamageToughness.java new file mode 100644 index 00000000000..a1a64c04879 --- /dev/null +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCombatDamageToughness.java @@ -0,0 +1,33 @@ +package forge.game.staticability; + +import forge.game.Game; +import forge.game.card.Card; +import forge.game.zone.ZoneType; + +public class StaticAbilityCombatDamageToughness { + + static String MODE = "CombatDamageToughness"; + + public static boolean combatDamageToughness(final Card card) { + final Game game = card.getGame(); + for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.getParam("Mode").equals(MODE) || stAb.isSuppressed() || !stAb.checkConditions()) { + continue; + } + + if (applyCombatDamageToughnessAbility(stAb, card)) { + return true; + } + } + } + return false; + } + + public static boolean applyCombatDamageToughnessAbility(final StaticAbility stAb, final Card card) { + if (!stAb.matchesValidParam("ValidCard", card)) { + return false; + } + return true; + } +} 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 8c112ab1ab0..b9f957a509f 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java @@ -30,6 +30,7 @@ import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import forge.GameCommand; import forge.card.CardStateName; @@ -779,6 +780,7 @@ public final class StaticAbilityContinuous { // add SVars if (addSVars != null) { + Map map = Maps.newHashMap(); for (final String sVar : addSVars) { String actualSVar = AbilityUtils.getSVar(stAb, sVar); String name = sVar; @@ -787,8 +789,9 @@ public final class StaticAbilityContinuous { name = actualSVar.split(":")[0]; actualSVar = actualSVar.split(":")[1]; } - affectedCard.setSVar(name, actualSVar); + map.put(name, actualSVar); } + affectedCard.addChangedSVars(map, se.getTimestamp(), stAb.getId()); } if (layer == StaticAbilityLayer.ABILITIES) { @@ -840,7 +843,7 @@ public final class StaticAbilityContinuous { newSA.getRestrictions().setLimitToCheck(params.get("GainsAbilitiesLimitPerTurn")); } if (params.containsKey("GainsAbilitiesActivateIgnoreColor")) { - newSA.putParam("ActivateIgnoreColor","True"); + newSA.putParam("ActivateIgnoreColor", params.get("GainsAbilitiesActivateIgnoreColor")); } newSA.setOriginalAbility(sa); // need to be set to get the Once Per turn Clause correct newSA.setGrantorStatic(stAb); 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 426ee4a0779..5b26b7adcbe 100644 --- a/forge-game/src/main/java/forge/game/trigger/Trigger.java +++ b/forge-game/src/main/java/forge/game/trigger/Trigger.java @@ -371,8 +371,31 @@ public abstract class Trigger extends TriggerReplacementBase { String condition = getParam("Condition"); if ("AltCost".equals(condition)) { final Card moved = (Card) runParams.get(AbilityKey.Card); - if( null != moved && !moved.isOptionalCostPaid(OptionalCost.AltCost)) + if (null != moved && !moved.isOptionalCostPaid(OptionalCost.AltCost)) return false; + } else if ("NoOpponentHasMoreLifeThanAttacked".equals(condition)) { + GameEntity attacked = (GameEntity) runParams.get(AbilityKey.Attacked); + if (attacked == null) { + attacked = (GameEntity) runParams.get(AbilityKey.Defender); + } + // we should not have gotten this far if planeswalker was attacked, but just to be safe + if (!(attacked instanceof Player)) { + return false; + } + final Player attackedP = (Player) attacked; + int life = attackedP.getLife(); + boolean found = false; + for (Player opp : this.getHostCard().getController().getOpponents()) { + if (opp.equals(attackedP)) { + continue; + } else if (opp.getLife() > life) { + found = true; + break; + } + } + if (found) { + return false; + } } else if ("AttackedPlayerWithMostLife".equals(condition)) { GameEntity attacked = (GameEntity) runParams.get(AbilityKey.Attacked); if (attacked == null) { diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java b/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java index b3c7c9419dc..45ea1fa3851 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java @@ -18,6 +18,7 @@ package forge.game.trigger; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import forge.game.Game; import forge.game.ability.AbilityKey; import forge.game.card.Card; @@ -76,16 +77,14 @@ public class TriggerAbilityTriggered extends Trigger { return false; } - if (hasParam("ValidCause")) { - boolean match = false; - for (Card cause : causes) { - if (matchesValidParam("ValidCause", cause)) { - match = true; - } - } - if (!match) { - return false; - } + if (!matchesValidParam("ValidCause", causes)) + { + return false; + } + + if (hasParam("TriggeredOwnAbility") && "True".equals(getParam("TriggeredOwnAbility")) && !Iterables.contains(causes, source)) + { + return false; } return true; 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 627f763531d..d1a2c9ac8e7 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java @@ -166,7 +166,7 @@ public class TriggerChangesZone extends Trigger { // 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); - final boolean expr = Expressions.compare(lkiCard.getTotalDamageReceivedThisTurn(), cond, rightSide); + final boolean expr = Expressions.compare(lkiCard.getAssignedDamage(), cond, rightSide); if (!expr) { return false; } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerDamageAll.java b/forge-game/src/main/java/forge/game/trigger/TriggerDamageAll.java index cbdb908480a..1c026b941da 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerDamageAll.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDamageAll.java @@ -17,14 +17,8 @@ public class TriggerDamageAll extends Trigger { @Override public boolean performTest(Map runParams) { if (hasParam("CombatDamage")) { - if (getParam("CombatDamage").equals("True")) { - if (!((Boolean) runParams.get(AbilityKey.IsCombatDamage))) { - return false; - } - } else if (getParam("CombatDamage").equals("False")) { - if (((Boolean) runParams.get(AbilityKey.IsCombatDamage))) { - return false; - } + if (getParam("CombatDamage").equals("True") != (Boolean) runParams.get(AbilityKey.IsCombatDamage)) { + return false; } } final CardDamageMap table = (CardDamageMap) runParams.get(AbilityKey.DamageMap); 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 bab0dc829bf..7107a67d31b 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDealtOnce.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDealtOnce.java @@ -60,14 +60,8 @@ public class TriggerDamageDealtOnce extends Trigger { @Override public final boolean performTest(final Map runParams) { if (hasParam("CombatDamage")) { - if (getParam("CombatDamage").equals("True")) { - if (!((Boolean) runParams.get(AbilityKey.IsCombatDamage))) { - return false; - } - } else if (getParam("CombatDamage").equals("False")) { - if (((Boolean) runParams.get(AbilityKey.IsCombatDamage))) { - return false; - } + if (getParam("CombatDamage").equals("True") != (Boolean) runParams.get(AbilityKey.IsCombatDamage)) { + return false; } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDone.java b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDone.java index 3df9d4ffc39..55061cbe6f0 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDone.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDone.java @@ -101,7 +101,7 @@ public class TriggerDamageDone extends Trigger { if (target instanceof Player) { final Player trigTgt = (Player) target; - if (!Expressions.compare(trigTgt.getAssignedDamage(source), operator, operand)) { + if (!Expressions.compare(trigTgt.getAssignedDamage(null, source), operator, operand)) { return false; } } else { 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 5c750c0a381..48b25101d60 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDoneOnce.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDoneOnce.java @@ -22,14 +22,8 @@ public class TriggerDamageDoneOnce extends Trigger { @Override public boolean performTest(Map runParams) { if (hasParam("CombatDamage")) { - if (getParam("CombatDamage").equals("True")) { - if (!((Boolean) runParams.get(AbilityKey.IsCombatDamage))) { - return false; - } - } else if (getParam("CombatDamage").equals("False")) { - if (((Boolean) runParams.get(AbilityKey.IsCombatDamage))) { - return false; - } + if (getParam("CombatDamage").equals("True") != (Boolean) runParams.get(AbilityKey.IsCombatDamage)) { + return false; } } @@ -59,6 +53,10 @@ public class TriggerDamageDoneOnce extends Trigger { } sa.setTriggeringObject(AbilityKey.Target, target); sa.setTriggeringObject(AbilityKey.Sources, getDamageSources(damageMap)); + for (final Map.Entry entry : damageMap.entrySet()) { + sa.setTriggeringObject(AbilityKey.AttackingPlayer, entry.getKey().getController()); + break; + } sa.setTriggeringObject(AbilityKey.DamageAmount, getDamageAmount(damageMap)); } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDoneOnceByController.java b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDoneOnceByController.java new file mode 100644 index 00000000000..8d118146261 --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDoneOnceByController.java @@ -0,0 +1,56 @@ +package forge.game.trigger; + +import java.util.Map; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.card.CardUtil; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +public class TriggerDamageDoneOnceByController extends Trigger { + + public TriggerDamageDoneOnceByController(Map params, Card host, boolean intrinsic) { + super(params, host, intrinsic); + + } + + @Override + public boolean performTest(Map runParams) { + if (hasParam("CombatDamage")) { + if (getParam("CombatDamage").equals("True") != (Boolean) runParams.get(AbilityKey.IsCombatDamage)) { + return false; + } + } + + if (!matchesValidParam("ValidTarget", runParams.get(AbilityKey.DamageTarget))) { + return false; + } + + if (!matchesValidParam("ValidSource", runParams.get(AbilityKey.DamageSource))) { + return false; + } + + return true; + } + + @Override + public void setTriggeringObjects(SpellAbility sa, Map runParams) { + Object target = runParams.get(AbilityKey.DamageTarget); + if (target instanceof Card) { + target = CardUtil.getLKICopy((Card)runParams.get(AbilityKey.DamageTarget)); + } + sa.setTriggeringObject(AbilityKey.Target, target); + sa.setTriggeringObject(AbilityKey.Source, runParams.get(AbilityKey.DamageSource)); + } + + @Override + public String getImportantStackObjects(SpellAbility sa) { + StringBuilder sb = new StringBuilder(); + if (sa.getTriggeringObject(AbilityKey.Target) != null) { + sb.append(Localizer.getInstance().getMessage("lblDamaged")).append(": ").append(sa.getTriggeringObject(AbilityKey.Target)).append(", "); + } + sb.append(Localizer.getInstance().getMessage("lblDamageSource")).append(": ").append(sa.getTriggeringObject(AbilityKey.Source)); + return sb.toString(); + } +} diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerDamagePrevented.java b/forge-game/src/main/java/forge/game/trigger/TriggerDamagePrevented.java index 3686a45805e..f58dab78055 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerDamagePrevented.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDamagePrevented.java @@ -65,14 +65,8 @@ public class TriggerDamagePrevented extends Trigger { } if (hasParam("CombatDamage")) { - if (getParam("CombatDamage").equals("True")) { - if (!((Boolean) runParams.get(AbilityKey.IsCombatDamage))) { - return false; - } - } else if (getParam("CombatDamage").equals("False")) { - if (((Boolean) runParams.get(AbilityKey.IsCombatDamage))) { - return false; - } + if (getParam("CombatDamage").equals("True") != (Boolean) runParams.get(AbilityKey.IsCombatDamage)) { + 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 4e1bed39fab..47e199d060e 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java @@ -243,7 +243,8 @@ public class TriggerHandler { TriggerType.Exploited.equals(t.getMode()) || TriggerType.Sacrificed.equals(t.getMode()) || TriggerType.Destroyed.equals(t.getMode()) || - (TriggerType.ChangesZone.equals(t.getMode()) && "Battlefield".equals(t.getParam("Origin")))) { // TODO needs additional logic in case origin=Any + ((TriggerType.ChangesZone.equals(t.getMode()) || TriggerType.ChangesZoneAll.equals(t.getMode())) + && "Battlefield".equals(t.getParam("Origin")))) { // TODO needs additional logic in case origin=Any registerOneTrigger(t); } } @@ -580,6 +581,11 @@ public class TriggerHandler { host.addRemembered(runParams.get(AbilityKey.fromString(regtrig.getParam("RememberKey")))); } + if (regtrig.hasParam("RememberAmount")) { + Integer amount = (Integer) sa.getTriggeringObject(AbilityKey.fromString(regtrig.getParam("RememberAmount"))); + host.addRemembered(amount); + } + sa.setStackDescription(sa.toString()); Player decider = null; diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerScry.java b/forge-game/src/main/java/forge/game/trigger/TriggerScry.java index 4cba3657554..5512ddbc89a 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerScry.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerScry.java @@ -65,12 +65,15 @@ public class TriggerScry extends Trigger { @Override public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player); + sa.setTriggeringObjectsFrom(runParams, AbilityKey.ScryNum); } @Override public String getImportantStackObjects(SpellAbility sa) { StringBuilder sb = new StringBuilder(); - sb.append(Localizer.getInstance().getMessage("lblScryer")).append(": ").append(sa.getTriggeringObject(AbilityKey.Player)); + sb.append(Localizer.getInstance().getMessage("lblScryer")).append(": "); + sb.append(sa.getTriggeringObject(AbilityKey.Player)).append(", "); + sb.append(sa.getTriggeringObject(AbilityKey.ScryNum)); return sb.toString(); } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerSpellAbilityCastOrCopy.java b/forge-game/src/main/java/forge/game/trigger/TriggerSpellAbilityCastOrCopy.java index f3888601b19..b253d660fe3 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerSpellAbilityCastOrCopy.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerSpellAbilityCastOrCopy.java @@ -28,6 +28,7 @@ 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.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; @@ -200,8 +201,12 @@ public class TriggerSpellAbilityCastOrCopy extends Trigger { } } - if (hasParam("NoManaSpent")) { - if (spellAbility.getTotalManaSpent() != 0) { + if (hasParam("AmountManaSpent")) { + String value = getParam("AmountManaSpent"); + int manaSpent = spellAbility.getTotalManaSpent(); + String comparator = value.substring(0, 2); + int y = AbilityUtils.calculateAmount(spellAbility.getHostCard(), value.substring(2), spellAbility); + if (!Expressions.compare(manaSpent, comparator, y)) { return false; } } @@ -295,10 +300,14 @@ public class TriggerSpellAbilityCastOrCopy extends Trigger { public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { final SpellAbility castSA = (SpellAbility) runParams.get(AbilityKey.CastSA); final SpellAbilityStackInstance si = sa.getHostCard().getGame().getStack().getInstanceFromSpellAbility(castSA); + final SpellAbility saForTargets = si != null ? si.getSpellAbility(true) : castSA; sa.setTriggeringObject(AbilityKey.Card, castSA.getHostCard()); sa.setTriggeringObject(AbilityKey.SpellAbility, castSA.copy(castSA.getHostCard(), true)); sa.setTriggeringObject(AbilityKey.StackInstance, si); - sa.setTriggeringObject(AbilityKey.SpellAbilityTargetingCards, (si != null ? si.getSpellAbility(true) : castSA).getTargets().getTargetCards()); + if (!saForTargets.getTargets().isEmpty()) { + sa.setTriggeringObject(AbilityKey.SpellAbilityTarget, saForTargets.getTargets().get(0)); + } + sa.setTriggeringObject(AbilityKey.SpellAbilityTargetingCards, saForTargets.getTargets().getTargetCards()); sa.setTriggeringObjectsFrom( runParams, AbilityKey.Player, diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerTakesInitiative.java b/forge-game/src/main/java/forge/game/trigger/TriggerTakesInitiative.java new file mode 100644 index 00000000000..b542c8dbb19 --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerTakesInitiative.java @@ -0,0 +1,38 @@ +package forge.game.trigger; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +import java.util.Map; + +public class TriggerTakesInitiative extends Trigger { + + public TriggerTakesInitiative(Map params, Card host, boolean intrinsic) { + super(params, host, intrinsic); + } + + @Override + public boolean performTest(Map runParams) { + + if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) { + return false; + } + return true; + } + + /** {@inheritDoc} */ + @Override + public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player); + } + + @Override + public String getImportantStackObjects(SpellAbility sa) { + StringBuilder sb = new StringBuilder(); + sb.append(Localizer.getInstance().getMessage("lblPlayer")).append(": "); + sb.append(sa.getTriggeringObject(AbilityKey.Player)).append(", "); + return sb.toString(); + } +} 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 94f3165ad72..ae3673c726e 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerTapsForMana.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerTapsForMana.java @@ -56,11 +56,12 @@ public class TriggerTapsForMana extends Trigger { /** {@inheritDoc} * @param runParams*/ @Override - public final boolean performTest(final Map runParams) { - //Check for tapping + public final boolean performTest(final Map runParams) { + SpellAbility manaAbility = (SpellAbility) runParams.get(AbilityKey.AbilityMana); + + // Caged Sun special case if (!hasParam("NoTapCheck")) { - final SpellAbility manaAbility = (SpellAbility) runParams.get(AbilityKey.AbilityMana); - if (manaAbility == null || !manaAbility.getRootAbility().getPayCosts().hasTapCost()) { + if (manaAbility == null || !manaAbility.isManaAbility() || !manaAbility.getPayCosts().hasTapCost()) { return false; } } 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 3dd96834576..4f7c94c9ba8 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerType.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerType.java @@ -53,6 +53,7 @@ public enum TriggerType { DamageDealtOnce(TriggerDamageDealtOnce.class), DamageDone(TriggerDamageDone.class), DamageDoneOnce(TriggerDamageDoneOnce.class), + DamageDoneOnceByController(TriggerDamageDoneOnceByController.class), DamagePrevented(TriggerDamagePrevented.class), DamagePreventedOnce(TriggerDamagePreventedOnce.class), DayTimeChanges (TriggerDayTimeChanges.class), @@ -106,6 +107,7 @@ public enum TriggerType { SpellCastOrCopy(TriggerSpellAbilityCastOrCopy.class), SpellCopy(TriggerSpellAbilityCastOrCopy.class), Surveil(TriggerSurveil.class), + TakesInitiative(TriggerTakesInitiative.class), Taps(TriggerTaps.class), TapsForMana(TriggerTapsForMana.class), TokenCreated(TriggerTokenCreated.class), diff --git a/forge-gui-android/AndroidManifest.xml b/forge-gui-android/AndroidManifest.xml index 8237552827d..5fbb2247567 100644 --- a/forge-gui-android/AndroidManifest.xml +++ b/forge-gui-android/AndroidManifest.xml @@ -2,7 +2,7 @@ + android:versionName="1.6.53" > jar -Xms1024m -Xmx1536m - 1.6.49.001 + 1.6.53.001 keystore alias storepass @@ -19,7 +19,7 @@ forge forge - 1.6.50-SNAPSHOT + 1.6.54-SNAPSHOT forge-gui-android diff --git a/forge-gui-android/proguard.cfg b/forge-gui-android/proguard.cfg index 5b4a814d1f6..99c82ed356b 100644 --- a/forge-gui-android/proguard.cfg +++ b/forge-gui-android/proguard.cfg @@ -6,8 +6,6 @@ -verbose -optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable --android - ## Uncomment the line below and set it to the location of rt.jar in JDK if the Proguard step fails to find the libraries ## and spits out a thousand-something Class Not Found errors ##-libraryjars /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/rt.jar diff --git a/forge-gui-desktop/pom.xml b/forge-gui-desktop/pom.xml index 15049b5e173..218e6410966 100644 --- a/forge-gui-desktop/pom.xml +++ b/forge-gui-desktop/pom.xml @@ -4,7 +4,7 @@ forge forge - 1.6.50-SNAPSHOT + 1.6.54-SNAPSHOT forge-gui-desktop @@ -474,10 +474,26 @@ -Dfile.encoding=UTF-8 --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.math=ALL-UNNAMED + --add-opens java.base/jdk.internal.misc=ALL-UNNAMED + --add-opens java.base/java.nio=ALL-UNNAMED + --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED + --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED + --add-opens java.desktop/java.awt.image=ALL-UNNAMED + --add-opens java.desktop/java.awt.color=ALL-UNNAMED + --add-opens java.desktop/sun.awt.image=ALL-UNNAMED + --add-opens java.desktop/javax.swing=ALL-UNNAMED + --add-opens java.desktop/javax.swing.border=ALL-UNNAMED + --add-opens java.desktop/javax.swing.event=ALL-UNNAMED + --add-opens java.desktop/sun.swing=ALL-UNNAMED + --add-opens java.desktop/java.beans=ALL-UNNAMED + --add-opens java.base/java.util.concurrent=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + -Dio.netty.tryReflectionSetAccessible=true diff --git a/forge-gui-desktop/src/main/config/forge.cmd b/forge-gui-desktop/src/main/config/forge.cmd index a707e9a64d6..8779fa25870 100644 --- a/forge-gui-desktop/src/main/config/forge.cmd +++ b/forge-gui-desktop/src/main/config/forge.cmd @@ -10,7 +10,7 @@ java -version 1>nul 2>nul || ( for /f tokens^=2^ delims^=.-_^+^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j" if %jver% GEQ 17 ( - java --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$ + java --add-opens java.desktop/java.beans=ALL-UNNAMED --add-opens java.desktop/javax.swing.border=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED --add-opens java.desktop/sun.swing=ALL-UNNAMED --add-opens java.desktop/java.awt.image=ALL-UNNAMED --add-opens java.desktop/java.awt.color=ALL-UNNAMED --add-opens java.desktop/sun.awt.image=ALL-UNNAMED --add-opens java.desktop/javax.swing=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$ popd exit /b 0 ) diff --git a/forge-gui-desktop/src/main/config/forge.sh b/forge-gui-desktop/src/main/config/forge.sh index 3edf69ceec2..7a4dc6dd3a1 100644 --- a/forge-gui-desktop/src/main/config/forge.sh +++ b/forge-gui-desktop/src/main/config/forge.sh @@ -43,7 +43,7 @@ cd $(dirname "${0}") if [[ $v -ge 17 ]] then - java --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED $SHAREDPARAMS + java --add-opens java.desktop/java.beans=ALL-UNNAMED --add-opens java.desktop/java.awt.color=ALL-UNNAMED --add-opens java.desktop/javax.swing.border=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED --add-opens java.desktop/sun.awt.image=ALL-UNNAMED --add-opens java.desktop/sun.swing=ALL-UNNAMED --add-opens java.desktop/javax.swing=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.desktop/java.awt.image=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true $SHAREDPARAMS elif [[ $v -ge 11 ]] then java --illegal-access=permit $SHAREDPARAMS diff --git a/forge-gui-desktop/src/main/java/forge/itemmanager/CardManager.java b/forge-gui-desktop/src/main/java/forge/itemmanager/CardManager.java index b381c6eda0a..f045ea88793 100644 --- a/forge-gui-desktop/src/main/java/forge/itemmanager/CardManager.java +++ b/forge-gui-desktop/src/main/java/forge/itemmanager/CardManager.java @@ -216,7 +216,7 @@ public class CardManager extends ItemManager { } menu.add(world); - if (FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.LOAD_HISTORIC_FORMATS)) { + if (FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.LOAD_ARCHIVED_FORMATS)) { JMenu blocks = GuiUtils.createMenu(localizer.getMessage("lblBlock")); final Iterable blockFormats = FModel.getFormats().getBlockList(); for (final GameFormat f : blockFormats) { diff --git a/forge-gui-desktop/src/main/java/forge/itemmanager/DeckManager.java b/forge-gui-desktop/src/main/java/forge/itemmanager/DeckManager.java index eeb80e19a3e..03e156e1c35 100644 --- a/forge-gui-desktop/src/main/java/forge/itemmanager/DeckManager.java +++ b/forge-gui-desktop/src/main/java/forge/itemmanager/DeckManager.java @@ -279,7 +279,7 @@ public final class DeckManager extends ItemManager implements IHasGam } menu.add(world); - if (FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.LOAD_HISTORIC_FORMATS)) { + if (FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.LOAD_ARCHIVED_FORMATS)) { JMenu blocks = GuiUtils.createMenu(localizer.getMessage("lblBlock")); final Iterable blockFormats = FModel.getFormats().getBlockList(); for (final GameFormat f : blockFormats) { diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/quest/DialogChooseFormats.java b/forge-gui-desktop/src/main/java/forge/screens/home/quest/DialogChooseFormats.java index 68f3454effe..b277da7d749 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/quest/DialogChooseFormats.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/quest/DialogChooseFormats.java @@ -42,7 +42,7 @@ public class DialogChooseFormats { List sanctioned = new ArrayList<>(); List casual = new ArrayList<>(); - List historic = new ArrayList<>(); + List archived = new ArrayList<>(); for (GameFormat format : FModel.getFormats().getOrderedList()){ FCheckBox box = new FCheckBox(format.getName()); @@ -51,8 +51,8 @@ public class DialogChooseFormats { case SANCTIONED: sanctioned.add(box); break; - case HISTORIC: - historic.add(box); + case ARCHIVED: + archived.add(box); break; case CUSTOM: case CASUAL: @@ -74,7 +74,7 @@ public class DialogChooseFormats { String constraints = "aligny top"; panel.add(makeCheckBoxList(sanctioned, localizer.getMessage("lblSanctioned"), true), constraints); panel.add(makeCheckBoxList(casual, localizer.getMessage("lblOther"), false), constraints); - panel.add(makeCheckBoxList(historic, localizer.getMessage("lblHistoric"), false), constraints); + panel.add(makeCheckBoxList(archived, localizer.getMessage("lblArchived"), false), constraints); final JPanel overlay = FOverlay.SINGLETON_INSTANCE.getPanel(); overlay.setLayout(new MigLayout("insets 0, gap 0, wrap, ax center, ay center")); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java index e476eb3668e..b9caefe73ef 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java @@ -167,7 +167,7 @@ public enum CSubmenuPreferences implements ICDoc { lstControls.add(Pair.of(view.getCbFilterLandsByColorId(), FPref.UI_FILTER_LANDS_BY_COLOR_IDENTITY)); lstControls.add(Pair.of(view.getCbLoadCardsLazily(), FPref.LOAD_CARD_SCRIPTS_LAZILY)); - lstControls.add(Pair.of(view.getCbLoadHistoricFormats(), FPref.LOAD_HISTORIC_FORMATS)); + lstControls.add(Pair.of(view.getCbLoadArchivedFormats(), FPref.LOAD_ARCHIVED_FORMATS)); lstControls.add(Pair.of(view.getCbSmartCardArtSelectionOpt(), FPref.UI_SMART_CARD_ART)); lstControls.add(Pair.of(view.getCbShowDraftRanking(), FPref.UI_OVERLAY_DRAFT_RANKING)); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java index 3e163d79219..0ca149a8395 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java @@ -72,7 +72,7 @@ public enum VSubmenuPreferences implements IVSubmenu { private final JCheckBox cbManaLostPrompt = new OptionsCheckBox(localizer.getMessage("cbManaLostPrompt")); private final JCheckBox cbDevMode = new OptionsCheckBox(localizer.getMessage("cbDevMode")); private final JCheckBox cbLoadCardsLazily = new OptionsCheckBox(localizer.getMessage("cbLoadCardsLazily")); - private final JCheckBox cbLoadHistoricFormats = new OptionsCheckBox(localizer.getMessage("cbLoadHistoricFormats")); + private final JCheckBox cbLoadArchivedFormats = new OptionsCheckBox(localizer.getMessage("cbLoadArchivedFormats")); private final JCheckBox cbWorkshopSyntax = new OptionsCheckBox(localizer.getMessage("cbWorkshopSyntax")); private final JCheckBox cbEnforceDeckLegality = new OptionsCheckBox(localizer.getMessage("cbEnforceDeckLegality")); private final JCheckBox cbSideboardForAI = new OptionsCheckBox(localizer.getMessage("cbSideboardForAI")); @@ -322,8 +322,8 @@ public enum VSubmenuPreferences implements IVSubmenu { pnlPrefs.add(cbLoadCardsLazily, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlLoadCardsLazily")), descriptionConstraints); - pnlPrefs.add(cbLoadHistoricFormats, titleConstraints); - pnlPrefs.add(new NoteLabel(localizer.getMessage("nlLoadHistoricFormats")), descriptionConstraints); + pnlPrefs.add(cbLoadArchivedFormats, titleConstraints); + pnlPrefs.add(new NoteLabel(localizer.getMessage("nlLoadArchivedFormats")), descriptionConstraints); pnlPrefs.add(cbEnableUnknownCards, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlEnableUnknownCards")), descriptionConstraints); @@ -741,8 +741,8 @@ public enum VSubmenuPreferences implements IVSubmenu { } /** @return {@link javax.swing.JCheckBox} */ - public JCheckBox getCbLoadHistoricFormats() { - return cbLoadHistoricFormats; + public JCheckBox getCbLoadArchivedFormats() { + return cbLoadArchivedFormats; } public JCheckBox getCbWorkshopSyntax() { diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java index bf86600cb0c..bd4dad3a7f8 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java @@ -2394,4 +2394,105 @@ public class GameSimulationTest extends SimulationTest { AssertJUnit.assertEquals(20, simGame.getPlayers().get(0).getLife()); AssertJUnit.assertEquals(2, simGame.getPlayers().get(1).getLife()); } + + @Test + public void testETBCounterMowu() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(0); + //Player p2 = game.getPlayers().get(1); + + String grumName = "Grumgully, the Generous"; + String mowuName = "Mowu, Loyal Companion"; + + addCard(grumName, p); + Card mowu = addCardToZone(mowuName, p, ZoneType.Hand); + + for (int i = 0; i < 7; ++i) { + addCard("Forest", p); + } + SpellAbility mowuSA = mowu.getFirstSpellAbility(); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + GameSimulator sim = createSimulator(game, p); + sim.simulateSpellAbility(mowuSA); + Game simGame = sim.getSimulatedGameState(); + + Card simMowu = findCardWithName(simGame, mowuName); + + AssertJUnit.assertNotNull(simMowu); + AssertJUnit.assertEquals(2, simMowu.getCounters(CounterEnumType.P1P1)); + } + + @Test + public void testETBCounterCorpsejack() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(0); + //Player p2 = game.getPlayers().get(1); + + String grumName = "Grumgully, the Generous"; + String corpsejackName = "Corpsejack Menace"; + + addCard(grumName, p); + Card corpsejack = addCardToZone(corpsejackName, p, ZoneType.Hand); + + for (int i = 0; i < 7; ++i) { + addCard("Forest", p); + addCard("Swamp", p); + } + SpellAbility corpsejackSA = corpsejack.getFirstSpellAbility(); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + GameSimulator sim = createSimulator(game, p); + sim.simulateSpellAbility(corpsejackSA); + Game simGame = sim.getSimulatedGameState(); + + Card simCorpsejack = findCardWithName(simGame, corpsejackName); + + AssertJUnit.assertNotNull(simCorpsejack); + AssertJUnit.assertEquals(1, simCorpsejack.getCounters(CounterEnumType.P1P1)); + } + + @Test + public void testETBCounterCorpsejackMentor() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(0); + //Player p2 = game.getPlayers().get(1); + + String grumName = "Grumgully, the Generous"; + String corpsejackName = "Corpsejack Menace"; + String mentorName = "Conclave Mentor"; + String everAfterName = "Ever After"; + + addCard(grumName, p); + Card corpsejack = addCardToZone(corpsejackName, p, ZoneType.Graveyard); + Card mentor = addCardToZone(mentorName, p, ZoneType.Graveyard); + + Card everAfter = addCardToZone(everAfterName, p, ZoneType.Hand); + + for (int i = 0; i < 7; ++i) { + addCard("Swamp", p); + } + SpellAbility everSA = everAfter.getFirstSpellAbility(); + everSA.getTargets().add(corpsejack); + everSA.getTargets().add(mentor); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + GameSimulator sim = createSimulator(game, p); + sim.simulateSpellAbility(everSA); + Game simGame = sim.getSimulatedGameState(); + + Card simCorpsejack = findCardWithName(simGame, corpsejackName); + Card simMentor = findCardWithName(simGame, mentorName); + + AssertJUnit.assertNotNull(simCorpsejack); + AssertJUnit.assertEquals(1, simCorpsejack.getCounters(CounterEnumType.P1P1)); + AssertJUnit.assertNotNull(simMentor); + AssertJUnit.assertEquals(1, simMentor.getCounters(CounterEnumType.P1P1)); + } } diff --git a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java index 44d641d8591..b8444645190 100644 --- a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java +++ b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java @@ -52,7 +52,7 @@ import forge.util.MyRandom; import forge.util.collect.FCollectionView; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; -import org.testng.collections.Lists; +import com.google.common.collect.Lists; import java.util.Collection; import java.util.HashMap; @@ -191,7 +191,7 @@ public class PlayerControllerForTests extends PlayerController { } @Override - public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) { + public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, Map newParam) { return true; } diff --git a/forge-gui-ios/pom.xml b/forge-gui-ios/pom.xml index c2cd6c9c430..1d4bd7cbcb3 100644 --- a/forge-gui-ios/pom.xml +++ b/forge-gui-ios/pom.xml @@ -6,13 +6,13 @@ jar -Xms128m -Xmx2048m - 1.6.49.001 + 1.6.53.001 forge forge - 1.6.50-SNAPSHOT + 1.6.54-SNAPSHOT forge-gui-ios diff --git a/forge-gui-mobile-dev/pom.xml b/forge-gui-mobile-dev/pom.xml index 16eb277d566..af58c410c50 100644 --- a/forge-gui-mobile-dev/pom.xml +++ b/forge-gui-mobile-dev/pom.xml @@ -4,7 +4,7 @@ forge forge - 1.6.50-SNAPSHOT + 1.6.54-SNAPSHOT forge-gui-mobile-dev diff --git a/forge-gui-mobile/pom.xml b/forge-gui-mobile/pom.xml index 1b8f85ac3ed..0c4ef128e59 100644 --- a/forge-gui-mobile/pom.xml +++ b/forge-gui-mobile/pom.xml @@ -4,7 +4,7 @@ forge forge - 1.6.50-SNAPSHOT + 1.6.54-SNAPSHOT forge-gui-mobile diff --git a/forge-gui-mobile/src/forge/Forge.java b/forge-gui-mobile/src/forge/Forge.java index dd33b3d57da..7267170a25b 100644 --- a/forge-gui-mobile/src/forge/Forge.java +++ b/forge-gui-mobile/src/forge/Forge.java @@ -23,6 +23,7 @@ import forge.adventure.scene.SceneType; import forge.adventure.stage.MapStage; import forge.adventure.util.Config; import forge.animation.ForgeAnimation; +import forge.assets.Assets; import forge.assets.AssetsDownloader; import forge.assets.FSkin; import forge.assets.FSkinFont; @@ -59,7 +60,7 @@ import java.util.List; import java.util.Map; public class Forge implements ApplicationListener { - public static final String CURRENT_VERSION = "1.6.49.001"; + public static final String CURRENT_VERSION = "1.6.53.001"; private static ApplicationListener app = null; static Scene currentScene = null; @@ -99,9 +100,12 @@ public class Forge implements ApplicationListener { public static boolean altZoneTabs = false; public static boolean animatedCardTapUntap = false; public static String enableUIMask = "Crop"; + public static String selector = "Default"; public static boolean enablePreloadExtendedArt = false; public static boolean isTabletDevice = false; public static String locale = "en-US"; + public Assets cardAssets; + public Assets otherAssets; public static boolean hdbuttons = false; public static boolean hdstart = false; public static boolean isPortraitMode = false; @@ -162,7 +166,8 @@ public class Forge implements ApplicationListener { // don't allow to read and process ForgeConstants.SPRITE_CARDBG_FILE = ""; } - + cardAssets = new Assets(); + otherAssets = new Assets(); graphics = new Graphics(); splashScreen = new SplashScreen(); frameRate = new FrameRate(); @@ -194,6 +199,7 @@ public class Forge implements ApplicationListener { altPlayerLayout = prefs.getPrefBoolean(FPref.UI_ALT_PLAYERINFOLAYOUT); altZoneTabs = prefs.getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS); animatedCardTapUntap = prefs.getPrefBoolean(FPref.UI_ANIMATED_CARD_TAPUNTAP); + selector = prefs.getPref(FPref.UI_SELECTOR_MODE); enableUIMask = prefs.getPref(FPref.UI_ENABLE_BORDER_MASKING); if (prefs.getPref(FPref.UI_ENABLE_BORDER_MASKING).equals("true")) //override old settings if not updated enableUIMask = "Full"; @@ -214,50 +220,44 @@ public class Forge implements ApplicationListener { ImageCache.initCache(cacheSize); //load model on background thread (using progress bar to report progress) - FThreads.invokeInBackgroundThread(new Runnable() { - @Override - public void run() { - //see if app or assets need updating - AssetsDownloader.checkForUpdates(splashScreen); - if (exited) { - return; - } //don't continue if user chose to exit or couldn't download required assets + FThreads.invokeInBackgroundThread(() -> { + //see if app or assets need updating + AssetsDownloader.checkForUpdates(splashScreen); + if (exited) { + return; + } //don't continue if user chose to exit or couldn't download required assets - safeToClose = false; - ImageKeys.setIsLibGDXPort(GuiBase.getInterface().isLibgdxPort()); - FModel.initialize(splashScreen.getProgressBar(), null); + safeToClose = false; + ImageKeys.setIsLibGDXPort(GuiBase.getInterface().isLibgdxPort()); + FModel.initialize(splashScreen.getProgressBar(), null); - splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblLoadingFonts")); - FSkinFont.preloadAll(locale); + splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblLoadingFonts")); + FSkinFont.preloadAll(locale); - splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblLoadingCardTranslations")); - CardTranslation.preloadTranslation(locale, ForgeConstants.LANG_DIR); + splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblLoadingCardTranslations")); + CardTranslation.preloadTranslation(locale, ForgeConstants.LANG_DIR); - splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblFinishingStartup")); + splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblFinishingStartup")); - //add reminder to preload - if (enablePreloadExtendedArt) { - if (autoCache) - splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblPreloadExtendedArt") + "\nDetected RAM: " + totalDeviceRAM + "MB. Cache size: " + cacheSize); - else - splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblPreloadExtendedArt")); - } else { - if (autoCache) - splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblFinishingStartup") + "\nDetected RAM: " + totalDeviceRAM + "MB. Cache size: " + cacheSize); - else - splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblFinishingStartup")); - } - - Gdx.app.postRunnable(new Runnable() { - @Override - public void run() { - afterDbLoaded(); - /* call preloadExtendedArt here, if we put it above we will * - * get error: No OpenGL context found in the current thread. */ - preloadExtendedArt(); - } - }); + //add reminder to preload + if (enablePreloadExtendedArt) { + if (autoCache) + splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblPreloadExtendedArt") + "\nDetected RAM: " + totalDeviceRAM + "MB. Cache size: " + cacheSize); + else + splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblPreloadExtendedArt")); + } else { + if (autoCache) + splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblFinishingStartup") + "\nDetected RAM: " + totalDeviceRAM + "MB. Cache size: " + cacheSize); + else + splashScreen.getProgressBar().setDescription(getLocalizer().getMessage("lblFinishingStartup")); } + + Gdx.app.postRunnable(() -> { + afterDbLoaded(); + /* call preloadExtendedArt here, if we put it above we will * + * get error: No OpenGL context found in the current thread. */ + preloadExtendedArt(); + }); }); } @@ -375,35 +375,35 @@ public class Forge implements ApplicationListener { FModel.getPreferences().save(); } - FThreads.invokeInBackgroundThread(new Runnable() { - @Override - public void run() { - FThreads.invokeInEdtLater(new Runnable() { - @Override - public void run() { - //load skin full - FSkin.loadFull(splashScreen); - FThreads.invokeInBackgroundThread(new Runnable() { - @Override - public void run() { - //load Drafts - preloadBoosterDrafts(); - FThreads.invokeInEdtLater(new Runnable() { - @Override - public void run() { - //selection - splashScreen.setShowModeSelector(true); - //start background music - SoundSystem.instance.setBackgroundMusic(MusicPlaylist.MENUS); - safeToClose = true; - } - }); - } - }); - } + FThreads.invokeInBackgroundThread(() -> FThreads.invokeInEdtLater(() -> { + //load skin full + FSkin.loadFull(splashScreen); + FThreads.invokeInBackgroundThread(() -> { + //load Drafts + preloadBoosterDrafts(); + FThreads.invokeInEdtLater(() -> { + //selection transition + setTransitionScreen(new TransitionScreen(() -> { + if (selector.equals("Classic")) { + openHomeDefault(); + clearSplashScreen(); + } else if (selector.equals("Adventure")) { + openAdventure(); + clearSplashScreen(); + } else if (splashScreen != null) { + splashScreen.setShowModeSelector(true); + } else {//default mode in case splashscreen is null at some point as seen on resume.. + openHomeDefault(); + clearSplashScreen(); + } + //start background music + SoundSystem.instance.setBackgroundMusic(MusicPlaylist.MENUS); + safeToClose = true; + clearTransitionScreen(); + }, Forge.takeScreenshot(), false, false, true, false)); }); - } - }); + }); + })); } public static void setCursor(TextureRegion textureRegion, String name) { @@ -735,30 +735,26 @@ public class Forge implements ApplicationListener { } public static void switchToClassic() { - setTransitionScreen(new TransitionScreen(new Runnable() { - @Override - public void run() { - isMobileAdventureMode = false; - GuiBase.setIsAdventureMode(false); - setCursor(FSkin.getCursor().get(0), "0"); - altZoneTabs = FModel.getPreferences().getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS); - Gdx.input.setInputProcessor(getInputProcessor()); - clearTransitionScreen(); - openHomeDefault(); - exited = false; - } - }, ScreenUtils.getFrameBufferTexture(), false, false)); + setTransitionScreen(new TransitionScreen(() -> { + ImageCache.disposeTextures(); + isMobileAdventureMode = false; + GuiBase.setIsAdventureMode(false); + setCursor(FSkin.getCursor().get(0), "0"); + altZoneTabs = FModel.getPreferences().getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS); + Gdx.input.setInputProcessor(getInputProcessor()); + clearTransitionScreen(); + openHomeDefault(); + exited = false; + }, Forge.takeScreenshot(), false, false)); } public static void switchToAdventure() { - setTransitionScreen(new TransitionScreen(new Runnable() { - @Override - public void run() { - clearCurrentScreen(); - clearTransitionScreen(); - openAdventure(); - exited = false; - } + setTransitionScreen(new TransitionScreen(() -> { + ImageCache.disposeTextures(); + clearCurrentScreen(); + clearTransitionScreen(); + openAdventure(); + exited = false; }, null, false, true)); } @@ -773,12 +769,17 @@ public class Forge implements ApplicationListener { public static void clearSplashScreen() { splashScreen = null; } + public static TextureRegion takeScreenshot() { + TextureRegion screenShot = ScreenUtils.getFrameBufferTexture(); + return screenShot; + } private static void setCurrentScreen(FScreen screen0) { String toNewScreen = screen0 != null ? screen0.toString() : ""; String previousScreen = currentScreen != null ? currentScreen.toString() : ""; - + //update gameInProgress for preload decks gameInProgress = toNewScreen.toLowerCase().contains("match") || previousScreen.toLowerCase().contains("match"); + //dispose card textures handled by assetmanager boolean dispose = toNewScreen.toLowerCase().contains("homescreen") && disposeTextures; try { endKeyInput(); //end key input before switching screens @@ -793,7 +794,7 @@ public class Forge implements ApplicationListener { BugReporter.reportException(ex); } finally { if (dispose) - ImageCache.disposeTexture(); + ImageCache.disposeTextures(); } } @@ -907,19 +908,11 @@ public class Forge implements ApplicationListener { } public static void delayedSwitchBack() { - FThreads.invokeInBackgroundThread(new Runnable() { - @Override - public void run() { - FThreads.invokeInEdtLater(new Runnable() { - @Override - public void run() { - clearTransitionScreen(); - clearCurrentScreen(); - switchToLast(); - } - }); - } - }); + FThreads.invokeInBackgroundThread(() -> FThreads.invokeInEdtLater(() -> { + clearTransitionScreen(); + clearCurrentScreen(); + switchToLast(); + })); } @Override @@ -961,6 +954,8 @@ public class Forge implements ApplicationListener { currentScreen.onClose(null); currentScreen = null; } + cardAssets.dispose(); + otherAssets.dispose(); Dscreens.clear(); graphics.dispose(); SoundSystem.instance.dispose(); @@ -969,7 +964,15 @@ public class Forge implements ApplicationListener { } catch (Exception e) { } } - + /** Retrieve assets. + * @param other if set to true returns otherAssets otherwise returns cardAssets + */ + public static Assets getAssets(boolean other) { + if (other) + return ((Forge)Gdx.app.getApplicationListener()).otherAssets; + else + return ((Forge)Gdx.app.getApplicationListener()).cardAssets; + } public static boolean switchScene(Scene newScene) { if (currentScene != null) { if (!currentScene.leave()) @@ -989,7 +992,7 @@ public class Forge implements ApplicationListener { if (!(currentScene instanceof ForgeScene)) { if (lastScreenTexture != null) lastScreenTexture.getTexture().dispose(); - lastScreenTexture = ScreenUtils.getFrameBufferTexture(); + lastScreenTexture = Forge.takeScreenshot(); } diff --git a/forge-gui-mobile/src/forge/Graphics.java b/forge-gui-mobile/src/forge/Graphics.java index 0183fc15070..48cba88cbcd 100644 --- a/forge-gui-mobile/src/forge/Graphics.java +++ b/forge-gui-mobile/src/forge/Graphics.java @@ -1132,6 +1132,7 @@ public class Graphics { P.setColor(1f,1f,1f,1f); P.drawPixel(0, 0); dummyTexture = new Texture(P); + P.dispose(); } return dummyTexture; } diff --git a/forge-gui-mobile/src/forge/adventure/character/EnemySprite.java b/forge-gui-mobile/src/forge/adventure/character/EnemySprite.java index 1a873fe741e..67e70e448c4 100644 --- a/forge-gui-mobile/src/forge/adventure/character/EnemySprite.java +++ b/forge-gui-mobile/src/forge/adventure/character/EnemySprite.java @@ -8,7 +8,9 @@ import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.utils.Array; +import com.google.common.base.Predicates; import forge.Forge; +import forge.adventure.data.DialogData; import forge.adventure.data.EffectData; import forge.adventure.data.EnemyData; import forge.adventure.data.RewardData; @@ -17,6 +19,9 @@ import forge.adventure.util.Current; import forge.adventure.util.MapDialog; import forge.adventure.util.Reward; import forge.card.CardRarity; +import forge.card.CardRulesPredicates; +import forge.deck.CardPool; +import forge.deck.Deck; import forge.item.PaperCard; import forge.util.Aggregates; import forge.util.MyRandom; @@ -35,6 +40,7 @@ public class EnemySprite extends CharacterSprite { public EffectData effect; //Battle effect for this enemy. Similar to a player's blessing. public String nameOverride = ""; //Override name of this enemy in battles. public RewardData[] rewards; //Additional rewards for this enemy. + public DialogData.ConditionData spawnCondition; //Condition to spawn. public EnemySprite(EnemyData enemyData) { this(0,enemyData); @@ -100,13 +106,16 @@ public class EnemySprite extends CharacterSprite { ret.add(new Reward(Reward.Type.Life, 1)); } else { if(data.rewards != null) { //Collect standard rewards. + Deck enemyDeck = Current.latestDeck(); // By popular demand, remove basic lands from the reward pool. + CardPool deckNoBasicLands = enemyDeck.getMain().getFilteredPool(Predicates.compose(Predicates.not(CardRulesPredicates.Presets.IS_BASIC_LAND), PaperCard.FN_GET_RULES)); for (RewardData rdata : data.rewards) { - ret.addAll(rdata.generate(false, (Current.latestDeck() != null ? Current.latestDeck().getMain().toFlatList() : null))); + ret.addAll(rdata.generate(false, deckNoBasicLands.toFlatList() )); } } if(rewards != null) { //Collect additional rewards. for(RewardData rdata:rewards) { - ret.addAll(rdata.generate(false,(Current.latestDeck()!=null? Current.latestDeck().getMain().toFlatList():null))); + //Do not filter in case we want to FORCE basic lands. If it ever becomes a problem just repeat the same as above. + ret.addAll(rdata.generate(false,(Current.latestDeck() != null ? Current.latestDeck().getMain().toFlatList() : null))); } } } diff --git a/forge-gui-mobile/src/forge/adventure/character/PlayerSprite.java b/forge-gui-mobile/src/forge/adventure/character/PlayerSprite.java index eac20f7d387..9cc22f3f82e 100644 --- a/forge-gui-mobile/src/forge/adventure/character/PlayerSprite.java +++ b/forge-gui-mobile/src/forge/adventure/character/PlayerSprite.java @@ -25,9 +25,12 @@ public class PlayerSprite extends CharacterSprite { PlayerSprite.this.updatePlayer(); } }); + playerSpeed = Config.instance().getConfigData().playerBaseSpeed; - Current.player().onBlessing( () -> playerSpeedEquipmentModifier = Current.player().equipmentSpeed() ); - Current.player().onEquipmentChanged( () -> playerSpeedEquipmentModifier=Current.player().equipmentSpeed() ); + + //Attach signals here. + Current.player().onBlessing( () -> playerSpeedEquipmentModifier = Current.player().equipmentSpeed() ); + Current.player().onEquipmentChanged( () -> playerSpeedEquipmentModifier = Current.player().equipmentSpeed() ); } private void updatePlayer() { @@ -60,13 +63,11 @@ public class PlayerSprite extends CharacterSprite { super.act(delta); direction.setLength(playerSpeed * delta * playerSpeedModifier*playerSpeedEquipmentModifier); - if(!direction.isZero()) - { + if(!direction.isZero()) { gameStage.prepareCollision(pos(),direction,boundingRect); direction.set(gameStage.adjustMovement(direction,boundingRect)); moveBy(direction.x, direction.y); } - } public boolean isMoving() { diff --git a/forge-gui-mobile/src/forge/adventure/data/ConfigData.java b/forge-gui-mobile/src/forge/adventure/data/ConfigData.java index e8bbf34636d..007cf58b0ee 100644 --- a/forge-gui-mobile/src/forge/adventure/data/ConfigData.java +++ b/forge-gui-mobile/src/forge/adventure/data/ConfigData.java @@ -20,4 +20,5 @@ public class ConfigData { public DifficultyData[] difficulties; public RewardData legalCards; public List restrictedCards; + public List restrictedEditions; } diff --git a/forge-gui-mobile/src/forge/adventure/data/EffectData.java b/forge-gui-mobile/src/forge/adventure/data/EffectData.java index e09214a0657..a41f9e7bfc1 100644 --- a/forge-gui-mobile/src/forge/adventure/data/EffectData.java +++ b/forge-gui-mobile/src/forge/adventure/data/EffectData.java @@ -17,21 +17,20 @@ public class EffectData implements Serializable { //Map only effects. public boolean colorView = false; //Allows to display enemy colors on the map. public float moveSpeed = 1.0f; //Change of movement speed. Map only. + public float goldModifier = -1.0f; //Modifier for shop discounts. + public int cardRewardBonus = 0; //Bonus "DeckCard" drops. Max 3. + //Opponent field. public EffectData opponent; //Effects to be applied to the opponent's side. - public EffectData() - { - - } + public EffectData() {} public EffectData(EffectData effect) { - name=effect.name; lifeModifier=effect.lifeModifier; changeStartCards=effect.changeStartCards; startBattleWithCard=effect.startBattleWithCard; colorView=effect.colorView; - opponent=opponent==null?null:new EffectData(effect.opponent); + opponent = (effect.opponent == null) ? null : new EffectData(effect.opponent); } public Array startBattleWithCards() { @@ -52,29 +51,31 @@ public class EffectData implements Serializable { } public String cardNames() { - String ret = ""; + StringBuilder ret = new StringBuilder(); Array array=startBattleWithCards(); for(int i =0;i 0) ? "+" : "") + this.lifeModifier + "\n"; - if(this.startBattleWithCard != null && this.startBattleWithCard.length != 0) - description+="Cards on battlefield: \n" + this.cardNames() + "\n"; - if(this.moveSpeed!=0 && this.moveSpeed != 1) - description+="Movement speed: " + ((this.lifeModifier > 0) ? "+" : "") + Math.round((this.moveSpeed-1.f)*100) + "%\n"; - if(this.changeStartCards != 0) - description+="Starting hand: " + this.changeStartCards + "\n"; + if(name != null && !name.isEmpty()) description += name + "\n"; + if(colorView) description += "Manasight.\n"; + if(lifeModifier != 0) + description += "Life: " + ((lifeModifier > 0) ? "+" : "") + lifeModifier + "\n"; + if(startBattleWithCard != null && startBattleWithCard.length != 0) + description+="Cards on battlefield: \n" + cardNames() + "\n"; + if(changeStartCards != 0) + description+="Starting hand: " + changeStartCards + "\n"; + if(moveSpeed!=0 && moveSpeed != 1) + description+="Movement speed: " + ((lifeModifier > 0) ? "+" : "") + Math.round((moveSpeed-1.f)*100) + "%\n"; + if(goldModifier > 0.0f) + description+="Shop discount: x" + (goldModifier) + "\n"; + if(cardRewardBonus > 0) + description += "Bonus enemy deck rewards: +" + (cardRewardBonus) + "\n"; if(this.opponent != null) { String oppEffect = this.opponent.getDescription(); description += "Gives Opponent:\n"; diff --git a/forge-gui-mobile/src/forge/adventure/data/EnemyData.java b/forge-gui-mobile/src/forge/adventure/data/EnemyData.java index 79f6a72f020..7256a8e8a3f 100644 --- a/forge-gui-mobile/src/forge/adventure/data/EnemyData.java +++ b/forge-gui-mobile/src/forge/adventure/data/EnemyData.java @@ -14,6 +14,7 @@ public class EnemyData { public String deck; public boolean copyPlayerDeck = false; public String ai; + public boolean boss = false; public float spawnRate; public float difficulty; public float speed; @@ -28,6 +29,7 @@ public class EnemyData { sprite = enemyData.sprite; deck = enemyData.deck; ai = enemyData.ai; + boss = enemyData.boss; spawnRate = enemyData.spawnRate; copyPlayerDeck = enemyData.copyPlayerDeck; difficulty = enemyData.difficulty; @@ -44,7 +46,7 @@ public class EnemyData { } } - public Deck generateDeck(boolean isFantasyMode) { - return CardUtil.getDeck(deck, true, isFantasyMode, colors, life > 15); + public Deck generateDeck(boolean isFantasyMode, boolean useGeneticAI) { + return CardUtil.getDeck(deck, true, isFantasyMode, colors, life > 13, life > 16 && useGeneticAI); } } diff --git a/forge-gui-mobile/src/forge/adventure/data/RewardData.java b/forge-gui-mobile/src/forge/adventure/data/RewardData.java index bdef49bfccc..a968e4c8d09 100644 --- a/forge-gui-mobile/src/forge/adventure/data/RewardData.java +++ b/forge-gui-mobile/src/forge/adventure/data/RewardData.java @@ -66,31 +66,39 @@ public class RewardData { private static Iterable allCards; private static Iterable allEnemyCards; + private void initializeAllCards(){ + RewardData legals = Config.instance().getConfigData().legalCards; + if(legals==null) allCards = FModel.getMagicDb().getCommonCards().getUniqueCardsNoAltNoOnline(); + else allCards = Iterables.filter(FModel.getMagicDb().getCommonCards().getUniqueCardsNoAltNoOnline(), new CardUtil.CardPredicate(legals, true)); + //Filter out specific cards. + allCards = Iterables.filter(allCards, new Predicate() { + @Override + public boolean apply(PaperCard input){ + if(input == null) return false; + if(Config.instance().getConfigData().restrictedEditions.contains(input.getEdition())) return false; + return !Config.instance().getConfigData().restrictedCards.contains(input.getName()); + } + }); + //Filter AI cards for enemies. + allEnemyCards=Iterables.filter(allCards, new Predicate() { + @Override + public boolean apply(PaperCard input) { + if (input == null) return false; + return !input.getRules().getAiHints().getRemAIDecks(); + } + }); + } + + public Iterable getAllCards() { + if(allCards == null) initializeAllCards(); + return allCards; + } + public Array generate(boolean isForEnemy) { return generate(isForEnemy, null); } public Array generate(boolean isForEnemy, Iterable cards) { - if(allCards==null) { - RewardData legals = Config.instance().getConfigData().legalCards; - if(legals==null) allCards = FModel.getMagicDb().getCommonCards().getUniqueCardsNoAltNoOnline(); - else allCards = Iterables.filter(FModel.getMagicDb().getCommonCards().getUniqueCardsNoAltNoOnline(), new CardUtil.CardPredicate(legals, true)); - //Filter out specific cards. - allCards = Iterables.filter(allCards, new Predicate() { - @Override - public boolean apply(PaperCard input){ - if(input == null) return false; - return !Config.instance().getConfigData().restrictedCards.contains(input.getName()); - } - }); - //Filter AI cards for enemies. - allEnemyCards=Iterables.filter(allCards, new Predicate() { - @Override - public boolean apply(PaperCard input) { - if (input == null) return false; - return !input.getRules().getAiHints().getRemAIDecks(); - } - }); - } + if(allCards==null) initializeAllCards(); Array ret=new Array<>(); if(probability == 0 || WorldSave.getCurrentSave().getWorld().getRandom().nextFloat() <= probability) { if(type==null || type.isEmpty()) @@ -114,12 +122,12 @@ public class RewardData { switch(type) { case "card": case "randomCard": - if( cardName!=null && !cardName.isEmpty() ) { - for(int i=0;i generateAllCards(Iterable dataList, boolean isForEnemy) - { - + static public List generateAllCards(Iterable dataList, boolean isForEnemy) { return rewardsToCards(generateAll(dataList, isForEnemy)); } - static public Iterable generateAll(Iterable dataList, boolean isForEnemy) - { + static public Iterable generateAll(Iterable dataList, boolean isForEnemy) { Array ret=new Array(); for (RewardData data:dataList) ret.addAll(data.generate(isForEnemy)); return ret; } - static public List rewardsToCards(Iterable dataList) - { + static public List rewardsToCards(Iterable dataList) { ArrayList ret=new ArrayList(); - for (Reward data:dataList) - { + for (Reward data:dataList) { ret.add(data.getCard()); } return ret; diff --git a/forge-gui-mobile/src/forge/adventure/data/SettingData.java b/forge-gui-mobile/src/forge/adventure/data/SettingData.java index 8c25e724b73..018d3d42abd 100644 --- a/forge-gui-mobile/src/forge/adventure/data/SettingData.java +++ b/forge-gui-mobile/src/forge/adventure/data/SettingData.java @@ -13,4 +13,5 @@ public class SettingData { public String plane; public boolean fullScreen; public String videomode; + public String lastActiveSave; } diff --git a/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java b/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java index 572b65a43a7..42e5f08ce5f 100644 --- a/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java +++ b/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java @@ -14,6 +14,7 @@ import forge.deck.DeckSection; import forge.item.InventoryItem; import forge.item.PaperCard; import forge.util.ItemPool; +import forge.util.MyRandom; import java.io.Serializable; import java.util.*; @@ -22,86 +23,120 @@ import java.util.*; * Class that represents the player (not the player sprite) */ public class AdventurePlayer implements Serializable, SaveFileContent { - private enum ColorID { COLORLESS, WHITE, BLACK, BLUE, RED, GREEN } public static final int NUMBER_OF_DECKS=10; + private enum ColorID { COLORLESS, WHITE, BLACK, BLUE, RED, GREEN } - private Deck deck; - private int avatarIndex; + // Player profile data. + private String name; private int heroRace; + private int avatarIndex; private boolean isFemale; + private ColorID colorIdentity = ColorID.COLORLESS; + + // Deck data + private Deck deck; + private final Deck[] decks = new Deck[NUMBER_OF_DECKS]; + private int selectedDeckIndex = 0; + private final DifficultyData difficultyData = new DifficultyData(); + + // Game data. private float worldPosX; private float worldPosY; - private String name; - private ColorID colorIdentity = ColorID.COLORLESS; - private int gold=0; - private int maxLife=20; - private int life=20; - private int selectedDeckIndex=0; - private Map questFlags = new HashMap<>(); + private int gold = 0; + private int maxLife= 20; + private int life = 20; private EffectData blessing; //Blessing to apply for next battle. - private PlayerStatistic statistic = new PlayerStatistic(); - private Deck[] decks=new Deck[NUMBER_OF_DECKS]; - private final DifficultyData difficultyData=new DifficultyData(); + private final PlayerStatistic statistic = new PlayerStatistic(); + private final Map questFlags = new HashMap<>(); private final Array inventoryItems=new Array<>(); private final HashMap equippedItems=new HashMap<>(); - private boolean fantasyMode = false; + + // Fantasy/Chaos mode settings. + private boolean fantasyMode = false; private boolean announceFantasy = false; - public AdventurePlayer() { - for(int i=0;i newCards=new ItemPool<>(InventoryItem.class); public void create(String n, int startingColorIdentity, Deck startingDeck, boolean male, int race, int avatar, boolean isFantasy, DifficultyData difficultyData) { - inventoryItems.clear(); - equippedItems.clear(); - deck = startingDeck; - decks[0]=deck; - fantasyMode = isFantasy; - announceFantasy = fantasyMode; - gold =difficultyData.staringMoney; - cards.clear(); + clear(); + announceFantasy = fantasyMode = isFantasy; //Set Chaos mode first. + + deck = startingDeck; + decks[0] = deck; + cards.addAllFlat(deck.getAllCardsInASinglePool().toFlatList()); - maxLife=difficultyData.startingLife; - this.difficultyData.startingLife=difficultyData.startingLife; - this.difficultyData.staringMoney=difficultyData.staringMoney; - this.difficultyData.startingDifficulty=difficultyData.startingDifficulty; - this.difficultyData.name=difficultyData.name; - this.difficultyData.spawnRank = difficultyData.spawnRank; - this.difficultyData.enemyLifeFactor=difficultyData.enemyLifeFactor; - this.difficultyData.sellFactor=difficultyData.sellFactor; - life=maxLife; + + this.difficultyData.startingLife = difficultyData.startingLife; + this.difficultyData.staringMoney = difficultyData.staringMoney; + this.difficultyData.startingDifficulty = difficultyData.startingDifficulty; + this.difficultyData.name = difficultyData.name; + this.difficultyData.spawnRank = difficultyData.spawnRank; + this.difficultyData.enemyLifeFactor = difficultyData.enemyLifeFactor; + this.difficultyData.sellFactor = difficultyData.sellFactor; + + gold = difficultyData.staringMoney; + name = n; + heroRace = race; avatarIndex = avatar; - heroRace = race; - isFemale = !male; - name = n; - //todo make coloridentity as colorset so if player is using multi color deck, get rewards based on the current selected deck color - setColorIdentity(fantasyMode ? 0 : startingColorIdentity + 1); //+1 because index 0 is colorless. - statistic.clear(); - newCards.clear(); + isFemale = !male; + + if(fantasyMode){ //Set a random ColorID in fantasy mode. + setColorIdentity(MyRandom.getRandom().nextInt(5)); // MyRandom to not interfere with the unstable RNG. + } else setColorIdentity(startingColorIdentity + 1); // +1 because index 0 is colorless. + + life = maxLife = difficultyData.startingLife; + + inventoryItems.addAll(difficultyData.startItems); onGoldChangeList.emit(); onLifeTotalChangeList.emit(); - blessing = null; - inventoryItems.addAll(difficultyData.startItems); - questFlags.clear(); } public void setSelectedDeckSlot(int slot) { - if(slot>=0&&slot=0&&slot getItems() { return inventoryItems; } + public Deck getDeck(int index) { return decks[index]; } + public CardPool getCards() { return cards; } + public String getName() { return name; } + public float getWorldPosX() { return worldPosX; } + public float getWorldPosY() { return worldPosY; } + public int getGold() { return gold; } + public int getLife() { return life; } + public int getMaxLife() { return maxLife; } + public @Null EffectData getBlessing() { return blessing; } + + public Collection getEquippedItems() { return equippedItems.values(); } + public ItemPool getNewCards() { return newCards; } + + public String getColorIdentity(){ + switch (colorIdentity){ + case BLUE : return "U"; + case GREEN : return "G"; + case RED : return "R"; + case BLACK : return "B"; + case WHITE : return "W"; + case COLORLESS: default: return "C"; //You are either Ugin or an Eldrazi. Nice. + } } - public Deck getSelectedDeck() { - return deck; - } - public Array getItems() { - return inventoryItems; - } - public Deck getDeck(int index) { - return decks[index]; - } - public CardPool getCards() { - return cards; + + public String getColorIdentityLong(){ + switch (colorIdentity){ + case BLUE : return "blue"; + case GREEN : return "green"; + case RED : return "red"; + case BLACK : return "black"; + case WHITE : return "white"; + case COLORLESS: default: return "colorless"; + } } - public String getName() { - return name; - } - - public float getWorldPosX() { - return worldPosX; - } - + //Setters public void setWorldPosX(float worldPosX) { this.worldPosX = worldPosX; } - - public float getWorldPosY() { - return worldPosY; - } - public void setWorldPosY(float worldPosY) { this.worldPosY = worldPosY; } @@ -174,30 +221,12 @@ public class AdventurePlayer implements Serializable, SaveFileContent { } } - public String getColorIdentity(){ - switch (colorIdentity){ - case BLUE : return "U"; - case GREEN : return "G"; - case RED : return "R"; - case BLACK : return "B"; - case WHITE : return "W"; - case COLORLESS: default: return "C"; //You are either Ugin or an Eldrazi. Nice. - } - } - public String getColorIdentityLong(){ - switch (colorIdentity){ - case BLUE : return "blue"; - case GREEN : return "green"; - case RED : return "red"; - case BLACK : return "black"; - case WHITE : return "white"; - case COLORLESS: default: return "colorless"; - } - } + @Override public void load(SaveFileData data) { + clear(); //Reset player data. this.statistic.load(data.readSubData("statistic")); this.difficultyData.startingLife=data.readInt("startingLife"); this.difficultyData.staringMoney=data.readInt("staringMoney"); @@ -208,44 +237,55 @@ public class AdventurePlayer implements Serializable, SaveFileContent { if(this.difficultyData.sellFactor==0) this.difficultyData.sellFactor=0.2f; - name = data.readString("name"); - worldPosX = data.readFloat("worldPosX"); - worldPosY = data.readFloat("worldPosY"); - + name = data.readString("name"); + heroRace = data.readInt("heroRace"); avatarIndex = data.readInt("avatarIndex"); - heroRace = data.readInt("heroRace"); - isFemale = data.readBool("isFemale"); - fantasyMode = data.containsKey("fantasyMode") ? data.readBool("fantasyMode") : false; - announceFantasy = data.containsKey("announceFantasy") ? data.readBool("announceFantasy") : false; - colorIdentity = ColorID.COLORLESS; - if(data.containsKey("colorIdentity")) - setColorIdentity(data.readString("colorIdentity")); - gold = data.readInt("gold"); - life = data.readInt("life"); - maxLife = data.readInt("maxLife"); - blessing = null; + isFemale = data.readBool("isFemale"); + if(data.containsKey("colorIdentity")) setColorIdentity(data.readString("colorIdentity")); + else colorIdentity = ColorID.COLORLESS; + + gold = data.readInt("gold"); + maxLife = data.readInt("maxLife"); + life = data.readInt("life"); + worldPosX = data.readFloat("worldPosX"); + worldPosY = data.readFloat("worldPosY"); + if(data.containsKey("blessing")) blessing = (EffectData)data.readObject("blessing"); - inventoryItems.clear(); - equippedItems.clear(); + if(data.containsKey("inventory")) { String[] inv=(String[])data.readObject("inventory"); - inventoryItems.addAll(inv); + //Prevent items with wrong names from getting through. Hell breaks loose if it causes null pointers. + //This only needs to be done on load. + for(String i : inv){ + if(ItemData.getItem(i) != null) inventoryItems.add(i); + else { + System.err.printf("Cannot find item name %s\n", i); + //Allow official© permission for the player to get a refund. We will allow it this time. + //TODoooo: Divine retribution if the player refunds too much. Use the orbital laser cannon. + System.out.println("Developers have blessed you! You are allowed to cheat the cost of the item back!"); + } + } } if(data.containsKey("equippedSlots") && data.containsKey("equippedItems")) { String[] slots=(String[])data.readObject("equippedSlots"); String[] items=(String[])data.readObject("equippedItems"); assert(slots.length==items.length); + //Like above, prevent items with wrong names. If it triggered in inventory it'll trigger here as well. for(int i=0;i slots=new ArrayList<>(); ArrayList items=new ArrayList<>(); @@ -318,6 +356,8 @@ public class AdventurePlayer implements Serializable, SaveFileContent { data.storeObject("equippedSlots",slots.toArray(new String[0])); data.storeObject("equippedItems",items.toArray(new String[0])); + data.storeObject("blessing", blessing); + //Save quest flags. ArrayList questFlagsKey = new ArrayList<>(); ArrayList questFlagsValue = new ArrayList<>(); @@ -331,9 +371,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent { data.storeObject("deckCards",deck.getMain().toCardList("\n").split("\n")); if(deck.get(DeckSection.Sideboard)!=null) data.storeObject("sideBoardCards",deck.get(DeckSection.Sideboard).toCardList("\n").split("\n")); - for(int i=0;i 0) result += data.effect.cardRewardBonus; + } + if(blessing != null) { + if(blessing.cardRewardBonus > 0) result += blessing.cardRewardBonus; + } + return Math.max(result, 3); + } public DifficultyData getDifficulty() { return difficultyData; } public void renameDeck( String text) { - deck = (Deck)deck.copyTo(text); decks[selectedDeckIndex]=deck; } - public ItemPool getNewCards() { - return newCards; - } - public int cardSellPrice(PaperCard card) - { - return (int) (CardUtil.getCardPrice(card)*difficultyData.sellFactor); + public int cardSellPrice(PaperCard card) { + return (int)(CardUtil.getCardPrice(card)*difficultyData.sellFactor); } public void sellCard(PaperCard card, Integer result) { - float price= CardUtil.getCardPrice(card)*result; - price=difficultyData.sellFactor*price; - cards.remove(card, result); - addGold((int) price); + float price = CardUtil.getCardPrice(card) * result; + price *= difficultyData.sellFactor; + cards.remove(card, result); + addGold((int)price); } public void removeItem(String name) { - if(name==null||name.equals(""))return; + if(name == null || name.equals("")) return; inventoryItems.removeValue(name,false); - if(equippedItems.values().contains(name)&&!inventoryItems.contains(name,false)) - { + if(equippedItems.values().contains(name) && !inventoryItems.contains(name,false)) { equippedItems.values().remove(name); } } public void equip(ItemData item) { - if(equippedItems.get(item.equipmentSlot)!=null&&equippedItems.get(item.equipmentSlot).equals(item.name)) - { + if(equippedItems.get(item.equipmentSlot) != null && equippedItems.get(item.equipmentSlot).equals(item.name)) { equippedItems.remove(item.equipmentSlot); - } - else - { + } else { equippedItems.put(item.equipmentSlot,item.name); } onEquipmentChange.emit(); } - public String itemInSlot(String key) { - return equippedItems.get(key); - } - - public Collection getEquippedItems() { - return equippedItems.values(); - } + public String itemInSlot(String key) { return equippedItems.get(key); } public float equipmentSpeed() { float factor=1.0f; @@ -566,6 +580,24 @@ public class AdventurePlayer implements Serializable, SaveFileContent { return factor; } + public float goldModifier(boolean sale) { + float factor = 1.0f; + for(String name:equippedItems.values()) { + ItemData data=ItemData.getItem(name); + if(data != null && data.effect.goldModifier > 0.0) //Avoid negative modifiers. + factor *= data.effect.goldModifier; + } + if(blessing != null) { //If a blessing gives speed, take it into account. + if(blessing.goldModifier > 0.0) + factor *= blessing.goldModifier; + } + if(sale) return Math.max(1.0f + (1.0f - factor), 2.5f); + return Math.max(factor, 0.25f); + } + public float goldModifier(){ + return goldModifier(false); + } + public boolean hasItem(String name) { return inventoryItems.contains(name, false); } @@ -578,6 +610,8 @@ public class AdventurePlayer implements Serializable, SaveFileContent { return true; } + + // Quest functions. public void setQuestFlag(String key, int value){ questFlags.put(key, (byte) value); } diff --git a/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java b/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java index 8f36772308b..20dcc49108e 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java @@ -35,6 +35,7 @@ import forge.sound.SoundSystem; import forge.toolbox.FOptionPane; import forge.trackable.TrackableCollection; import forge.util.Aggregates; +import forge.util.Callback; import org.apache.commons.lang3.tuple.Pair; import java.util.*; @@ -44,7 +45,6 @@ import java.util.*; * Forge screen scene that contains the duel screen */ public class DuelScene extends ForgeScene { - //GameLobby lobby; HostedMatch hostedMatch; EnemySprite enemy; @@ -53,28 +53,66 @@ public class DuelScene extends ForgeScene { private EffectData dungeonEffect; Deck playerDeck, enemyDeck; boolean chaosBattle = false; + boolean callbackExit = false; List playerExtras = new ArrayList<>(); List AIExtras = new ArrayList<>(); - public DuelScene() { - - } + public DuelScene() {} @Override - public void dispose() { - } + public void dispose() {} + public boolean hasCallbackExit() { + return callbackExit; + } public void GameEnd() { boolean winner=humanPlayer == hostedMatch.getGame().getMatch().getWinner(); String enemyName=(enemy.nameOverride.isEmpty() ? enemy.getData().name : enemy.nameOverride); Current.player().clearBlessing(); + if (chaosBattle && !winner) { + callbackExit = true; + List insult = Lists.newArrayList("I'm sorry...","... ...." ,"Learn from your defeat.", + "I haven't begun to use my full power.","No matter how much you try, you still won't beat me.", + "Your technique need work.","Rookie.","That's your best?","Hah ha ha ha ha ha ha!","?!......... (Seriously?!)", + "Forget about a rematch. Practice more instead." ,"That was a 100% effort on my part! Well, actually, no... That was more like 50%.", + "If you expected me to lose out of generosity, I'm truly sorry!" ,"You'll appreciate that I held back during the match!", + "That's the best you can do?","Don't waste my time with your skills!","Ha-ha-ha! What's the matter?", + "I hope I didn't hurt your ego too badly... Oops!","This match... I think I've learned something from this.", + "Hey! Don't worry about it!","You are not worthy!","Hm! You should go back to playing puzzle games!", + "Thought you could beat me? Whew, talk about conceited.","*Yawn* ... Huh? It's over already? But I just woke up!", + "Next time bring an army. It might give you a chance." ,"The reason you lost is quite simple...", + "Is that all you can do?","You need to learn more to stand a chance.","You weren't that bad.","You made an effort at least.", + "From today, you can call me teacher.", "Hmph, predictable!", "I haven't used a fraction of my REAL power!" ); + String message = Aggregates.random(insult); + FThreads.invokeInEdtNowOrLater(() -> FOptionPane.showMessageDialog(message, enemyName, new FBufferedImage(120, 120) { + @Override + protected void draw(Graphics g, float w, float h) { + if (FSkin.getAvatars().get(90000) != null) + g.drawImage(FSkin.getAvatars().get(90000), 0, 0, w, h); + } + }, new Callback() { + @Override + public void run(Integer result) { + if (result == 0) { + afterGameEnd(enemyName, winner); + } + } + })); + } else { + afterGameEnd(enemyName, winner); + } + } + void afterGameEnd(String enemyName, boolean winner) { Gdx.app.postRunnable(new Runnable() { @Override public void run() { SoundSystem.instance.setBackgroundMusic(MusicPlaylist.MENUS); //start background music dungeonEffect = null; + callbackExit = false; + Forge.clearTransitionScreen(); + Forge.clearCurrentScreen(); Scene last = Forge.switchToLast(); if (last instanceof HudScene) { @@ -83,12 +121,10 @@ public class DuelScene extends ForgeScene { } } }); - - } void addEffects(RegisteredPlayer player,Array effects) { if( effects == null ) return; - //Apply various effects. + //Apply various combat effects. int lifeMod=0; int changeStartCards=0; Array startCards=new Array<>(); @@ -140,8 +176,12 @@ public class DuelScene extends ForgeScene { //Collect and add items effects first. for(String playerItem:advPlayer.getEquippedItems()) { ItemData item=ItemData.getItem(playerItem); - playerEffects.add(item.effect); - if(item.effect.opponent != null) oppEffects.add(item.effect.opponent); + if(item != null) { + playerEffects.add(item.effect); + if (item.effect.opponent != null) oppEffects.add(item.effect.opponent); + } else { + System.err.printf("Item %s not found.", playerItem); + } } if(enemy.getData().equipment!=null) { for(String oppItem:enemy.getData().equipment) { @@ -213,10 +253,11 @@ public class DuelScene extends ForgeScene { "It's all or nothing!","It's all on the line!","You can't back down now!","Do you have what it takes?","What will happen next?", "Don't blink!","You can't lose here!","There's no turning back!","It's all or nothing now!"); String message = Aggregates.random(list); - FThreads.delayInEDT(1000, () -> FThreads.invokeInEdtNowOrLater(() -> FOptionPane.showMessageDialog(message, aiPlayer.getPlayer().getName(), new FBufferedImage(120, 120) { + FThreads.delayInEDT(600, () -> FThreads.invokeInEdtNowOrLater(() -> FOptionPane.showMessageDialog(message, aiPlayer.getPlayer().getName(), new FBufferedImage(120, 120) { @Override protected void draw(Graphics g, float w, float h) { - g.drawImage(FSkin.getAvatars().get(90000), 0, 0, w, h); + if (FSkin.getAvatars().get(90000) != null) + g.drawImage(FSkin.getAvatars().get(90000), 0, 0, w, h); } }))); } @@ -264,7 +305,7 @@ public class DuelScene extends ForgeScene { } else { this.AIExtras.clear(); this.playerExtras.clear(); - this.enemyDeck = this.enemy.getData().copyPlayerDeck ? this.playerDeck : this.enemy.getData().generateDeck(Current.player().isFantasyMode()); + this.enemyDeck = this.enemy.getData().copyPlayerDeck ? this.playerDeck : this.enemy.getData().generateDeck(Current.player().isFantasyMode(), Current.player().getDifficulty().name.equalsIgnoreCase("Hard")); } } public Deck getPlayerDeck() { diff --git a/forge-gui-mobile/src/forge/adventure/scene/InventoryScene.java b/forge-gui-mobile/src/forge/adventure/scene/InventoryScene.java index dea63ec1147..6d03ba899fb 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/InventoryScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/InventoryScene.java @@ -43,13 +43,14 @@ public class InventoryScene extends UIScene { public void delete() { ItemData data = ItemData.getItem(itemLocation.get(selected)); - Current.player().removeItem(data.name); - + if(data != null) { + Current.player().removeItem(data.name); + } updateInventory(); } public void equip() { - if(selected==null)return; + if(selected == null) return; ItemData data = ItemData.getItem(itemLocation.get(selected)); Current.player().equip(data); updateInventory(); @@ -242,17 +243,19 @@ public class InventoryScene extends UIScene { } }); } - for(Map.Entry slot :equipmentSlots.entrySet()) - { + for(Map.Entry slot :equipmentSlots.entrySet()) { if(slot.getValue().getChildren().size>=2) slot.getValue().removeActorAt(1,false); String equippedItem=Current.player().itemInSlot(slot.getKey()); - if(equippedItem==null||equippedItem.equals("")) + if(equippedItem == null || equippedItem.equals("")) continue; - Image img=new Image(ItemData.getItem(equippedItem).sprite()); - img.setX((slot.getValue().getWidth()-img.getWidth())/2); - img.setY((slot.getValue().getHeight()-img.getHeight())/2); - slot.getValue().addActor(img); + ItemData item = ItemData.getItem(equippedItem); + if(item != null) { + Image img = new Image(item.sprite()); + img.setX((slot.getValue().getWidth() - img.getWidth()) / 2); + img.setY((slot.getValue().getHeight() - img.getHeight()) / 2); + slot.getValue().addActor(img); + } } } diff --git a/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java b/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java index bc4d24d74e2..9f693e86889 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java @@ -47,10 +47,24 @@ public class RewardScene extends UIScene { float flipCountDown = 1.0f; float exitCountDown = 0.0f; //Serves as additional check for when scene is exiting, so you can't double tap too fast. + public void quitScene() { + //There were reports of memory leaks after using the shop many times, so remove() everything on exit to be sure. + for(Actor A: new Array.ArrayIterator<>(generated)) { + if(A instanceof RewardActor){ + ((RewardActor) A).dispose(); + A.remove(); + } + } + Forge.switchToLast(); + } + public boolean done() { GameHUD.getInstance().getTouchpad().setVisible(false); if (doneClicked) { - if(exitCountDown > 0.2f) { Forge.switchToLast(); } //Wait a little bit to prevent double tap. + if(exitCountDown > 0.2f) { + clearGenerated(); + Forge.switchToLast(); + } return true; } @@ -72,23 +86,39 @@ public class RewardScene extends UIScene { exitCountDown = 0.0f; doneClicked = true; } else { - Forge.switchToLast(); + clearGenerated(); + quitScene(); } } else { - Forge.switchToLast(); + clearGenerated(); + quitScene(); } return true; } + void clearGenerated() { + for (Actor actor : new Array.ArrayIterator<>(generated)) { + if (!(actor instanceof RewardActor)) { + continue; + } + RewardActor reward = (RewardActor) actor; + reward.clearHoldToolTip(); + try { + stage.getActors().removeValue(reward, true); + } catch (Exception e) {} + } + } @Override public void act(float delta) { stage.act(delta); ImageCache.allowSingleLoad(); if (doneClicked) { - if (type == Type.Loot) + if (type == Type.Loot) { flipCountDown -= Gdx.graphics.getDeltaTime(); exitCountDown += Gdx.graphics.getDeltaTime(); + } if (flipCountDown <= 0) { + clearGenerated(); Forge.switchToLast(); } } @@ -117,10 +147,8 @@ public class RewardScene extends UIScene { public void loadRewards(Array newRewards, Type type, ShopActor shopActor) { - this.type = type; + this.type = type; doneClicked = false; - - for (Actor actor : new Array.ArrayIterator<>(generated)) { actor.remove(); if (actor instanceof RewardActor) { @@ -218,9 +246,7 @@ public class RewardScene extends UIScene { if (currentRow != ((i + 1) / numberOfColumns)) yOff += doneButton.getHeight(); - TextButton buyCardButton = new BuyButton(shopActor.getObjectId(), i, shopActor.isUnlimited()?null:shopActor.getMapStage().getChanges(), actor, doneButton); - generated.add(buyCardButton); if (!skipCard) { stage.addActor(buyCardButton); @@ -265,6 +291,7 @@ public class RewardScene extends UIScene { setX(actor.getX()); setY(actor.getY() - getHeight()); price = CardUtil.getRewardPrice(actor.getReward()); + price *= Current.player().goldModifier(); setText("$ " + price); addListener(new ClickListener() { @Override diff --git a/forge-gui-mobile/src/forge/adventure/scene/SceneType.java b/forge-gui-mobile/src/forge/adventure/scene/SceneType.java index 66455737811..0bd3a81bc58 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/SceneType.java +++ b/forge-gui-mobile/src/forge/adventure/scene/SceneType.java @@ -17,8 +17,8 @@ public enum SceneType { DeckSelectScene(new DeckSelectScene()), ShopScene(new ShopScene()), PlayerStatisticScene(new PlayerStatisticScene()), - InventoryScene(new InventoryScene()); - + InventoryScene(new InventoryScene()), + SpellSmithScene(new SpellSmithScene()); public final Scene instance; SceneType(Scene scene) { diff --git a/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java b/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java index 26ffaf5dc16..0d6e55b270a 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java @@ -133,7 +133,7 @@ public class SettingsScene extends UIScene { Label label = Controls.newLabel(name); label.setWrap(true); settingGroup.row().space(5); - settingGroup.add(label).align(Align.left).pad(2, 2, 2, 5).expand(); + settingGroup.add(label).align(Align.left).pad(2, 2, 2, 5).width(100).expand(); } @Override diff --git a/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java b/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java new file mode 100644 index 00000000000..d56b2c8a3d5 --- /dev/null +++ b/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java @@ -0,0 +1,301 @@ +package forge.adventure.scene; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.InputEvent; +import com.badlogic.gdx.scenes.scene2d.ui.*; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; +import forge.Forge; +import forge.StaticData; +import forge.adventure.data.RewardData; +import forge.adventure.util.Config; +import forge.adventure.util.Current; +import forge.adventure.util.Reward; +import forge.adventure.util.RewardActor; +import forge.card.CardEdition; +import forge.card.ColorSet; +import forge.item.PaperCard; +import forge.util.MyRandom; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + + +public class SpellSmithScene extends UIScene { + private List cardPool = new ArrayList<>(); + private Label goldLabel; + private TextButton pullButton; + private ScrollPane rewardDummy; + private RewardActor rewardActor; + SelectBox editionList; + //Button containers. + final private HashMap rarityButtons = new HashMap<>(); + final private HashMap costButtons = new HashMap<>(); + final private HashMap colorButtons = new HashMap<>(); + //Filter variables. + private String edition = ""; + private String rarity = ""; + private int cost_low = -1; + private int cost_high = 9999; + //Other + private float basePrice = 125f; + private int currentPrice = 0; + + public SpellSmithScene() { super(Forge.isLandscapeMode() ? "ui/spellsmith.json" : "ui/spellsmith_portrait.json"); } + + public boolean done() { + if(rewardActor != null) rewardActor.remove(); + cardPool.clear(); //Get rid of cardPool, filtering is fast enough to justify keeping it cached. + Forge.switchToLast(); + return true; + } + + private boolean selectRarity(String what){ + for(Map.Entry B : rarityButtons.entrySet()) + B.getValue().setColor(Color.WHITE); + switch(what){ + case "BCommon": + if(rarity.equals("C")) { rarity = ""; return false; } + rarity = "C"; break; + case "BUncommon": + if(rarity.equals("U")) { rarity = ""; return false; } + rarity = "U"; break; + case "BRare": + if(rarity.equals("R")) { rarity = ""; return false; } + rarity = "R"; break; + case "BMythic": + if(rarity.equals("M")) { rarity = ""; return false; } + rarity = "M"; break; + default: + rarity = ""; break; + } + return true; + } + + private void selectColor(String what){ + TextButton B = colorButtons.get(what); + switch(what){ + case "BColorless": + if(B.getColor().equals(Color.RED)) B.setColor(Color.WHITE); else { + for (Map.Entry BT : colorButtons.entrySet()) + BT.getValue().setColor(Color.WHITE); + B.setColor(Color.RED); + } + break; + case "BBlack": + case "BBlue": + case "BGreen": + case "BRed": + case "BWhite": + if(B.getColor().equals(Color.RED)) B.setColor(Color.WHITE); else B.setColor(Color.RED); + break; + + } + } + + private boolean selectCost(String what){ + for(Map.Entry B : costButtons.entrySet()) + B.getValue().setColor(Color.WHITE); + switch(what){ + case "B02": + if(cost_low == 0 && cost_high == 2) { cost_low = -1; cost_high = 9999; return false; } + cost_low = 0; cost_high = 2; break; + case "B35": + if(cost_low == 3 && cost_high == 5) { cost_low = -1; cost_high = 9999; return false; } + cost_low = 3; cost_high = 5; break; + case "B68": + if(cost_low == 6 && cost_high == 8) { cost_low = -1; cost_high = 9999; return false; } + cost_low = 6; cost_high = 8; break; + case "B9X": + if(cost_low == 9 && cost_high == 9999) { cost_low = -1; cost_high = 9999; return false; } + cost_low = 9; cost_high = 9999; break; + default: + cost_low = -1; break; + } + return true; + } + + @Override + public void enter(){ + edition = ""; + cost_low = -1; cost_high = 9999; + rarity = ""; + currentPrice = (int)basePrice; + goldLabel.setText("Gold: "+ Current.player().getGold()); + + for(Map.Entry B : colorButtons.entrySet()) B.getValue().setColor(Color.WHITE); + for(Map.Entry B : costButtons.entrySet()) B.getValue().setColor(Color.WHITE); + for(Map.Entry B : rarityButtons.entrySet()) B.getValue().setColor(Color.WHITE); + editionList.setColor(Color.WHITE); + filterResults(); + super.enter(); + } + + @Override + public void resLoaded() { + super.resLoaded(); + List editions = StaticData.instance().getSortedEditions(); + editions = editions.stream().filter(input -> { + if(input == null) return false; + return(!Config.instance().getConfigData().restrictedEditions.contains(input.getCode())); + }).collect(Collectors.toList()); + editionList = ui.findActor("BSelectPlane"); + rewardDummy = ui.findActor("RewardDummy"); + rewardDummy.setVisible(false); + editionList.clearItems(); + editionList.showScrollPane(); + editionList.setItems(editions.toArray(new CardEdition[editions.size()])); + editionList.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor){ + CardEdition E = editionList.getSelected(); + edition = E.getCode(); + editionList.setColor(Color.RED); + filterResults(); + } + }); + + goldLabel = ui.findActor("gold"); + pullButton = ui.findActor("pull"); + pullButton.setDisabled(true); + goldLabel.setText("Gold: "+ Current.player().getGold()); + for(String i : new String[]{"BBlack", "BBlue", "BGreen", "BRed", "BWhite", "BColorless"} ){ + TextButton button = ui.findActor(i); + if(button != null){ + colorButtons.put(i, button); + button.addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y){ + selectColor(i); + filterResults(); + } + }); + } + } + for(String i : new String[]{"BCommon", "BUncommon", "BRare", "BMythic"} ){ + TextButton button = ui.findActor(i); + if(button != null) { + rarityButtons.put(i, button); + button.addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + if(selectRarity(i)) button.setColor(Color.RED); + filterResults(); + } + }); + } + } + for(String i : new String[]{"B02", "B35", "B68", "B9X"} ){ + TextButton button = ui.findActor(i); + if(button != null) { + costButtons.put(i, button); + button.addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + if(selectCost(i)) button.setColor(Color.RED); + filterResults(); + } + }); + } + } + + ui.onButtonPress("done", new Runnable() { + @Override + public void run() { + SpellSmithScene.this.done(); + } + }); + ui.onButtonPress("pull", new Runnable() { + @Override + public void run() { SpellSmithScene.this.pullCard(); } + }); + ui.onButtonPress("BResetEdition", new Runnable() { + @Override + public void run() { + editionList.setColor(Color.WHITE); + edition = ""; + filterResults(); + } + }); + + } + + + public void filterResults() { + RewardData R = new RewardData(); + Iterable P = R.getAllCards(); + goldLabel.setText("Gold: "+ Current.player().getGold()); + float totalCost = basePrice * Current.player().goldModifier(); + final List colorFilter = new ArrayList<>(); + for(Map.Entry B : colorButtons.entrySet()) + switch (B.getKey()){ + case "BColorless": + if(B.getValue().getColor().equals(Color.RED)) colorFilter.add("Colorless"); + continue; + case "BBlack": + if(B.getValue().getColor().equals(Color.RED)) colorFilter.add("Black"); + break; + case "BBlue": + if(B.getValue().getColor().equals(Color.RED)) colorFilter.add("Blue"); + break; + case "BGreen": + if(B.getValue().getColor().equals(Color.RED)) colorFilter.add("Green"); + break; + case "BRed": + if(B.getValue().getColor().equals(Color.RED)) colorFilter.add("Red"); + break; + case "BWhite": + if(B.getValue().getColor().equals(Color.RED)) colorFilter.add("White"); + break; + } + P = StreamSupport.stream(P.spliterator(), false).filter(input -> { + //L|Basic Land, C|Common, U|Uncommon, R|Rare, M|Mythic Rare, S|Special, N|None + if (input == null) return false; + if(!edition.isEmpty()) if (!input.getEdition().equals(edition)) return false; + if(colorFilter.size() > 0) if(input.getRules().getColor() != ColorSet.fromNames(colorFilter)) return false; + if(!rarity.isEmpty()) if (!input.getRarity().toString().equals(rarity)) return false; + if(cost_low > -1) { + if (!(input.getRules().getManaCost().getCMC() >= cost_low && input.getRules().getManaCost().getCMC() <= cost_high)) + return false; + } + return true; + }).collect(Collectors.toList()); + //Stream method is very fast, might not be necessary to precache anything. + if(!edition.isEmpty()) totalCost *= 4.0f; //Edition select cost multiplier. This is a huge factor, so it's most expensive. + if(colorFilter.size() > 0) totalCost *= Math.min(colorFilter.size() * 2.5f, 6.0f); //Color filter cost multiplier. + if(!rarity.isEmpty()){ //Rarity cost multiplier. + switch(rarity){ + case "C": totalCost *= 1.5f; break; + case "U": totalCost *= 2.5f; break; + case "R": totalCost *= 4.0f; break; + case "M": totalCost *= 5.5f; break; + default: break; + } + } + if(cost_low > -1) totalCost *= 2.5f; //And CMC cost multiplier. + cardPool = StreamSupport.stream(P.spliterator(), false).collect(Collectors.toList()); + pullButton.setText("Pull (" + cardPool.size() + ") " + totalCost + "G"); + currentPrice = (int)totalCost; + pullButton.setDisabled(false); + if(!(cardPool.size() > 0) || Current.player().getGold() < totalCost) + pullButton.setDisabled(true); + } + + public void pullCard() { + PaperCard P = cardPool.get(MyRandom.getRandom().nextInt(cardPool.size())); //Don't use the standard RNG. + Reward R = new Reward(P); + Current.player().addReward(R); + Current.player().takeGold(currentPrice); + if(Current.player().getGold() < currentPrice) pullButton.setDisabled(true); + if(rewardActor != null) rewardActor.remove(); + rewardActor = new RewardActor(R, true); + rewardActor.flip(); //Make it flip so it draws visual attention, why not. + rewardActor.setBounds(rewardDummy.getX(), rewardDummy.getY(), rewardDummy.getWidth(), rewardDummy.getHeight()); + stage.addActor(rewardActor); + } +} diff --git a/forge-gui-mobile/src/forge/adventure/scene/StartScene.java b/forge-gui-mobile/src/forge/adventure/scene/StartScene.java index cd72c20d35f..38f4dc0b770 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/StartScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/StartScene.java @@ -2,20 +2,27 @@ package forge.adventure.scene; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; +import com.badlogic.gdx.scenes.scene2d.ui.Dialog; import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.utils.Align; import forge.Forge; import forge.adventure.stage.GameHUD; import forge.adventure.stage.MapStage; +import forge.adventure.util.Config; +import forge.adventure.util.Controls; import forge.adventure.world.WorldSave; +import forge.screens.TransitionScreen; /** * First scene after the splash screen */ public class StartScene extends UIScene { - TextButton saveButton, resumeButton, newGameButton, newGameButtonPlus, loadButton, settingsButton, exitButton, switchButton; + + TextButton saveButton, resumeButton, continueButton, newGameButton, newGameButtonPlus, loadButton, settingsButton, exitButton, switchButton; + Dialog dialog; public StartScene() { - super(Forge.isLandscapeMode()?"ui/start_menu.json":"ui/start_menu_portrait.json"); + super(Forge.isLandscapeMode() ? "ui/start_menu.json" : "ui/start_menu_portrait.json"); } @@ -45,13 +52,31 @@ public class StartScene extends UIScene { return true; } + public boolean Continue() { + final String lastActiveSave = Config.instance().getSettingData().lastActiveSave; + + if (WorldSave.isSafeFile(lastActiveSave) && WorldSave.load(WorldSave.filenameToSlot(lastActiveSave))) { + Forge.setTransitionScreen(new TransitionScreen(new Runnable() { + @Override + public void run() { + Forge.switchScene(SceneType.GameScene.instance); + } + }, null, false, true)); + } else { + Forge.clearTransitionScreen(); + } + + return true; + } + public boolean settings() { Forge.switchScene(SceneType.SettingsScene.instance); return true; } public boolean Exit() { - Forge.exit(true); + if (dialog != null) + dialog.show(stage); return true; } @@ -61,7 +86,21 @@ public class StartScene extends UIScene { if (hasSaveButton) hasSaveButton = !((TileMapScene) SceneType.TileMapScene.instance).currentMap().isInMap(); saveButton.setVisible(hasSaveButton); - resumeButton.setVisible(WorldSave.getCurrentSave().getWorld().getData() != null); + + boolean hasResumeButton = WorldSave.getCurrentSave().getWorld().getData() != null; + resumeButton.setVisible(hasResumeButton); + + // Continue button mutually exclusive with resume button + if (Config.instance().getSettingData().lastActiveSave != null && !hasResumeButton) { + continueButton.setVisible(true); + if (!Forge.isLandscapeMode()) { + continueButton.setX(resumeButton.getX()); + continueButton.setY(resumeButton.getY()); + } + } else { + continueButton.setVisible(false); + } + Gdx.input.setInputProcessor(stage); //Start taking input from the ui } @@ -82,34 +121,43 @@ public class StartScene extends UIScene { @Override public void resLoaded() { super.resLoaded(); - ui.onButtonPress("Start", () -> StartScene.this.NewGame()); - ui.onButtonPress("Start+", () -> NewGamePlus()); - ui.onButtonPress("Load", () -> StartScene.this.Load()); - ui.onButtonPress("Save", () -> StartScene.this.Save()); - ui.onButtonPress("Resume", () -> StartScene.this.Resume()); - ui.onButtonPress("Settings", () -> StartScene.this.settings()); - ui.onButtonPress("Exit", () -> StartScene.this.Exit()); - ui.onButtonPress("Switch", () -> Forge.switchToClassic()); + ui.onButtonPress("Start", () -> StartScene.this.NewGame()); + ui.onButtonPress("Start+", () -> NewGamePlus()); + ui.onButtonPress("Load", () -> StartScene.this.Load()); + ui.onButtonPress("Save", () -> StartScene.this.Save()); + ui.onButtonPress("Resume", () -> StartScene.this.Resume()); + ui.onButtonPress("Continue", () -> StartScene.this.Continue()); + ui.onButtonPress("Settings", () -> StartScene.this.settings()); + ui.onButtonPress("Exit", () -> StartScene.this.Exit()); + ui.onButtonPress("Switch", () -> Forge.switchToClassic()); - newGameButton = ui.findActor("Start"); - newGameButton.getLabel().setText(Forge.getLocalizer().getMessage("lblNewGame")); - newGameButtonPlus = ui.findActor("Start+"); - newGameButtonPlus.getLabel().setText(Forge.getLocalizer().getMessage("lblNewGame")+"+"); - loadButton = ui.findActor("Load"); - loadButton.getLabel().setText(Forge.getLocalizer().getMessage("lblLoad")); - saveButton = ui.findActor("Save"); - saveButton.getLabel().setText(Forge.getLocalizer().getMessage("lblSave")); - resumeButton = ui.findActor("Resume"); - resumeButton.getLabel().setText(Forge.getLocalizer().getMessage("lblResume")); - settingsButton = ui.findActor("Settings"); - settingsButton.getLabel().setText(Forge.getLocalizer().getMessage("lblSettings")); - exitButton = ui.findActor("Exit"); - exitButton.getLabel().setText(Forge.getLocalizer().getMessage("lblExit")); - switchButton = ui.findActor("Switch"); - switchButton.getLabel().setText(Forge.getLocalizer().getMessage("lblClassic")); + newGameButton = ui.findActor("Start"); + newGameButton.getLabel().setText(Forge.getLocalizer().getMessage("lblNewGame")); + newGameButtonPlus = ui.findActor("Start+"); + newGameButtonPlus.getLabel().setText(Forge.getLocalizer().getMessage("lblNewGame") + "+"); + loadButton = ui.findActor("Load"); + loadButton.getLabel().setText(Forge.getLocalizer().getMessage("lblLoad")); + saveButton = ui.findActor("Save"); + saveButton.getLabel().setText(Forge.getLocalizer().getMessage("lblSave")); + resumeButton = ui.findActor("Resume"); + resumeButton.getLabel().setText(Forge.getLocalizer().getMessage("lblResume")); + continueButton = ui.findActor("Continue"); + continueButton.getLabel().setText(Forge.getLocalizer().getMessage("lblContinue")); + settingsButton = ui.findActor("Settings"); + settingsButton.getLabel().setText(Forge.getLocalizer().getMessage("lblSettings")); + exitButton = ui.findActor("Exit"); + exitButton.getLabel().setText(Forge.getLocalizer().getMessage("lblExit")); + switchButton = ui.findActor("Switch"); + switchButton.getLabel().setText(Forge.getLocalizer().getMessage("lblClassic")); - saveButton.setVisible(false); - resumeButton.setVisible(false); + saveButton.setVisible(false); + resumeButton.setVisible(false); + dialog = Controls.newDialog(Forge.getLocalizer().getMessage("lblExitForge")); + dialog.getButtonTable().add(Controls.newLabel(Forge.getLocalizer().getMessage("lblAreYouSureYouWishExitForge"))).colspan(2).pad(2, 15, 2, 15); + dialog.getButtonTable().row(); + dialog.getButtonTable().add(Controls.newTextButton(Forge.getLocalizer().getMessage("lblExit"), () -> Forge.exit(true))).width(60).align(Align.left).padLeft(15); + dialog.getButtonTable().add(Controls.newTextButton(Forge.getLocalizer().getMessage("lblCancel"), () -> dialog.hide())).width(60).align(Align.right).padRight(15); + dialog.getColor().a = 0; } private void NewGamePlus() { diff --git a/forge-gui-mobile/src/forge/adventure/stage/Console.java b/forge-gui-mobile/src/forge/adventure/stage/Console.java index 19db3d919c9..0aeb1827749 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/Console.java +++ b/forge-gui-mobile/src/forge/adventure/stage/Console.java @@ -8,23 +8,22 @@ import forge.adventure.util.Controls; public class Console extends Window { private final ScrollPane scroll; + private String last = ""; + private final InputLine input; + private final Table content; + private final ConsoleCommandInterpreter interpreter = new ConsoleCommandInterpreter(); public void toggle() { - if(isVisible()) - { + if(isVisible()) { setVisible(false); getStage().unfocus(input); - } - else - { + } else { setVisible(true); getStage().setKeyboardFocus(input); } } - class InputLine extends TextField - { - + class InputLine extends TextField { private Console console; public InputLine(Console console) { @@ -32,36 +31,34 @@ public class Console extends Window { this.console = console; writeEnters=true; } + @Override protected InputListener createInputListener () { - TextField self=this; + TextField self = this; return new TextFieldClickListener() { @Override public boolean keyTyped (InputEvent event, char character) { - - - // Disallow "typing" most ASCII control characters, which would show up as a space when onlyFontChars is true. - switch (character) { - case BACKSPACE: - break; - case TAB: - self.setText(console.complete(self.getText())); - self.setCursorPosition(Integer.MAX_VALUE); - break; - case NEWLINE: - case CARRIAGE_RETURN: - console.command(self.getText()); - self.setText(""); - return false; - default: - if (character < 32) return false; - } - return super.keyTyped(event,character); + // Disallow "typing" most ASCII control characters, which would show up as a space when onlyFontChars is true. + switch (character) { + case BACKSPACE: + break; + case TAB: + self.setText(console.complete(self.getText())); + self.setCursorPosition(Integer.MAX_VALUE); + break; + case NEWLINE: + case CARRIAGE_RETURN: + console.command(self.getText()); + self.setText(""); + return false; + default: + if (character < 32) return false; + } + return super.keyTyped(event,character); } }; } - } private String complete(String text) { @@ -72,6 +69,7 @@ public class Console extends Window { Cell