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 de5ac94e04b..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; } diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 66b8d2d6e17..b2b2ce2ea96 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1326,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; } @@ -1339,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) { diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 83520cd8c55..a282325a750 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -131,10 +131,9 @@ public class ComputerUtil { sa = GameActionUtil.addExtraKeywordCost(sa); - if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { - if (!CharmEffect.makeChoices(sa)) { - return false; - } + if (sa.getApi() == ApiType.Charm && !CharmEffect.makeChoices(sa)) { + // 603.3c If no mode is chosen, the ability is removed from the stack. + return false; } if (chooseTargets != null) { chooseTargets.run(); @@ -250,6 +249,13 @@ public class ComputerUtil { return false; final Card source = sa.getHostCard(); + + Zone fromZone = game.getZoneOf(source); + int zonePosition = 0; + if (fromZone != null) { + zonePosition = fromZone.getCards().indexOf(source); + } + if (sa.isSpell() && !source.isCopiedSpell()) { sa.setHostCard(game.getAction().moveToStack(source, sa)); } @@ -257,11 +263,18 @@ public class ComputerUtil { sa = GameActionUtil.addExtraKeywordCost(sa); final Cost cost = sa.getPayCosts(); + final CostPayment pay = new CostPayment(cost, sa); + + // do this after card got added to stack + if (!sa.checkRestrictions(ai)) { + GameActionUtil.rollbackAbility(sa, fromZone, zonePosition, pay, source); + return false; + } + if (cost == null) { ComputerUtilMana.payManaCost(ai, sa, false); game.getStack().add(sa); } else { - final CostPayment pay = new CostPayment(cost, sa); if (pay.payComputerCosts(new AiCostDecision(ai, sa, false))) { game.getStack().add(sa); } @@ -292,17 +305,30 @@ public class ComputerUtil { newSA = GameActionUtil.addExtraKeywordCost(newSA); final Card source = newSA.getHostCard(); + + Zone fromZone = game.getZoneOf(source); + int zonePosition = 0; + if (fromZone != null) { + zonePosition = fromZone.getCards().indexOf(source); + } + if (newSA.isSpell() && !source.isCopiedSpell()) { newSA.setHostCard(game.getAction().moveToStack(source, newSA)); - if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) { - if (!CharmEffect.makeChoices(newSA)) { - return false; - } + if (newSA.getApi() == ApiType.Charm && !CharmEffect.makeChoices(newSA)) { + // 603.3c If no mode is chosen, the ability is removed from the stack. + return false; } } final CostPayment pay = new CostPayment(newSA.getPayCosts(), newSA); + + // do this after card got added to stack + if (!sa.checkRestrictions(ai)) { + GameActionUtil.rollbackAbility(sa, fromZone, zonePosition, pay, source); + return false; + } + pay.payComputerCosts(new AiCostDecision(ai, newSA, false)); game.getStack().add(newSA); @@ -2798,7 +2824,7 @@ public class ComputerUtil { } return type.is(CounterEnumType.AWAKENING) || type.is(CounterEnumType.MANIFESTATION) || type.is(CounterEnumType.PETRIFICATION) - || type.is(CounterEnumType.TRAINING); + || type.is(CounterEnumType.TRAINING) || type.is(CounterEnumType.GHOSTFORM); } public static Player evaluateBoardPosition(final List listToEvaluate) { 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/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java index 46f379218ce..2f39a64fbf5 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java @@ -7,6 +7,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; import forge.ai.AiCardMemory.MemorySet; import forge.ai.ability.AnimateAi; +import forge.ai.ability.TokenAi; import forge.card.ColorSet; import forge.game.Game; import forge.game.ability.AbilityUtils; @@ -14,6 +15,7 @@ import forge.game.ability.ApiType; import forge.game.card.*; import forge.game.card.CardPredicates.Presets; import forge.game.combat.Combat; +import forge.game.combat.CombatUtil; import forge.game.cost.*; import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; @@ -692,6 +694,72 @@ public class ComputerUtilCost { return false; } else if ("LowPriority".equals(aiLogic) && MyRandom.getRandom().nextInt(100) < 67) { return false; + } else if (aiLogic != null && aiLogic.startsWith("Fabricate")) { + final int n = Integer.valueOf(aiLogic.substring("Fabricate".length())); + + // if host would leave the play or if host is useless, create tokens + if (source.hasSVar("EndOfTurnLeavePlay") || ComputerUtilCard.isUselessCreature(payer, source)) { + return false; + } + + // need a copy for one with extra +1/+1 counter boost, + // without causing triggers to run + final Card copy = CardUtil.getLKICopy(source); + copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + n); + copy.setZone(source.getZone()); + + // if host would put into the battlefield attacking + Combat combat = source.getGame().getCombat(); + if (combat != null && combat.isAttacking(source)) { + final Player defender = combat.getDefenderPlayerByAttacker(source); + if (defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true)) { + return true; + } + return false; + } + + // if the host has haste and can attack + if (CombatUtil.canAttack(copy)) { + for (final Player opp : payer.getOpponents()) { + if (CombatUtil.canAttack(copy, opp) && + opp.canLoseLife() && + !ComputerUtilCard.canBeBlockedProfitably(opp, copy, true)) + return true; + } + } + + // TODO check for trigger to turn token ETB into +1/+1 counter for host + // TODO check for trigger to turn token ETB into damage or life loss for opponent + // in this cases Token might be prefered even if they would not survive + final Card tokenCard = TokenAi.spawnToken(payer, sa); + + // Token would not survive + if (!tokenCard.isCreature() || tokenCard.getNetToughness() < 1) { + return true; + } + + // Special Card logic, this one try to median its power with the number of artifacts + if ("Marionette Master".equals(source.getName())) { + CardCollection list = CardLists.filter(payer.getCardsIn(ZoneType.Battlefield), Presets.ARTIFACTS); + return list.size() >= copy.getNetPower(); + } else if ("Cultivator of Blades".equals(source.getName())) { + // Cultivator does try to median with number of Creatures + CardCollection list = payer.getCreaturesInPlay(); + return list.size() >= copy.getNetPower(); + } + + // evaluate Creature with +1/+1 + int evalCounter = ComputerUtilCard.evaluateCreature(copy); + + final CardCollection tokenList = new CardCollection(source); + for (int i = 0; i < n; ++i) { + tokenList.add(TokenAi.spawnToken(payer, sa)); + } + + // evaluate Host with Tokens + int evalToken = ComputerUtilCard.evaluateCreatureList(tokenList); + + return evalToken < evalCounter; } // Check for shocklands and similar ETB replacement effects 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 b7336ca66e8..40c0d4127ec 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -206,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); } @@ -375,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) { diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 990ab51e3df..cf01b8a09ae 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -34,6 +34,7 @@ import forge.game.GameObject; import forge.game.GameType; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; +import forge.game.ability.effects.CharmEffect; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; @@ -269,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 @@ -1045,16 +1046,14 @@ public class PlayerControllerAi extends PlayerController { } } - /* FIXME: the new implementation (below) requires implementing setupNewTargets in the AI controller, among other possible changes, otherwise breaks AI if (sa.isMayChooseNewTargets()) { - sa.setupNewTargets(player); - } - */ - if (sa.isMayChooseNewTargets() && !sa.setupTargets()) { - if (sa.isSpell()) { - getGame().getAction().ceaseToExist(sa.getHostCard(), false); + TargetChoices tc = sa.getTargets(); + if (!sa.setupTargets()) { + // if AI can't choose targets need to keep old one even if illegal + sa.setTargets(tc); } - continue; + // FIXME: the new implementation (below) requires implementing setupNewTargets in the AI controller, among other possible changes, otherwise breaks AI + // sa.setupNewTargets(player); } } // need finally add the new spell to the stack @@ -1064,6 +1063,9 @@ public class PlayerControllerAi extends PlayerController { } private boolean prepareSingleSa(final Card host, final SpellAbility sa, boolean isMandatory) { + if (sa.getApi() == ApiType.Charm) { + return CharmEffect.makeChoices(sa); + } if (sa.hasParam("TargetingPlayer")) { Player targetingPlayer = AbilityUtils.getDefinedPlayers(host, sa.getParam("TargetingPlayer"), sa).get(0); sa.setTargetingPlayer(targetingPlayer); @@ -1087,7 +1089,7 @@ public class PlayerControllerAi extends PlayerController { if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell? Spell spell = (Spell) tgtSA; // TODO if mandatory AI is only forced to use mana when it's already in the pool - if (tgtSA.checkRestrictions(brains.getPlayer()) && (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional)) { + if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) { if (noManaCost) { return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame()); } 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 28d0ced5046..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) 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 7f3a22dc133..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); } 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/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/CharmAi.java b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java index a78406df498..957dcb8f03d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java @@ -89,7 +89,6 @@ public class CharmAi extends SpellAbilityAi { // First pass using standard canPlayAi() for good choices for (AbilitySub sub : choices) { sub.setActivatingPlayer(ai); - sub.getRestrictions().setZone(sub.getParent().getRestrictions().getZone()); if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) { chosenList.add(sub); if (chosenList.size() == num) { @@ -101,8 +100,6 @@ public class CharmAi extends SpellAbilityAi { // Second pass using doTrigger(false) to fulfill minimum choice choices.removeAll(chosenList); for (AbilitySub sub : choices) { - sub.setActivatingPlayer(ai); - sub.getRestrictions().setZone(sub.getParent().getRestrictions().getZone()); if (aic.doTrigger(sub, false)) { chosenList.add(sub); if (chosenList.size() == min) { @@ -114,8 +111,6 @@ public class CharmAi extends SpellAbilityAi { if (chosenList.size() < min) { choices.removeAll(chosenList); for (AbilitySub sub : choices) { - sub.setActivatingPlayer(ai); - sub.getRestrictions().setZone(sub.getParent().getRestrictions().getZone()); if (aic.doTrigger(sub, true)) { chosenList.add(sub); if (chosenList.size() == min) { @@ -231,7 +226,6 @@ public class CharmAi extends SpellAbilityAi { } else { // Standard canPlayAi() sub.setActivatingPlayer(ai); - sub.getRestrictions().setZone(sub.getParent().getRestrictions().getZone()); if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) { chosenList.add(sub); if (chosenList.size() == min) { @@ -255,15 +249,6 @@ public class CharmAi extends SpellAbilityAi { return Aggregates.random(opponents); } - @Override - protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { - // already done by chooseOrderOfSimultaneousStackEntry - if (sa.getChosenList() != null) { - return true; - } - return super.doTriggerAINoCost(aiPlayer, sa, mandatory); - } - @Override public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) { // choices were already targeted diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java index e81059371c7..596ee8f6671 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java @@ -9,7 +9,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; import forge.ai.ComputerUtilAbility; -import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCost; import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; @@ -19,12 +18,9 @@ import forge.game.Game; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; -import forge.game.card.CardLists; -import forge.game.card.CardPredicates.Presets; import forge.game.card.CardUtil; import forge.game.card.CounterEnumType; import forge.game.combat.Combat; -import forge.game.combat.CombatUtil; import forge.game.cost.Cost; import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; @@ -43,7 +39,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) { return true; - } else if (aiLogic.startsWith("Fabricate") || "Riot".equals(aiLogic)) { + } else if ("Riot".equals(aiLogic)) { return true; } else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) { for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) { @@ -265,83 +261,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { } // if unsure, random? return Aggregates.random(spells); - } else if (logic.startsWith("Fabricate")) { - final int n = Integer.valueOf(logic.substring("Fabricate".length())); - if(spells.size() < 2) { - // If the creature is no longer on the battlefield, the option - // to add counters is already removed at this point. Return the - // only available option: create servo tokens. - return spells.get(0); - } - SpellAbility counterSA = spells.get(0), tokenSA = spells.get(1); - - // check for something which might prevent the counters to be placed on host - if (!host.canReceiveCounters(CounterEnumType.P1P1)) { - return tokenSA; - } - - // if host would leave the play or if host is useless, create tokens - if (host.hasSVar("EndOfTurnLeavePlay") || ComputerUtilCard.isUselessCreature(player, host)) { - return tokenSA; - } - - // need a copy for one with extra +1/+1 counter boost, - // without causing triggers to run - final Card copy = CardUtil.getLKICopy(host); - copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + n); - copy.setZone(host.getZone()); - - // if host would put into the battlefield attacking - if (combat != null && combat.isAttacking(host)) { - final Player defender = combat.getDefenderPlayerByAttacker(host); - if (defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true)) { - return counterSA; - } - return tokenSA; - } - - // if the host has haste and can attack - if (CombatUtil.canAttack(copy)) { - for (final Player opp : player.getOpponents()) { - if (CombatUtil.canAttack(copy, opp) && - opp.canLoseLife() && - !ComputerUtilCard.canBeBlockedProfitably(opp, copy, true)) - return counterSA; - } - } - - // TODO check for trigger to turn token ETB into +1/+1 counter for host - // TODO check for trigger to turn token ETB into damage or life loss for opponent - // in this cases Token might be prefered even if they would not survive - final Card tokenCard = TokenAi.spawnToken(player, tokenSA); - - // Token would not survive - if (!tokenCard.isCreature() || tokenCard.getNetToughness() < 1) { - return counterSA; - } - - // Special Card logic, this one try to median its power with the number of artifacts - if ("Marionette Master".equals(sourceName)) { - CardCollection list = CardLists.filter(player.getCardsIn(ZoneType.Battlefield), Presets.ARTIFACTS); - return list.size() >= copy.getNetPower() ? counterSA : tokenSA; - } else if ("Cultivator of Blades".equals(sourceName)) { - // Cultivator does try to median with number of Creatures - CardCollection list = player.getCreaturesInPlay(); - return list.size() >= copy.getNetPower() ? counterSA : tokenSA; - } - - // evaluate Creature with +1/+1 - int evalCounter = ComputerUtilCard.evaluateCreature(copy); - - final CardCollection tokenList = new CardCollection(host); - for (int i = 0; i < n; ++i) { - tokenList.add(TokenAi.spawnToken(player, tokenSA)); - } - - // evaluate Host with Tokens - int evalToken = ComputerUtilCard.evaluateCreatureList(tokenList); - - return evalToken >= evalCounter ? tokenSA : counterSA; } else if ("CombustibleGearhulk".equals(logic)) { Player controller = sa.getActivatingPlayer(); List zones = ZoneType.listValueOf("Graveyard, Battlefield, Exile"); 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 7c90cf65fd1..667aee15d71 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -913,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/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/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 f9c18915ca6..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; @@ -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/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 e5705d72927..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 { @@ -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/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/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/src/main/java/forge/ImageKeys.java b/forge-core/src/main/java/forge/ImageKeys.java index 3975890cf9b..12cb9493a6b 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; @@ -30,6 +31,7 @@ public final class ImageKeys { private static Map CACHE_CARD_PICS_SUBDIR; private static Map editionImageLookup = new HashMap<>(); + private static Set toFind = new HashSet<>(); private static boolean isLibGDXPort = false; @@ -109,7 +111,8 @@ public final class ImageKeys { filename = key; dir = CACHE_CARD_PICS_DIR; } - + if (toFind.contains(filename)) + return null; if (missingCards.contains(filename)) return null; @@ -171,10 +174,20 @@ public final class ImageKeys { return file; } //setlookup - file = setLookUpFile(filename, fullborderFile); - if (file != null) { - cachedCards.put(filename, file); - return file; + if (hasSetLookup(filename)) { + toFind.add(filename); + try { + ThreadUtil.getServicePool().submit(() -> { + File f = setLookUpFile(filename, fullborderFile); + if (f != null) + cachedCards.put(filename, f); + else //is null + missingCards.add(filename); + toFind.remove(filename); + }); + } catch (Exception e) { + toFind.remove(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 +263,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,34 +273,44 @@ 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 } - private static File setLookUpFile(String filename, String fullborderFile) { + public static boolean hasSetLookup(String filename) { + if (!StaticData.instance().getSetLookup().isEmpty()) { + return StaticData.instance().getSetLookup().keySet().stream().anyMatch(setKey -> filename.startsWith(setKey)); + } + + return false; + } + public static File setLookUpFile(String filename, String fullborderFile) { if (!StaticData.instance().getSetLookup().isEmpty()) { for (String setKey : StaticData.instance().getSetLookup().keySet()) { if (filename.startsWith(setKey)) { for (String setLookup : StaticData.instance().getSetLookup().get(setKey)) { String lookupDirectory = CACHE_CARD_PICS_DIR + setLookup; File f = new File(lookupDirectory); - String[] cardNames = f.list(); - if (cardNames != null) { - Set cardList = new HashSet<>(Arrays.asList(cardNames)); + if (f.exists() && f.isDirectory()) { for (String ext : FILE_EXTENSIONS) { if (ext.equals("")) continue; + File placeholder; String fb1 = fullborderFile.replace(setKey+"/","")+ext; - if (cardList.contains(fb1)) { - return new File(lookupDirectory+"/"+fb1); + placeholder = new File(lookupDirectory+"/"+fb1); + if (placeholder.exists()) { + return placeholder; } String fb2 = fullborderFile.replace(setKey+"/","").replaceAll("[0-9]*.fullborder", "1.fullborder")+ext; - if (cardList.contains(fb2)) { - return new File(lookupDirectory+"/"+fb2); + placeholder = new File(lookupDirectory+"/"+fb2); + if (placeholder.exists()) { + return placeholder; } String f1 = filename.replace(setKey+"/","")+ext; - if (cardList.contains(f1)) { - return new File(lookupDirectory+"/"+f1); + placeholder = new File(lookupDirectory+"/"+f1); + if (placeholder.exists()) { + return placeholder; } String f2 = filename.replace(setKey+"/","").replaceAll("[0-9]*.full", "1.full")+ext; - if (cardList.contains(f2)) { - return new File(lookupDirectory+"/"+f2); + placeholder = new File(lookupDirectory+"/"+f2); + if (placeholder.exists()) { + return placeholder; } } } diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java index e2d20940067..5570e7ea63a 100644 --- a/forge-core/src/main/java/forge/StaticData.java +++ b/forge-core/src/main/java/forge/StaticData.java @@ -8,9 +8,11 @@ import forge.card.PrintSheet; import forge.item.*; import forge.token.TokenDb; import forge.util.FileUtil; +import forge.util.ImageUtil; import forge.util.TextUtil; import forge.util.storage.IStorage; import forge.util.storage.StorageBase; +import org.apache.commons.lang3.tuple.Pair; import java.io.File; import java.util.*; @@ -45,6 +47,7 @@ public class StaticData { private boolean allowCustomCardsInDecksConformance; private boolean enableSmartCardArtSelection; + private boolean loadNonLegalCards; // Loaded lazily: private IStorage boosters; @@ -74,6 +77,7 @@ public class StaticData { this.customCardReader = customCardReader; this.allowCustomCardsInDecksConformance = allowCustomCardsInDecksConformance; this.enableSmartCardArtSelection = enableSmartCardArtSelection; + this.loadNonLegalCards = loadNonLegalCards; lastInstance = this; List funnyCards = new ArrayList<>(); List filtered = new ArrayList<>(); @@ -752,6 +756,129 @@ public class StaticData { preferences_avails[i] = prettifyCardArtPreferenceName(preferences[i]); return preferences_avails; } + public Pair audit(StringBuffer noImageFound, StringBuffer cardNotImplemented) { + int missingCount = 0; + int notImplementedCount = 0; + for (CardEdition e : editions) { + if (CardEdition.Type.FUNNY.equals(e.getType())) + continue; + boolean nifHeader = false; + boolean cniHeader = false; + boolean tokenHeader = false; + + String imagePath; + int artIndex = 1; + + HashMap> cardCount = new HashMap<>(); + for (CardEdition.CardInSet c : e.getAllCardsInSet()) { + if (cardCount.containsKey(c.name)) { + cardCount.put(c.name, Pair.of(c.collectorNumber.startsWith("F"), cardCount.get(c.name).getRight() + 1)); + } else { + cardCount.put(c.name, Pair.of(c.collectorNumber.startsWith("F"), 1)); + } + } + + // loop through the cards in this edition, considering art variations... + for (Map.Entry> entry : cardCount.entrySet()) { + String c = entry.getKey(); + artIndex = entry.getValue().getRight(); + + PaperCard cp = getCommonCards().getCard(c, e.getCode(), artIndex); + if (cp == null) { + cp = getVariantCards().getCard(c, e.getCode(), artIndex); + } + + if (cp == null) { + if (entry.getValue().getLeft()) //skip funny cards + continue; + if (!loadNonLegalCards && CardEdition.Type.FUNNY.equals(e.getType())) + continue; + if (!cniHeader) { + cardNotImplemented.append("Edition: ").append(e.getName()).append(" ").append("(").append(e.getCode()).append("/").append(e.getCode2()).append(")\n"); + cniHeader = true; + } + cardNotImplemented.append(" ").append(c).append("\n"); + notImplementedCount++; + continue; + } + + // check the front image + imagePath = ImageUtil.getImageRelativePath(cp, false, true, false); + if (imagePath != null) { + File file = ImageKeys.getImageFile(imagePath); + if (file == null && ImageKeys.hasSetLookup(imagePath)) + file = ImageKeys.setLookUpFile(imagePath, imagePath+"border"); + if (file == null) { + if (!nifHeader) { + noImageFound.append("Edition: ").append(e.getName()).append(" ").append("(").append(e.getCode()).append("/").append(e.getCode2()).append(")\n"); + nifHeader = true; + } + noImageFound.append(" ").append(imagePath).append("\n"); + missingCount++; + } + } + + // check the back face + if (cp.hasBackFace()) { + imagePath = ImageUtil.getImageRelativePath(cp, true, true, false); + if (imagePath != null) { + File file = ImageKeys.getImageFile(imagePath); + if (file == null && ImageKeys.hasSetLookup(imagePath)) + file = ImageKeys.setLookUpFile(imagePath, imagePath+"border"); + if (file == null) { + if (!nifHeader) { + noImageFound.append("Edition: ").append(e.getName()).append(" ").append("(").append(e.getCode()).append("/").append(e.getCode2()).append(")\n"); + nifHeader = true; + } + noImageFound.append(" ").append(imagePath).append("\n"); + missingCount++; + } + } + } + } + + // TODO: Audit token images here... + for(Map.Entry tokenEntry : e.getTokens().entrySet()) { + String name = tokenEntry.getKey(); + artIndex = tokenEntry.getValue(); + try { + PaperToken token = getAllTokens().getToken(name, e.getCode()); + if (token == null) { + continue; + } + + for(int i = 0; i < artIndex; i++) { + String imgKey = token.getImageKey(i); + File file = ImageKeys.getImageFile(imgKey); + if (file == null) { + if (!nifHeader) { + noImageFound.append("Edition: ").append(e.getName()).append(" ").append("(").append(e.getCode()).append("/").append(e.getCode2()).append(")\n"); + nifHeader = true; + } + if (!tokenHeader) { + noImageFound.append("\nTOKENS\n"); + tokenHeader = true; + } + noImageFound.append(" ").append(token.getImageFilename(i + 1)).append("\n"); + missingCount++; + } + } + } catch(Exception ex) { + System.out.println("No Token found: " + name + " in " + e.getName()); + } + } + if (nifHeader) + noImageFound.append("\n"); + } + + String totalStats = "Missing images: " + missingCount + "\nUnimplemented cards: " + notImplementedCount + "\n"; + cardNotImplemented.append("\n-----------\n"); + cardNotImplemented.append(totalStats); + cardNotImplemented.append("-----------\n\n"); + + noImageFound.append(cardNotImplemented); // combine things together... + return Pair.of(missingCount, notImplementedCount); + } private String prettifyCardArtPreferenceName(CardDb.CardArtPreference preference) { StringBuilder label = new StringBuilder(); diff --git a/forge-core/src/main/java/forge/card/CardType.java b/forge-core/src/main/java/forge/card/CardType.java index b22f2b1f9d0..6082b5f8000 100644 --- a/forge-core/src/main/java/forge/card/CardType.java +++ b/forge-core/src/main/java/forge/card/CardType.java @@ -51,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"), @@ -752,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; @@ -775,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 { 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-core/src/main/java/forge/util/ThreadUtil.java b/forge-core/src/main/java/forge/util/ThreadUtil.java index 87e54edbc4c..c481816ff95 100644 --- a/forge-core/src/main/java/forge/util/ThreadUtil.java +++ b/forge-core/src/main/java/forge/util/ThreadUtil.java @@ -53,6 +53,27 @@ public class ThreadUtil { return Thread.currentThread().getName().startsWith("Game"); } + private static ExecutorService service = Executors.newWorkStealingPool(); + public static ExecutorService getServicePool() { + return service; + } + public static void refreshServicePool() { + service = Executors.newWorkStealingPool(); + } + public static T limit(Callable task, long millis){ + Future future = null; + T result; + try { + future = service.submit(task); + result = future.get(millis, TimeUnit.MILLISECONDS); + } catch (Exception e) { + result = null; + } finally { + if (future != null) + future.cancel(true); + } + return result; + } public static T executeWithTimeout(Callable task, int milliseconds) { ExecutorService executor = Executors.newCachedThreadPool(); Future future = executor.submit(task); diff --git a/forge-game/src/main/java/forge/game/CardTraitBase.java b/forge-game/src/main/java/forge/game/CardTraitBase.java index c10cc1a01d2..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(), @@ -138,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. @@ -667,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/Game.java b/forge-game/src/main/java/forge/game/Game.java index 2c0e6295e0c..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,6 +122,9 @@ 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(); @@ -251,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(); @@ -1088,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(); @@ -1129,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 6b6844288da..a3f9d0598ec 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; @@ -163,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(); @@ -297,6 +314,14 @@ 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. @@ -334,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); + } } } @@ -505,6 +532,7 @@ public class GameAction { } } if (keepKeyword) { + ki.setHostCard(copied); newKw.add(ki); } } @@ -591,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(); @@ -1323,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 @@ -1576,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; } @@ -2327,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); } @@ -2343,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); } } @@ -2364,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()) { @@ -2390,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)) { @@ -2397,6 +2412,9 @@ public class GameAction { } } + // for Zangief do this before runWaitingTriggers DamageDone + damageMap.triggerExcessDamage(isCombat, lethalDamage, game); + // lose life simultaneously if (isCombat) { for (Player p : game.getPlayers()) { @@ -2425,7 +2443,6 @@ public class GameAction { preventMap.clear(); damageMap.triggerDamageDoneOnce(isCombat, game); - damageMap.triggerExcessDamage(isCombat, lethalDamage, game); damageMap.clear(); counterTable.replaceCounterEffect(game, cause, !isCombat); diff --git a/forge-game/src/main/java/forge/game/GameActionUtil.java b/forge-game/src/main/java/forge/game/GameActionUtil.java index 4711d9fb34f..22f49d4fa62 100644 --- a/forge-game/src/main/java/forge/game/GameActionUtil.java +++ b/forge-game/src/main/java/forge/game/GameActionUtil.java @@ -34,6 +34,7 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.CardPlayOption.PayManaCost; import forge.game.cost.Cost; +import forge.game.cost.CostPayment; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; import forge.game.player.Player; @@ -210,7 +211,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()); @@ -843,4 +843,46 @@ public final class GameActionUtil { c.getGame().getTriggerHandler().resetActiveTriggers(); } + public static void rollbackAbility(SpellAbility ability, final Zone fromZone, final int zonePosition, CostPayment payment, Card oldCard) { + // cancel ability during target choosing + final Game game = ability.getActivatingPlayer().getGame(); + + if (fromZone != null) { // and not a copy + oldCard.setCastSA(null); + oldCard.setCastFrom(null); + // add back to where it came from, hopefully old state + // skip GameAction + oldCard.getZone().remove(oldCard); + fromZone.add(oldCard, zonePosition >= 0 ? Integer.valueOf(zonePosition) : null); + ability.setHostCard(oldCard); + ability.setXManaCostPaid(null); + ability.setSpendPhyrexianMana(false); + if (ability.hasParam("Announce")) { + for (final String aVar : ability.getParam("Announce").split(",")) { + final String varName = aVar.trim(); + if (!varName.equals("X")) { + ability.setSVar(varName, "0"); + } + } + } + // better safe than sorry approach in case rolled back ability was copy (from addExtraKeywordCost) + for (SpellAbility sa : oldCard.getSpells()) { + sa.setHostCard(oldCard); + } + //for Chorus of the Conclave + ability.rollback(); + + oldCard.setBackSide(false); + oldCard.setState(oldCard.getFaceupCardStateName(), true); + oldCard.unanimateBestow(); + } + + ability.clearTargets(); + + ability.resetOnceResolved(); + payment.refundPayment(); + game.getStack().clearFrozen(); + game.getTriggerHandler().clearWaitingTriggers(); + } + } 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 2f42eb4db4b..1e81b78a21b 100644 --- a/forge-game/src/main/java/forge/game/GameEntityCounterTable.java +++ b/forge-game/src/main/java/forge/game/GameEntityCounterTable.java @@ -122,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; } @@ -135,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/ability/AbilityFactory.java b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java index cfb843b4e3f..e8b400abf19 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java @@ -277,7 +277,7 @@ public final class AbilityFactory { } } - if (api == ApiType.Charm || api == ApiType.GenericChoice || api == ApiType.AssignGroup) { + if (api == ApiType.Charm || api == ApiType.GenericChoice || api == ApiType.AssignGroup) { final String key = "Choices"; if (mapParams.containsKey(key)) { List names = Lists.newArrayList(mapParams.get(key).split(",")); @@ -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"); @@ -426,7 +426,9 @@ public final class AbilityFactory { private static final void makeRestrictions(final SpellAbility sa) { // SpellAbilityRestrictions should be added in here final SpellAbilityRestriction restrict = sa.getRestrictions(); - restrict.setRestrictions(sa.getMapParams()); + if (restrict != null) { + restrict.setRestrictions(sa.getMapParams()); + } } /** @@ -438,7 +440,7 @@ public final class AbilityFactory { * a {@link forge.game.spellability.SpellAbility} object. */ private static final void makeConditions(final SpellAbility sa) { - // SpellAbilityRestrictions should be added in here + // SpellAbilityConditions should be added in here final SpellAbilityCondition condition = sa.getConditions(); condition.setConditions(sa.getMapParams()); } 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 a3a2ff3681c..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,6 +113,7 @@ public enum AbilityKey { Result("Result"), RoomName("RoomName"), Scheme("Scheme"), + ScryNum("ScryNum"), Sides("Sides"), Source("Source"), Sources("Sources"), 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 18190a19b1c..0884b3598bd 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" @@ -162,17 +163,15 @@ public class AbilityUtils { return cards; } } - else if (defined.equals("Targeted") && sa instanceof SpellAbility) { + else if ((defined.equals("Targeted") || defined.equals("TargetedCard")) && sa instanceof SpellAbility) { for (TargetChoices tc : ((SpellAbility)sa).getAllTargetChoices()) { Iterables.addAll(cards, tc.getTargetCards()); } } else if (defined.equals("TargetedSource") && sa instanceof SpellAbility) { - for (TargetChoices tc : ((SpellAbility)sa).getAllTargetChoices()) { - for (SpellAbility s : tc.getTargetSpells()) { + for (TargetChoices tc : ((SpellAbility)sa).getAllTargetChoices()) for (SpellAbility s : tc.getTargetSpells()) { cards.add(s.getHostCard()); } - } } else if (defined.equals("ThisTargetedCard") && sa instanceof SpellAbility) { // do not add parent targeted if (((SpellAbility)sa).getTargets() != null) { @@ -972,7 +971,7 @@ public class AbilityUtils { final Player player = sa instanceof SpellAbility ? ((SpellAbility)sa).getActivatingPlayer() : card.getController(); - if (defined.equals("Self") || defined.equals("ThisTargetedCard") || defined.startsWith("Valid") || getPaidCards(sa, defined) != null) { + if (defined.equals("Self") || defined.equals("TargetedCard") || defined.equals("ThisTargetedCard") || defined.startsWith("Valid") || getPaidCards(sa, defined) != null) { // do nothing, Self is for Cards, not Players } else if (defined.equals("TargetedOrController")) { players.addAll(getDefinedPlayers(card, "Targeted", sa)); @@ -2030,7 +2029,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")) { @@ -2091,13 +2090,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); } @@ -2359,20 +2351,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")) { @@ -3325,12 +3325,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) { @@ -3387,7 +3381,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++) { 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 9016ad5f03b..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), 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 d79bb6d4502..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 @@ -4,7 +4,6 @@ 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; @@ -122,6 +121,12 @@ public class AnimateAllEffect extends AnimateEffectBase { 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)); @@ -140,10 +145,8 @@ 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 if (!sVarsMap.isEmpty() ) { 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 bc166d03de7..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 @@ -164,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; } } 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/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 1381502eaea..7a7e633dc4a 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 @@ -1,41 +1,18 @@ package forge.game.ability.effects; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import forge.game.*; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; - import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; - -import forge.GameCommand; -import forge.card.CardStateName; import forge.card.CardType; +import forge.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.CardCollectionView; -import forge.game.card.CardLists; -import forge.game.card.CardPredicates; -import forge.game.card.CardState; -import forge.game.card.CardUtil; -import forge.game.card.CardView; -import forge.game.card.CardZoneTable; -import forge.game.card.CounterType; +import forge.game.card.*; import forge.game.event.GameEventCombatChanged; -import forge.game.player.DelayedReveal; -import forge.game.player.Player; -import forge.game.player.PlayerActionConfirmMode; -import forge.game.player.PlayerCollection; -import forge.game.player.PlayerView; +import forge.game.player.*; import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; @@ -43,13 +20,13 @@ import forge.game.spellability.SpellAbilityStackInstance; import forge.game.trigger.TriggerType; import forge.game.zone.Zone; import forge.game.zone.ZoneType; -import forge.util.Aggregates; -import forge.util.CardTranslation; -import forge.util.Lang; -import forge.util.Localizer; -import forge.util.MessageUtil; -import forge.util.TextUtil; +import forge.util.*; import forge.util.collect.FCollectionView; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Map; public class ChangeZoneEffect extends SpellAbilityEffect { @@ -492,7 +469,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; } @@ -511,7 +488,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"); @@ -564,7 +541,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); @@ -689,7 +666,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { // need to be facedown before it hits the battlefield in case of Replacement Effects or Trigger if (sa.hasParam("FaceDown")) { gameCard.turnFaceDown(true); - setFaceDownState(gameCard, sa); + CardFactoryUtil.setFaceDownState(gameCard, sa); } movedCard = game.getAction().moveTo(gameCard.getController().getZone(destination), gameCard, sa, moveParams); @@ -956,7 +933,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; } } @@ -968,7 +945,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; } @@ -985,7 +962,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; } } @@ -1071,7 +1048,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"); @@ -1187,7 +1164,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--; @@ -1345,7 +1322,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { // need to be facedown before it hits the battlefield in case of Replacement Effects or Trigger if (sa.hasParam("FaceDown")) { c.turnFaceDown(true); - setFaceDownState(c, sa); + CardFactoryUtil.setFaceDownState(c, sa); } movedCard = game.getAction().moveToPlay(c, c.getController(), sa, moveParams); @@ -1448,6 +1425,13 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } } } + if (ZoneType.Exile.equals(destination) && sa.hasParam("WithCountersType")) { + CounterType cType = CounterType.getType(sa.getParam("WithCountersType")); + int cAmount = AbilityUtils.calculateAmount(sa.getOriginalHost(), sa.getParamOrDefault("WithCountersAmount", "1"), sa); + GameEntityCounterTable table = new GameEntityCounterTable(); + movedCard.addCounter(cType, cAmount, player, table); + table.replaceCounterEffect(game, sa, true); + } } if (((!ZoneType.Battlefield.equals(destination) && changeType != null && !defined && !changeType.equals("Card")) @@ -1496,39 +1480,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect { && sa.getParam("WithTotalCMC") == null; } - private static void setFaceDownState(Card c, SpellAbility sa) { - final Card source = sa.getHostCard(); - CardState faceDown = c.getFaceDownState(); - - // set New Pt doesn't work because this values need to be copyable for clone effects - if (sa.hasParam("FaceDownPower")) { - faceDown.setBasePower(AbilityUtils.calculateAmount( - source, sa.getParam("FaceDownPower"), sa)); - } - if (sa.hasParam("FaceDownToughness")) { - faceDown.setBaseToughness(AbilityUtils.calculateAmount( - source, sa.getParam("FaceDownToughness"), sa)); - } - - if (sa.hasParam("FaceDownSetType")) { - faceDown.setType(new CardType(Arrays.asList(sa.getParam("FaceDownSetType").split(" & ")), false)); - } - - if (sa.hasParam("FaceDownPower") || sa.hasParam("FaceDownToughness") - || sa.hasParam("FaceDownSetType")) { - final GameCommand unanimate = new GameCommand() { - private static final long serialVersionUID = 8853789549297846163L; - - @Override - public void run() { - c.clearStates(CardStateName.FaceDown, true); - } - }; - - c.addFaceupCommand(unanimate); - } - } - /** *

* removeFromStack. @@ -1543,7 +1494,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); @@ -1555,7 +1506,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 @@ -1587,13 +1537,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..1f5902bc90a 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 @@ -156,6 +156,11 @@ public class CharmEffect extends SpellAbilityEffect { } public static boolean makeChoices(SpellAbility sa) { + // CR 700.2g + if (sa.isCopied()) { + return true; + } + //this resets all previous choices sa.setSubAbility(null); @@ -180,7 +185,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..af29e46cf5e 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 @@ -1,9 +1,8 @@ package forge.game.ability.effects; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; +import com.google.common.collect.Sets; import forge.game.player.DelayedReveal; import forge.game.player.PlayerView; import forge.util.CardTranslation; @@ -119,6 +118,23 @@ public class ChooseCardEffect extends SpellAbilityEffect { } } } + } else if (sa.hasParam("ChooseParty")) { + Set partyTypes = Sets.newHashSet("Cleric", "Rogue", "Warrior", "Wizard"); + for (final String type : partyTypes) { + CardCollection valids = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), + CardPredicates.isType(type)); + for (Card alreadyChosen : chosen) { + valids.remove(alreadyChosen); + } + if (!valids.isEmpty()) { + final String prompt = Localizer.getInstance().getMessage("lblChoose") + " " + + Lang.nounWithNumeralExceptOne(1, type); + Card c = p.getController().chooseSingleEntityForEffect(valids, sa, prompt, true, null); + if (c != null) { + chosen.add(c); + } + } + } } else if (sa.hasParam("WithTotalPower")) { final int totP = AbilityUtils.calculateAmount(host, sa.getParam("WithTotalPower"), sa); CardCollection negativeCreats = CardLists.filterLEPower(p.getCreaturesInPlay(), -1); @@ -131,7 +147,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 { @@ -187,6 +203,18 @@ public class ChooseCardEffect extends SpellAbilityEffect { chosenPool.add(choice); } chosen.addAll(chosenPool); + } else if (sa.hasParam("ControlAndNot")) { + String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChooseCreature"); + // Targeted player (p) chooses N creatures that belongs to them + CardCollection tgtPlayerCtrl = CardLists.filterControlledBy(choices, p); + chosen.addAll(p.getController().chooseCardsForEffect(tgtPlayerCtrl, sa, title + " " + "you control", minAmount, validAmount, + !sa.hasParam("Mandatory"), null)); + // Targeted player (p) chooses N creatures that don't belong to them + CardCollection notTgtPlayerCtrl = new CardCollection(choices); + notTgtPlayerCtrl.removeAll(tgtPlayerCtrl); + chosen.addAll(p.getController().chooseCardsForEffect(notTgtPlayerCtrl, sa, title + " " + "you don't control", minAmount, validAmount, + !sa.hasParam("Mandatory"), null)); + } else if ((tgt == null) || p.canBeTargetedBy(sa)) { if (sa.hasParam("AtRandom") && !choices.isEmpty()) { Aggregates.random(choices, validAmount, chosen); @@ -217,8 +245,13 @@ public class ChooseCardEffect extends SpellAbilityEffect { } } } - if (sa.hasParam("Reveal")) { - game.getAction().reveal(chosen, p, true, Localizer.getInstance().getMessage("lblChosenCards") + " "); + if (sa.hasParam("Reveal") && !sa.hasParam("SecretlyChoose")) { + game.getAction().reveal(chosen, p, true, sa.hasParam("RevealTitle") ? sa.getParam("RevealTitle") : Localizer.getInstance().getMessage("lblChosenCards") + " "); + } + } + if(sa.hasParam("Reveal") && sa.hasParam("SecretlyChoose")) { + for (final Player p : tgtPlayers) { + game.getAction().reveal(chosen, p, true, sa.hasParam("RevealTitle") ? sa.getParam("RevealTitle") : Localizer.getInstance().getMessage("lblChosenCards") + " "); } } host.setChosenCards(chosen); 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/ChooseGenericEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java index 5f95d0c4dd8..c1724471c82 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseGenericEffect.java @@ -50,7 +50,7 @@ public class ChooseGenericEffect extends SpellAbilityEffect { List saToRemove = Lists.newArrayList(); for (SpellAbility saChoice : abilities) { - if (!saChoice.getRestrictions().checkOtherRestrictions(host, saChoice, sa.getActivatingPlayer()) ) { + if (saChoice.getRestrictions() != null && !saChoice.getRestrictions().checkOtherRestrictions(host, saChoice, sa.getActivatingPlayer())) { saToRemove.add(saChoice); } else if (saChoice.hasParam("UnlessCost")) { // generic check for if the cost can be paid 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/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/CopyPermanentEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CopyPermanentEffect.java index bf90e75864b..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; } } 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 4fd4b6d36d2..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 @@ -89,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; } 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/CountersPutEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java index df3ef86ba29..3dec9d6ae4e 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 @@ -67,8 +67,9 @@ public class CountersPutEffect extends SpellAbilityEffect { stringBuilder.append(pronoun ? "they" : who).append(" "); + String desc = sa.getDescription(); + boolean forEach = desc.contains("for each"); if (sa.hasParam("CounterTypes")) { - String desc = sa.getDescription(); if (desc.contains("Put ") && desc.contains(" on ")) { desc = desc.substring(desc.indexOf("Put "), desc.indexOf(" on ") + 4) .replaceFirst("Put ", "puts "); @@ -84,8 +85,8 @@ public class CountersPutEffect extends SpellAbilityEffect { } } - final int amount = AbilityUtils.calculateAmount(card, - sa.getParamOrDefault("CounterNum", "1"), sa); + final String key = forEach ? "ForEachNum" : "CounterNum"; + final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault(key, "1"), sa); if (sa.hasParam("Bolster")) { stringBuilder.append("bolsters ").append(amount).append("."); @@ -155,7 +156,7 @@ public class CountersPutEffect extends SpellAbilityEffect { } } } - stringBuilder.append("."); + stringBuilder.append(forEach ? desc.substring(desc.indexOf(" for each")) : "."); return stringBuilder.toString(); } @@ -174,7 +175,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 +232,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 { @@ -493,7 +496,7 @@ public class CountersPutEffect extends SpellAbilityEffect { // need to unfreeze tracker game.getTracker().unfreeze(); - // check if it can recive the Tribute + // check if it can receive the Tribute if (abort) { continue; } @@ -509,7 +512,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 bb1d424d7f6..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; } } 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 9a8d1caa1a2..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 @@ -44,11 +44,11 @@ public class DigEffect extends SpellAbilityEffect { final int numToChange = toChange.startsWith("All") ? numToDig : AbilityUtils.calculateAmount(host, sa.getParam("ChangeNum"), 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) { + 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 "); @@ -203,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); } @@ -276,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; } } 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..61aa696a247 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 @@ -67,7 +67,7 @@ public class DigUntilEffect extends SpellAbilityEffect { if (revealed.equals(ZoneType.Exile)) { sb.append("and exile all other cards revealed this way."); } - } else { + } else if (revealed != null) { if (revealed.equals(ZoneType.Hand)) { sb.append("all cards revealed this way into their hand"); } @@ -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(); @@ -209,7 +209,9 @@ public class DigUntilEffect extends SpellAbilityEffect { Collections.shuffle(revealed, MyRandom.getRandom()); } - if (sa.hasParam("NoneFoundDestination") && found.size() < untilAmount) { + if (sa.hasParam("NoMoveRevealed")) { + //don't do anything + } else if (sa.hasParam("NoneFoundDestination") && found.size() < untilAmount) { // Allow ordering the revealed cards if (noneFoundDest.isKnown() && revealed.size() >= 2) { revealed = (CardCollection)p.getController().orderMoveToZoneList(revealed, noneFoundDest, sa); 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/EffectEffect.java b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java index a33e668fecd..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 @@ -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(",")) { 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/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/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/PlayEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java index 1ef7a2bfef1..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")) { 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 94c86203432..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 @@ -352,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/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 fb0a99ace99..3a349fdd6ff 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 @@ -1,6 +1,7 @@ package forge.game.ability.effects; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -144,6 +145,9 @@ public class RollDiceEffect extends SpellAbilityEffect { sa.setSVar(sa.getParam("OtherSVar"), Integer.toString(other)); } } + if (sa.hasParam("UseHighestRoll")) { + total = Collections.max(rolls); + } Map diceAbilities = sa.getAdditionalAbilities(); SpellAbility resultAbility = null; 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..820e1ae2a91 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; } } @@ -162,6 +162,10 @@ public class SetStateEffect extends SpellAbilityEffect { hasTransformed = gameCard.turnFaceUp(true, true, sa); } else { hasTransformed = gameCard.changeCardState(mode, sa.getParam("NewState"), sa); + if (gameCard.isFaceDown() && (sa.hasParam("FaceDownPower") || sa.hasParam("FaceDownToughness") + || sa.hasParam("FaceDownSetType"))) { + CardFactoryUtil.setFaceDownState(gameCard, sa); + } } if (hasTransformed) { if (sa.isMorphUp()) { 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/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 dece7177118..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,18 +43,21 @@ public class VentureEffect extends SpellAbilityEffect { } } - Card dungeon = null; + List dungeonCards = null; if (sa.hasParam("Dungeon")) { - dungeon = Card.fromPaperCard(StaticData.instance().getVariantCards().getUniqueByName( - sa.getParam("Dungeon")), player); + 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. - List dungeonCards = StaticData.instance().getVariantCards().getAllCards( + 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"); - dungeon = player.getController().chooseDungeon(player, dungeonCards, message); } + String message = Localizer.getInstance().getMessage("lblChooseDungeon"); + Card dungeon = player.getController().chooseDungeon(player, dungeonCards, message); game.getTriggerHandler().suppressMode(TriggerType.ChangesZone); game.getAction().moveTo(ZoneType.Command, dungeon, sa, moveParams); 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 7c1975016e1..627d0d2a116 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -180,9 +180,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { 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; @@ -257,8 +254,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private String oracleText = ""; - private int damage; + private Map damage = Maps.newHashMap(); private boolean hasBeenDealtDeathtouchDamage; + private boolean hasBeenDealtExcessDamageThisTurn; // regeneration private FCollection shields = new FCollection<>(); @@ -503,7 +501,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()) { - updateTypesForView();; + updateTypesForView(); } updateColorForView(); @@ -951,7 +949,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } devouredCards.add(c); } - public final void clearDevoured() { devouredCards = null; } @@ -982,7 +979,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { delvedCards = null; } - public final CardCollectionView getConvoked() { return CardCollection.getView(convokedCards); } @@ -4463,6 +4459,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); @@ -4472,6 +4469,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 @@ -5292,62 +5295,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; } /** @@ -5356,11 +5312,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 @@ -5383,15 +5335,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; } @@ -5399,6 +5362,13 @@ public class Card extends GameEntity implements Comparable, IHasSVars { this.hasBeenDealtDeathtouchDamage = hasBeenDealtDeatchtouchDamage; } + public final boolean hasBeenDealtExcessDamageThisTurn() { + return hasBeenDealtExcessDamageThisTurn; + } + public final void setHasBeenDealtExcessDamageThisTurn(final boolean bool) { + this.hasBeenDealtExcessDamageThisTurn = bool; + } + public final Map getAssignedDamageMap() { return assignedDamageMap; } @@ -5542,14 +5512,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); @@ -5561,7 +5523,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } // Defending player at the time the damage was dealt runParams.put(AbilityKey.DefendingPlayer, game.getCombat() != null ? game.getCombat().getDefendingPlayerRelatedTo(source) : null); - getGame().getTriggerHandler().runTrigger(TriggerType.DamageDone, runParams, false); + getGame().getTriggerHandler().runTrigger(TriggerType.DamageDone, runParams, true); DamageType damageType = DamageType.Normal; if (isPlaneswalker()) { // 120.3c @@ -5576,7 +5538,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); } @@ -5714,7 +5677,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } return false; } - public final void setForetold(final boolean foretold) { this.foretold = foretold; } @@ -5732,7 +5694,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public final void setForetoldThisTurn(final boolean foretoldThisTurn) { this.foretoldThisTurn = foretoldThisTurn; } - public void resetForetoldThisTurn() { foretoldThisTurn = false; } @@ -5803,7 +5764,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public final long getBestowTimestamp() { return bestowTimestamp; } - public final void setBestowTimestamp(final long t) { bestowTimestamp = t; } @@ -6206,7 +6166,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { setDamage(0); } setHasBeenDealtDeathtouchDamage(false); - resetReceivedDamageFromThisTurn(); + setHasBeenDealtExcessDamageThisTurn(false); setRegeneratedThisTurn(0); resetShield(); setBecameTargetThisTurn(false); @@ -6214,6 +6174,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 9337293bedc..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,9 +4,11 @@ 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; @@ -22,7 +24,6 @@ public class CardDamageHistory { private boolean creatureBlockedThisCombat = false; private boolean creatureGotBlockedThisCombat = false; - boolean hasdealtDamagetoAny = false; private List attackedThisTurn = Lists.newArrayList(); private final List creatureAttackedLastTurnOf = Lists.newArrayList(); @@ -30,14 +31,13 @@ public class CardDamageHistory { private final List NotBlockedSinceLastUpkeepOf = Lists.newArrayList(); private final List NotBeenBlockedSinceLastUpkeepOf = Lists.newArrayList(); + 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<>(); - - private final Map damagedThisTurn = Maps.newHashMap(); - private final Map damagedThisTurnInCombat = Maps.newHashMap(); - private boolean receivedNonCombatDamageThisTurn = false; + boolean hasdealtDamagetoAny = false; public final boolean getHasdealtDamagetoAny() { return hasdealtDamagetoAny; @@ -225,38 +225,9 @@ 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 List getThisCombatDamaged() { return damagedThisCombat; } - public final Map getThisTurnDamaged() { - return damagedThisTurn; - } - public final Map getThisTurnCombatDamaged() { - return damagedThisTurnInCombat; - } public final FCollection getThisGameDamaged() { return damagedThisGame; } @@ -264,38 +235,44 @@ public class CardDamageHistory { * TODO: Write javadoc for this method. * @param player */ - public void registerCombatDamage(GameEntity entity, int amount) { - int old = 0; - if (entity instanceof Player) { - damagedThisCombat.add((Player) entity); - } - old = 0; - if (damagedThisTurnInCombat.containsKey(entity)) { - old = damagedThisTurnInCombat.get(entity); - } - damagedThisTurnInCombat.put(entity, old + amount); + 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); + } + 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. - * @param player - */ - public void registerDamage(GameEntity entity, int amount) { - int old = 0; - if (damagedThisTurn.containsKey(entity)) { - old = damagedThisTurn.get(entity); + 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; + } } - damagedThisTurn.put(entity, old + amount); - damagedThisGame.add(entity); - hasdealtDamagetoAny = true; + return sum; } public void newTurn() { - damagedThisCombat.clear(); - damagedThisTurnInCombat.clear(); - damagedThisTurn.clear(); attackedThisTurn.clear(); + 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(); @@ -307,7 +284,6 @@ public class CardDamageHistory { } } damagedThisGame.removeAll(toRemove); - setHasBeenDealtNonCombatDamageThisTurn(false); } public void endCombat() { 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 ad93f326739..37dd9c620b8 100644 --- a/forge-game/src/main/java/forge/game/card/CardDamageMap.java +++ b/forge-game/src/main/java/forge/game/card/CardDamageMap.java @@ -113,6 +113,7 @@ public class CardDamageMap extends ForwardingTable { int excess = sum - (damaged.getKey().hasBeenDealtDeathtouchDamage() ? 1 : damaged.getValue()); if (excess > 0) { + damaged.getKey().setHasBeenDealtExcessDamageThisTurn(true); // Run triggers final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.DamageTarget, damaged.getKey()); 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 6ab7afbd927..c99ec287ff7 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import forge.GameCommand; import forge.game.event.GameEventCardForetold; import forge.game.trigger.TriggerType; import org.apache.commons.lang3.StringUtils; @@ -1147,29 +1148,21 @@ public class CardFactoryUtil { + " | ValidCard$ Card.Self | Secondary$ True" + " | TriggerDescription$ Fabricate " + n + " (" + inst.getReminderText() + ")"; - final String choose = "DB$ GenericChoice | AILogic$ " + name; - final String counter = "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ " + n + - " | IsPresent$ Card.StrictlySelf | SpellDescription$ Put " - + Lang.nounWithNumeral(n, "+1/+1 counter") + " on it."; - final String token = "DB$ Token | TokenAmount$ " + n + " | TokenScript$ c_1_1_a_servo | TokenOwner$ You " - + " | SpellDescription$ Create " + final String token = "DB$ Token | TokenAmount$ " + n + " | TokenScript$ c_1_1_a_servo" + + " | UnlessCost$ AddCounter<" + n + "/P1P1> | UnlessPayer$ You | UnlessAI$ " + name + + " | SpellDescription$ Fabricate - Create " + Lang.nounWithNumeral(n, "1/1 colorless Servo artifact creature token") + "."; final Trigger trigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic); - SpellAbility saChoose = AbilityFactory.getAbility(choose, card); - - List list = Lists.newArrayList(); - list.add((AbilitySub)AbilityFactory.getAbility(counter, card)); - list.add((AbilitySub)AbilityFactory.getAbility(token, card)); - saChoose.setAdditionalAbilityList("Choices", list); + SpellAbility saChoose = AbilityFactory.getAbility(token, card); saChoose.setIntrinsic(intrinsic); trigger.setOverridingAbility(saChoose); inst.addTrigger(trigger); } else if (keyword.startsWith("Fading")) { - String upkeepTrig = "Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Secondary$ True " + + String upkeepTrig = "Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Secondary$ True" + " | TriggerDescription$ At the beginning of your upkeep, remove a fade counter from CARDNAME. If you can't, sacrifice CARDNAME."; final String removeCounterStr = "DB$ RemoveCounter | Defined$ Self | CounterType$ FADE | CounterNum$ 1 | RememberRemoved$ True"; @@ -1680,7 +1673,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"; @@ -1893,7 +1886,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)); @@ -2323,12 +2316,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); @@ -2795,7 +2787,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")); @@ -3438,9 +3441,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); @@ -3514,7 +3529,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."; @@ -3726,4 +3741,37 @@ public class CardFactoryUtil { re.setOverridingAbility(saExile); card.addReplacementEffect(re); } + + public static void setFaceDownState(Card c, SpellAbility sa) { + final Card source = sa.getHostCard(); + CardState faceDown = c.getFaceDownState(); + + // set New Pt doesn't work because this values need to be copyable for clone effects + if (sa.hasParam("FaceDownPower")) { + faceDown.setBasePower(AbilityUtils.calculateAmount( + source, sa.getParam("FaceDownPower"), sa)); + } + if (sa.hasParam("FaceDownToughness")) { + faceDown.setBaseToughness(AbilityUtils.calculateAmount( + source, sa.getParam("FaceDownToughness"), sa)); + } + + if (sa.hasParam("FaceDownSetType")) { + faceDown.setType(new CardType(Arrays.asList(sa.getParam("FaceDownSetType").split(" & ")), false)); + } + + if (sa.hasParam("FaceDownPower") || sa.hasParam("FaceDownToughness") + || sa.hasParam("FaceDownSetType")) { + final GameCommand unanimate = new GameCommand() { + private static final long serialVersionUID = 8853789549297846163L; + + @Override + public void run() { + c.clearStates(CardStateName.FaceDown, true); + } + }; + + c.addFaceupCommand(unanimate); + } + } } 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 562c4bf0fa2..e3351576672 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 { @@ -255,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; @@ -620,36 +607,7 @@ public class CardProperty { return false; } } else if (property.startsWith("Cloned")) { - 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)) { + if (card.getCloneOrigin() == null || !card.getCloneOrigin().equals(source)) { return false; } } else if (property.startsWith("SharesCMCWith")) { @@ -1148,38 +1106,82 @@ 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") ? - Lists.newArrayList(card.getDamageHistory().getThisCombatDamaged()) : - 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("wasDealtExcessDamageThisTurn")) { + if (!card.hasBeenDealtExcessDamageThisTurn()) { return false; } } else if (property.startsWith("wasDealtDamageByThisGame")) { @@ -1477,6 +1479,14 @@ public class CardProperty { if (!card.getManaCost().getShortString().equals(property.substring(8))) { return false; } + } else if (property.equals("HasCounters")) { + if (!card.hasCounters()) { + return false; + } + } else if (property.equals("NoCounters")) { + if (card.hasCounters()) { + return false; + } } // syntax example: countersGE9 P1P1 or countersLT12TIME (greater number @@ -1692,6 +1702,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); @@ -1804,13 +1819,20 @@ public class CardProperty { if (!card.hasNoAbilities()) { return false; } - } else if (property.equals("HasCounters")) { - if (!card.hasCounters()) { + } else if (property.equals("castKeyword")) { + SpellAbility castSA = card.getCastSA(); + if (castSA == null) { return false; } - } else if (property.equals("NoCounters")) { - if (card.hasCounters()) { - 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(); 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/card/CardZoneTable.java b/forge-game/src/main/java/forge/game/card/CardZoneTable.java index f0e9bb8d387..12b09e2e65a 100644 --- a/forge-game/src/main/java/forge/game/card/CardZoneTable.java +++ b/forge-game/src/main/java/forge/game/card/CardZoneTable.java @@ -56,7 +56,7 @@ public class CardZoneTable extends ForwardingTable runParams = AbilityKey.newMap(); runParams.put(AbilityKey.Cards, new CardZoneTable(this)); runParams.put(AbilityKey.Cause, cause); - game.getTriggerHandler().runTrigger(TriggerType.ChangesZoneAll, AbilityKey.newMap(runParams), false); + game.getTriggerHandler().runTrigger(TriggerType.ChangesZoneAll, runParams, false); } } 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 f8d8de99028..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; @@ -724,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); @@ -797,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; @@ -815,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); } @@ -826,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/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/CostPutCounter.java b/forge-game/src/main/java/forge/game/cost/CostPutCounter.java index 8b8ed940eb5..ca4d4fd2b45 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPutCounter.java +++ b/forge-game/src/main/java/forge/game/cost/CostPutCounter.java @@ -141,16 +141,16 @@ public class CostPutCounter extends CostPartWithList { public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) { final Card source = ability.getHostCard(); if (this.payCostFromSource()) { - return source.canReceiveCounters(this.counter); - } else { - // 3 Cards have Put a -1/-1 Counter on a Creature you control. - List typeList = CardLists.getValidCards(source.getGame().getCardsIn(ZoneType.Battlefield), - this.getType().split(";"), payer, source, ability); - - typeList = CardLists.filter(typeList, CardPredicates.canReceiveCounters(this.counter)); - - return !typeList.isEmpty(); + return source.isInPlay() && source.canReceiveCounters(this.counter); } + + // 3 Cards have Put a -1/-1 Counter on a Creature you control. + List typeList = CardLists.getValidCards(source.getGame().getCardsIn(ZoneType.Battlefield), + this.getType().split(";"), payer, source, ability); + + typeList = CardLists.filter(typeList, CardPredicates.canReceiveCounters(this.counter)); + + return !typeList.isEmpty(); } /* 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 0dd761bb918..b267caa6292 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -836,7 +836,6 @@ public class PhaseHandler implements java.io.Serializable { game.getStack().onNextTurn(); game.getTriggerHandler().clearThisTurnDelayedTrigger(); - game.getTriggerHandler().resetTurnTriggerState(); Player next = getNextActivePlayer(); while (next.hasLost()) { @@ -1064,7 +1063,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 6689bcdd50f..e6c7dc253a7 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -147,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; @@ -219,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; @@ -704,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); @@ -830,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. */ @@ -1662,6 +1603,12 @@ public class Player extends GameEntity implements Comparable { } } + // MilledAll trigger + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Cards, milled); + runParams.put(AbilityKey.Player, this); + game.getTriggerHandler().runTrigger(TriggerType.MilledAll, runParams, false); + return milled; } @@ -2082,8 +2029,8 @@ public class Player extends GameEntity implements Comparable { } 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; } } @@ -2102,14 +2049,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) { @@ -2150,7 +2095,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; @@ -2239,10 +2184,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()); @@ -2260,6 +2201,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(); } @@ -2452,10 +2396,8 @@ public class Player extends GameEntity implements Comparable { resetCycledThisTurn(); resetEquippedThisTurn(); resetSacrificedThisTurn(); - clearAssignedDamage(); resetVenturedThisTurn(); setRevolt(false); - resetProwl(); setSpellsCastLastTurn(getSpellsCastThisTurn()); resetSpellsCastThisTurn(); setLifeLostLastTurn(getLifeLostThisTurn()); @@ -2467,6 +2409,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); 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 f9b591391c7..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; @@ -86,81 +89,66 @@ public class PlayerProperty { 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().contains(player)) { - found++; + 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().contains(player)) { - found++; + 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")) { @@ -171,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; @@ -243,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; } 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/ReplacementHandler.java b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java index 7ce788fd936..030e8cd3879 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java @@ -44,6 +44,7 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.Card; import forge.game.card.CardCollection; +import forge.game.card.CardCollectionView; import forge.game.card.CardDamageMap; import forge.game.card.CardState; import forge.game.card.CardTraitChanges; @@ -175,9 +176,17 @@ public class ReplacementHandler { final Card c = preList.get(crd); for (final ReplacementEffect replacementEffect : c.getReplacementEffects()) { - // Use "CheckLKIZone" parameter to test for effects that care abut where the card was last (e.g. Kalitas, Traitor of Ghet + Zone cardZone; + // Use "CheckLKIZone" parameter to test for effects that care about where the card was last (e.g. Kalitas, Traitor of Ghet // getting hit by mass removal should still produce tokens). - Zone cardZone = "True".equals(replacementEffect.getParam("CheckSelfLKIZone")) ? game.getChangeZoneLKIInfo(c).getLastKnownZone() : game.getZoneOf(c); + if ("True".equals(replacementEffect.getParam("CheckSelfLKIZone"))) { + cardZone = game.getChangeZoneLKIInfo(c).getLastKnownZone(); + if (cardZone.is(ZoneType.Battlefield) && runParams.containsKey(AbilityKey.LastStateBattlefield) && !((CardCollectionView) runParams.get(AbilityKey.LastStateBattlefield)).contains(crd)) { + continue; + } + } else { + cardZone = game.getZoneOf(c); + } // Replacement effects that are tied to keywords (e.g. damage prevention effects - if the keyword is removed, the replacement // effect should be inactive) @@ -351,8 +360,8 @@ public class ReplacementHandler { tailend = tailend.getSubAbility(); } while(tailend != null); - effectSA.setLastStateBattlefield(game.getLastStateBattlefield()); - effectSA.setLastStateGraveyard(game.getLastStateGraveyard()); + effectSA.setLastStateBattlefield((CardCollectionView) runParams.getOrDefault(AbilityKey.LastStateBattlefield, game.getLastStateBattlefield())); + effectSA.setLastStateGraveyard((CardCollectionView) runParams.getOrDefault(AbilityKey.LastStateBattlefield, game.getLastStateGraveyard())); if (replacementEffect.isIntrinsic()) { effectSA.setIntrinsic(true); effectSA.changeText(); 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 c767b4e36c9..abb22e116ea 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"); @@ -177,7 +179,7 @@ public class AbilityManaPart implements java.io.Serializable { if (source.isLand() && root.isManaAbility() && root.getPayCosts() != null && root.getPayCosts().hasTapCost()) { player.setTappedLandForManaThisTurn(true); } - } // end produceMana(String) + } /** *

@@ -339,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 a19859c37c4..1cdb04d8176 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -138,7 +138,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit /** The pay costs. */ private Cost payCosts; - private SpellAbilityRestriction restrictions = new SpellAbilityRestriction(); + private SpellAbilityRestriction restrictions; private SpellAbilityCondition conditions = new SpellAbilityCondition(); private AbilitySub subAbility; @@ -214,6 +214,9 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit view0 = new SpellAbilityView(this); } view = view0; + if (!(this instanceof AbilitySub)) { + restrictions = new SpellAbilityRestriction(); + } } @Override @@ -638,7 +641,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 +1100,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 +1159,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 +2048,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 +2101,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 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..583830f2dc0 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; } } @@ -449,7 +443,7 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { if (getPresentDefined() != null) { list = AbilityUtils.getDefinedObjects(sa.getHostCard(), getPresentDefined(), sa); } else { - list = new FCollection(game.getCardsIn(getPresentZone())); + list = new FCollection<>(game.getCardsIn(getPresentZone())); } final int left = Iterables.size(Iterables.filter(list, GameObjectPredicates.restriction(getIsPresent().split(","), activator, c, sa))); @@ -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/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/StaticAbilityContinuous.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java index 1a32a81b354..2fd7291fa80 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java @@ -111,12 +111,18 @@ public final class StaticAbilityContinuous { final List affectedPlayers = StaticAbilityContinuous.getAffectedPlayers(stAb); final Game game = hostCard.getGame(); - final StaticEffect se = game.getStaticEffects().getStaticEffect(stAb); + final StaticEffects effects = game.getStaticEffects(); + final StaticEffect se = effects.getStaticEffect(stAb); se.setAffectedCards(affectedCards); se.setAffectedPlayers(affectedPlayers); se.setParams(params); se.setTimestamp(hostCard.getTimestamp()); + // nothing more to do + if (stAb.hasParam("Affected") && affectedPlayers.isEmpty() && affectedCards.isEmpty()) { + return affectedCards; + } + String addP = ""; int powerBonus = 0; String addT = ""; @@ -161,7 +167,6 @@ public final class StaticAbilityContinuous { //Global rules changes if (layer == StaticAbilityLayer.RULES && params.containsKey("GlobalRule")) { - final StaticEffects effects = game.getStaticEffects(); effects.setGlobalRuleChange(GlobalRuleChange.fromString(params.get("GlobalRule"))); } @@ -197,8 +202,6 @@ public final class StaticAbilityContinuous { // update keywords with Chosen parts final String hostCardUID = Integer.toString(hostCard.getId()); // Protection with "doesn't remove" effect - final ColorSet colorsYouCtrl = CardUtil.getColorsYouCtrl(controller); - Iterables.removeIf(addKeywords, new Predicate() { @Override public boolean apply(String input) { @@ -246,6 +249,8 @@ public final class StaticAbilityContinuous { } // two variants for Red vs. red in keyword if (input.contains("ColorsYouCtrl") || input.contains("colorsYouCtrl")) { + final ColorSet colorsYouCtrl = CardUtil.getColorsYouCtrl(controller); + for (byte color : colorsYouCtrl) { final String colorWord = MagicColor.toLongString(color); String y = input.replaceAll("ColorsYouCtrl", StringUtils.capitalize(colorWord)); @@ -718,6 +723,7 @@ public final class StaticAbilityContinuous { // add P/T bonus if (layer == StaticAbilityLayer.MODIFYPT) { if (addP.contains("Affected")) { + // TODO don't calculate these above if this gets used instead powerBonus = AbilityUtils.calculateAmount(affectedCard, addP, stAb, true); } if (addT.contains("Affected")) { @@ -843,7 +849,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 5b26b7adcbe..d736589e14d 100644 --- a/forge-game/src/main/java/forge/game/trigger/Trigger.java +++ b/forge-game/src/main/java/forge/game/trigger/Trigger.java @@ -77,10 +77,6 @@ public abstract class Trigger extends TriggerReplacementBase { private List triggerRemembered = Lists.newArrayList(); - // number of times this trigger was activated this this turn - // used to handle once-per-turn triggers like Crawling Sensation - private int numberTurnActivations = 0; - private Set validPhases; private SpellAbility spawningAbility; @@ -536,16 +532,13 @@ public abstract class Trigger extends TriggerReplacementBase { } public int getActivationsThisTurn() { - return this.numberTurnActivations; + return hostCard.getAbilityActivatedThisTurn(this.getOverridingAbility()); } public void triggerRun() { - this.numberTurnActivations++; - } - - // Resets the state stored each turn for per-turn and per-instance restriction - public void resetTurnState() { - this.numberTurnActivations = 0; + if (this.getOverridingAbility() != null) { + hostCard.addAbilityActivated(this.getOverridingAbility()); + } } /** {@inheritDoc} */ 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/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/TriggerHandler.java b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java index 4e1bed39fab..b3abf325181 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); } } @@ -345,15 +346,6 @@ public class TriggerHandler { waitingTriggers.clear(); } - public void resetTurnTriggerState() { - for (final Trigger t : activeTriggers) { - t.resetTurnState(); - } - for (final Trigger t : delayedTriggers) { - t.resetTurnState(); - } - } - private boolean runNonStaticTriggersForPlayer(final Player player, final TriggerWaiting wt, final List delayedTriggersWorkingCopy) { final TriggerType mode = wt.getMode(); final Map runParams = wt.getParams(); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerMilledAll.java b/forge-game/src/main/java/forge/game/trigger/TriggerMilledAll.java new file mode 100644 index 00000000000..fa2fd2ea73c --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerMilledAll.java @@ -0,0 +1,71 @@ +package forge.game.trigger; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.card.CardLists; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +import java.util.Map; + +/** + *

+ * TriggerMilledAll class. + *

+ */ +public class TriggerMilledAll extends Trigger { + + /** + *

+ * Constructor for TriggerMilledAll + *

+ * + * @param params + * a {@link java.util.HashMap} object. + * @param host + * a {@link forge.game.card.Card} object. + * @param intrinsic + * the intrinsic + */ + public TriggerMilledAll (final Map params, final Card host, final boolean intrinsic) { + super(params, host, intrinsic); + } + + /** {@inheritDoc} + * @param runParams*/ + @Override + public final boolean performTest(final Map runParams) { + + if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) { + return false; + } + if (!matchesValidParam("ValidCard", runParams.get(AbilityKey.Cards))) { + return false; + } + + return true; + } + + /** {@inheritDoc} */ + @Override + public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { + CardCollection cards = (CardCollection) runParams.get(AbilityKey.Cards); + + if (hasParam("ValidCard")) { + cards = CardLists.getValidCards(cards, getParam("ValidCard"), getHostCard().getController(), + getHostCard(), this); + } + + sa.setTriggeringObject(AbilityKey.Cards, cards); + 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)); + return sb.toString(); + } +} 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..05a90284011 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerScry.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerScry.java @@ -64,13 +64,15 @@ public class TriggerScry extends Trigger { /** {@inheritDoc} */ @Override public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { - sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player); + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player, 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/TriggerType.java b/forge-game/src/main/java/forge/game/trigger/TriggerType.java index 4f7c94c9ba8..2490cb3b9aa 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerType.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerType.java @@ -80,6 +80,7 @@ public enum TriggerType { LifeGained(TriggerLifeGained.class), LifeLost(TriggerLifeLost.class), LosesGame(TriggerLosesGame.class), + MilledAll(TriggerMilledAll.class), Mutates(TriggerMutates.class), NewGame(TriggerNewGame.class), PayCumulativeUpkeep(TriggerPayCumulativeUpkeep.class), diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index 48efbd76acd..4fc81955212 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -41,7 +41,6 @@ import forge.game.GameObject; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; -import forge.game.ability.effects.CharmEffect; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardUtil; @@ -826,31 +825,18 @@ public class MagicStack /* extends MyObservable */ implements Iterable activePlayerSAs = Lists.newArrayList(); - final List failedSAs = Lists.newArrayList(); for (int i = 0; i < simultaneousStackEntryList.size(); i++) { SpellAbility sa = simultaneousStackEntryList.get(i); Player activator = sa.getActivatingPlayer(); - if (sa.getApi() == ApiType.Charm) { - if (!CharmEffect.makeChoices(sa)) { - // 603.3c If no mode is chosen, the ability is removed from the stack. - failedSAs.add(sa); - continue; - } - } - if (activator == null) { - if (sa.getHostCard().getController().equals(activePlayer)) { - activePlayerSAs.add(sa); - } - } else { - if (activator.equals(activePlayer)) { - activePlayerSAs.add(sa); - } + activator = sa.getHostCard().getController(); + } + if (activator.equals(activePlayer)) { + activePlayerSAs.add(sa); } } simultaneousStackEntryList.removeAll(activePlayerSAs); - simultaneousStackEntryList.removeAll(failedSAs); if (activePlayerSAs.isEmpty()) { return false; 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/VSubmenuDownloaders.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuDownloaders.java index 369aaac808b..0aea46f26fd 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuDownloaders.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuDownloaders.java @@ -7,9 +7,6 @@ import java.awt.Toolkit; import java.awt.datatransfer.StringSelection; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.io.File; -import java.util.HashMap; -import java.util.Map.Entry; import javax.swing.BorderFactory; import javax.swing.JPanel; @@ -17,24 +14,17 @@ import javax.swing.ScrollPaneConstants; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; -import forge.ImageKeys; import forge.StaticData; -import forge.card.CardDb; -import forge.card.CardEdition; -import forge.card.CardEdition.CardInSet; import forge.gui.SOverlayUtils; import forge.gui.UiCommand; import forge.gui.framework.DragCell; import forge.gui.framework.DragTab; import forge.gui.framework.EDocID; -import forge.item.PaperCard; -import forge.item.PaperToken; import forge.localinstance.properties.ForgeConstants; import forge.localinstance.skin.FSkinProp; import forge.screens.home.EMenuGroup; import forge.screens.home.IVSubmenu; import forge.screens.home.VHomeUI; -import forge.token.TokenDb; import forge.toolbox.FButton; import forge.toolbox.FLabel; import forge.toolbox.FOverlay; @@ -43,7 +33,6 @@ import forge.toolbox.FScrollPane; import forge.toolbox.FSkin; import forge.toolbox.FTextArea; import forge.util.FileUtil; -import forge.util.ImageUtil; import forge.util.Localizer; import forge.util.RuntimeVersion; import net.miginfocom.swing.MigLayout; @@ -232,17 +221,8 @@ public enum VSubmenuDownloaders implements IVSubmenu { * @param scr */ public void auditUpdate(FTextArea tar, FScrollPane scr) { - // Get top-level Forge objects - CardDb cardDb = StaticData.instance().getCommonCards(); - CardDb variantDb = StaticData.instance().getVariantCards(); - TokenDb tokenDb = StaticData.instance().getAllTokens(); - CardEdition.Collection editions = StaticData.instance().getEditions(); - - int missingCount = 0; - int notImplementedCount = 0; - - final StringBuffer nifSB = new StringBuffer(); // NO IMAGE FOUND BUFFER - final StringBuffer cniSB = new StringBuffer(); // CARD NOT IMPLEMENTED BUFFER + StringBuffer nifSB = new StringBuffer(); // NO IMAGE FOUND BUFFER + StringBuffer cniSB = new StringBuffer(); // CARD NOT IMPLEMENTED BUFFER nifSB.append("\n\n-------------------\n"); nifSB.append("NO IMAGE FOUND LIST\n"); @@ -252,118 +232,7 @@ public enum VSubmenuDownloaders implements IVSubmenu { cniSB.append("UNIMPLEMENTED CARD LIST\n"); cniSB.append("-------------------\n\n"); - for (CardEdition e : editions) { - if (CardEdition.Type.FUNNY.equals(e.getType())) - continue; - boolean nifHeader = false; - boolean cniHeader = false; - boolean tokenHeader = false; - - String imagePath; - int artIndex = 1; - - HashMap> cardCount = new HashMap<>(); - for (CardInSet c : e.getAllCardsInSet()) { - if (cardCount.containsKey(c.name)) { - cardCount.put(c.name, Pair.of(c.collectorNumber.startsWith("F"), cardCount.get(c.name).getRight() + 1)); - } else { - cardCount.put(c.name, Pair.of(c.collectorNumber.startsWith("F"), 1)); - } - } - - // loop through the cards in this edition, considering art variations... - for (Entry> entry : cardCount.entrySet()) { - String c = entry.getKey(); - artIndex = entry.getValue().getRight(); - - PaperCard cp = cardDb.getCard(c, e.getCode(), artIndex); - if (cp == null) { - cp = variantDb.getCard(c, e.getCode(), artIndex); - } - - if (cp == null) { - if (entry.getValue().getLeft()) //skip funny cards - continue; - if (!cniHeader) { - cniSB.append("Edition: ").append(e.getName()).append(" ").append("(").append(e.getCode()).append("/").append(e.getCode2()).append(")\n"); - cniHeader = true; - } - cniSB.append(" ").append(c).append("\n"); - notImplementedCount++; - continue; - } - - // check the front image - imagePath = ImageUtil.getImageRelativePath(cp, false, true, false); - if (imagePath != null) { - File file = ImageKeys.getImageFile(imagePath); - if (file == null) { - if (!nifHeader) { - nifSB.append("Edition: ").append(e.getName()).append(" ").append("(").append(e.getCode()).append("/").append(e.getCode2()).append(")\n"); - nifHeader = true; - } - nifSB.append(" ").append(imagePath).append("\n"); - missingCount++; - } - } - - // check the back face - if (cp.hasBackFace()) { - imagePath = ImageUtil.getImageRelativePath(cp, true, true, false); - if (imagePath != null) { - File file = ImageKeys.getImageFile(imagePath); - if (file == null) { - if (!nifHeader) { - nifSB.append("Edition: ").append(e.getName()).append(" ").append("(").append(e.getCode()).append("/").append(e.getCode2()).append(")\n"); - nifHeader = true; - } - nifSB.append(" ").append(imagePath).append("\n"); - missingCount++; - } - } - } - } - - // TODO: Audit token images here... - for(Entry tokenEntry : e.getTokens().entrySet()) { - String name = tokenEntry.getKey(); - artIndex = tokenEntry.getValue(); - try { - PaperToken token = tokenDb.getToken(name, e.getCode()); - if (token == null) { - continue; - } - - for(int i = 0; i < artIndex; i++) { - String imgKey = token.getImageKey(i); - File file = ImageKeys.getImageFile(imgKey); - if (file == null) { - if (!nifHeader) { - nifSB.append("Edition: ").append(e.getName()).append(" ").append("(").append(e.getCode()).append("/").append(e.getCode2()).append(")\n"); - nifHeader = true; - } - if (!tokenHeader) { - nifSB.append("\nTOKENS\n"); - tokenHeader = true; - } - nifSB.append(" ").append(token.getImageFilename(i + 1)).append("\n"); - missingCount++; - } - } - } catch(Exception ex) { - System.out.println("No Token found: " + name + " in " + e.getName()); - } - } - if (nifHeader) - nifSB.append("\n"); - } - - String totalStats = "Missing images: " + missingCount + "\nUnimplemented cards: " + notImplementedCount + "\n"; - cniSB.append("\n-----------\n"); - cniSB.append(totalStats); - cniSB.append("-----------\n\n"); - - nifSB.append(cniSB); // combine things together... + Pair totalAudit = StaticData.instance().audit(nifSB, cniSB); tar.setText(nifSB.toString()); tar.setCaretPosition(0); // this will move scroll view to the top... @@ -378,7 +247,7 @@ public enum VSubmenuDownloaders implements IVSubmenu { }); scr.getParent().add(btnClipboardCopy, "w 200!, h pref+12!, center, gaptop 10"); - String labelText = "Missing images: " + missingCount + "
Unimplemented cards: " + notImplementedCount + "
"; + String labelText = "Missing images: " + totalAudit.getLeft() + "
Unimplemented cards: " + totalAudit.getRight() + "
"; final FLabel statsLabel = new FLabel.Builder().text(labelText).fontSize(15).build(); scr.getParent().add(statsLabel); 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 e67b9483cec..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 @@ -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-mobile/src/forge/Forge.java b/forge-gui-mobile/src/forge/Forge.java index 6c264b57984..7a692d8cb6e 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; @@ -54,9 +55,7 @@ import java.io.File; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; -import java.util.HashMap; import java.util.List; -import java.util.Map; public class Forge implements ApplicationListener { public static final String CURRENT_VERSION = "1.6.53.001"; @@ -80,6 +79,7 @@ public class Forge implements ApplicationListener { protected static TransitionScreen transitionScreen; public static KeyInputAdapter keyInputAdapter; private static boolean exited; + private boolean needsUpdate = false; public static boolean safeToClose = true; public static boolean magnify = false; public static boolean magnifyToggle = true; @@ -103,6 +103,7 @@ public class Forge implements ApplicationListener { public static boolean enablePreloadExtendedArt = false; public static boolean isTabletDevice = false; public static String locale = "en-US"; + public Assets assets; public static boolean hdbuttons = false; public static boolean hdstart = false; public static boolean isPortraitMode = false; @@ -123,7 +124,6 @@ public class Forge implements ApplicationListener { public static boolean forcedEnglishonCJKMissing = false; public static boolean adventureLoaded = false; private static Localizer localizer; - static Map misc = new HashMap<>(); public static ApplicationListener getApp(Clipboard clipboard0, IDeviceAdapter deviceAdapter0, String assetDir0, boolean value, boolean androidOrientation, int totalRAM, boolean isTablet, int AndroidAPI, String AndroidRelease, String deviceName) { app = new Forge(); @@ -163,7 +163,7 @@ public class Forge implements ApplicationListener { // don't allow to read and process ForgeConstants.SPRITE_CARDBG_FILE = ""; } - + assets = new Assets(); graphics = new Graphics(); splashScreen = new SplashScreen(); frameRate = new FrameRate(); @@ -216,50 +216,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(); + }); }); } @@ -347,22 +341,6 @@ public class Forge implements ApplicationListener { e.printStackTrace(); } } - public static Texture getTitleBG() { - if (misc.get(0) == null) { - misc.put(0, new Texture(GuiBase.isAndroid() - ? Gdx.files.internal("fallback_skin").child("title_bg_lq.png") - : Gdx.files.classpath("fallback_skin").child("title_bg_lq.png"))); - } - return misc.get(0); - } - public static Texture getTransitionBG() { - if (misc.get(1) == null) { - misc.put(1, new Texture(GuiBase.isAndroid() - ? Gdx.files.internal("fallback_skin").child("transition.png") - : Gdx.files.classpath("fallback_skin").child("transition.png"))); - } - return misc.get(1); - } protected void afterDbLoaded() { destroyThis = false; //Allow back() Gdx.input.setCatchKey(Keys.MENU, true); @@ -377,52 +355,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 transition - setTransitionScreen(new TransitionScreen(new Runnable() { - @Override - public void run() { - 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)); - } - }); - } - }); - } + 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) { @@ -754,30 +715,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; - } + 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)); } @@ -800,8 +757,9 @@ public class Forge implements ApplicationListener { 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 @@ -816,7 +774,7 @@ public class Forge implements ApplicationListener { BugReporter.reportException(ex); } finally { if (dispose) - ImageCache.disposeTexture(); + ImageCache.disposeTextures(); } } @@ -859,8 +817,8 @@ public class Forge implements ApplicationListener { animationBatch.setColor(1, 1, 1, 1); animationBatch.draw(lastScreenTexture, 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); animationBatch.setColor(1, 1, 1, 1 - (1 / transitionTime) * animationTimeout); - animationBatch.draw(getTransitionBG(), 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); - animationBatch.draw(getTransitionBG(), 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + animationBatch.draw(getAssets().fallback_skins.get(1), 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + animationBatch.draw(getAssets().fallback_skins.get(1), 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); animationBatch.end(); if (animationTimeout < 0) { currentScene.render(); @@ -879,8 +837,8 @@ public class Forge implements ApplicationListener { animationBatch.setColor(1, 1, 1, 1); animationBatch.draw(lastScreenTexture, 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); animationBatch.setColor(1, 1, 1, (1 / transitionTime) * (animationTimeout + transitionTime)); - animationBatch.draw(getTransitionBG(), 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); - animationBatch.draw(getTransitionBG(), 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + animationBatch.draw(getAssets().fallback_skins.get(1), 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + animationBatch.draw(getAssets().fallback_skins.get(1), 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); animationBatch.end(); return; } @@ -918,6 +876,11 @@ public class Forge implements ApplicationListener { } } } + //update here + if (needsUpdate) { + if (getAssets().manager.update()) + needsUpdate = false; + } graphics.end(); } catch (Exception ex) { graphics.end(); @@ -930,19 +893,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 @@ -972,6 +927,15 @@ public class Forge implements ApplicationListener { @Override public void resume() { + try { + Texture.setAssetManager(getAssets().manager); + needsUpdate = true; + } catch (Exception e) { + //the application context must have been recreated from its last state. + //it could be triggered by the low memory on heap on android. + needsUpdate = false; + e.printStackTrace(); + } if (MatchController.getHostedMatch() != null) { MatchController.getHostedMatch().resume(); } @@ -984,6 +948,7 @@ public class Forge implements ApplicationListener { currentScreen.onClose(null); currentScreen = null; } + assets.dispose(); Dscreens.clear(); graphics.dispose(); SoundSystem.instance.dispose(); @@ -992,7 +957,12 @@ 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() { + return ((Forge)Gdx.app.getApplicationListener()).assets; + } public static boolean switchScene(Scene newScene) { if (currentScene != null) { if (!currentScene.leave()) diff --git a/forge-gui-mobile/src/forge/Graphics.java b/forge-gui-mobile/src/forge/Graphics.java index 0183fc15070..da4feba9f20 100644 --- a/forge-gui-mobile/src/forge/Graphics.java +++ b/forge-gui-mobile/src/forge/Graphics.java @@ -661,8 +661,10 @@ public class Graphics { drawRoundRect(2f, borderLining(borderColor.toString()), x, y, w, h, (h-w)/12); fillRoundRect(tintColor, x, y, w, h, (h-w)/12); } else { - image.draw(this, x, y, w, h); - fillRoundRect(borderColor, x, y, w, h, (h-w)/10);//show corners edges + if (image != null) { + image.draw(this, x, y, w, h); + fillRoundRect(borderColor, x, y, w, h, (h - w) / 10);//show corners edges + } } setAlphaComposite(oldalpha); } @@ -672,10 +674,14 @@ public class Graphics { setAlphaComposite(oldalpha); } public void drawImage(FImage image, Color borderColor, float x, float y, float w, float h) { + if (image == null) + return; image.draw(this, x, y, w, h); fillRoundRect(borderColor, x+1, y+1, w-1.5f, h-1.5f, (h-w)/10);//used by zoom let some edges show... } public void drawAvatarImage(FImage image, float x, float y, float w, float h, boolean drawGrayscale) { + if (image == null) + return; if (!drawGrayscale) { image.draw(this, x, y, w, h); } else { @@ -693,6 +699,8 @@ public class Graphics { } } public void drawCardImage(FImage image, TextureRegion damage_overlay, float x, float y, float w, float h, boolean drawGrayscale, boolean damaged) { + if (image == null) + return; if (!drawGrayscale) { image.draw(this, x, y, w, h); if (damage_overlay != null && damaged) @@ -752,6 +760,8 @@ public class Graphics { } } public void drawGrayTransitionImage(FImage image, float x, float y, float w, float h, boolean withDarkOverlay, float percentage) { + if (image == null) + return; batch.end(); shaderGrayscale.bind(); shaderGrayscale.setUniformf("u_grayness", percentage); @@ -839,6 +849,8 @@ public class Graphics { batch.begin(); } public void drawWarpImage(FImage image, float x, float y, float w, float h, float time) { + if (image == null) + return; batch.end(); shaderWarp.bind(); shaderWarp.setUniformf("u_amount", 0.2f); @@ -854,6 +866,8 @@ public class Graphics { batch.begin(); } public void drawUnderWaterImage(FImage image, float x, float y, float w, float h, float time, boolean withDarkOverlay) { + if (image == null) + return; batch.end(); shaderUnderwater.bind(); shaderUnderwater.setUniformf("u_amount", 10f*time); @@ -893,6 +907,8 @@ public class Graphics { drawImage(image, x, y, w, h, false); } public void drawImage(FImage image, float x, float y, float w, float h, boolean withDarkOverlay) { + if (image == null) + return; image.draw(this, x, y, w, h); if(withDarkOverlay){ float oldalpha = alphaComposite; @@ -1132,6 +1148,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 d6d8edda161..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; 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/player/AdventurePlayer.java b/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java index 572b65a43a7..b3658b2dcf2 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 +581,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 +611,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 65716571865..20dcc49108e 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java @@ -45,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; @@ -59,13 +58,10 @@ public class DuelScene extends ForgeScene { List AIExtras = new ArrayList<>(); - public DuelScene() { - - } + public DuelScene() {} @Override - public void dispose() { - } + public void dispose() {} public boolean hasCallbackExit() { return callbackExit; @@ -88,7 +84,7 @@ public class DuelScene extends ForgeScene { "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."); + "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 @@ -128,7 +124,7 @@ 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<>(); @@ -180,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) { 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 8f60ab6b771..9f693e86889 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java @@ -47,6 +47,17 @@ 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) { @@ -76,11 +87,11 @@ public class RewardScene extends UIScene { doneClicked = true; } else { clearGenerated(); - Forge.switchToLast(); + quitScene(); } } else { clearGenerated(); - Forge.switchToLast(); + quitScene(); } return true; } @@ -102,9 +113,10 @@ public class RewardScene extends UIScene { 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(); @@ -135,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) { @@ -236,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); @@ -283,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 0d6e55b270a..4f7dae601c9 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java @@ -133,7 +133,8 @@ 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).width(100).expand(); + int w = Forge.isLandscapeMode() ? 160 : 80; + settingGroup.add(label).align(Align.left).pad(2, 2, 2, 5).width(w).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 a71ea1698c3..38f4dc0b770 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/StartScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/StartScene.java @@ -2,11 +2,14 @@ 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; @@ -16,9 +19,10 @@ import forge.screens.TransitionScreen; public class StartScene extends UIScene { 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"); } @@ -71,7 +75,8 @@ public class StartScene extends UIScene { } public boolean Exit() { - Forge.exit(true); + if (dialog != null) + dialog.show(stage); return true; } @@ -116,37 +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("Continue", () -> StartScene.this.Continue()); - 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")); - 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")); + 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