From 4effb022195d4d7837818976c7bc3d9a40fb0934 Mon Sep 17 00:00:00 2001 From: Bug Hunter Date: Sat, 27 Mar 2021 04:37:27 +0000 Subject: [PATCH] Move all hidden zone changing after choosing --- .../game/ability/SpellAbilityEffect.java | 32 + .../game/ability/effects/BalanceEffect.java | 33 +- .../ability/effects/ChangeZoneEffect.java | 1017 +++++++++-------- .../game/ability/effects/DiscardEffect.java | 134 +-- .../res/cardsfolder/s/scheming_symmetry.txt | 3 +- 5 files changed, 602 insertions(+), 617 deletions(-) diff --git a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java index 2a403961a63..6e9860a20d2 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java @@ -2,6 +2,7 @@ package forge.game.ability; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.StringTokenizer; import org.apache.commons.lang3.StringUtils; @@ -661,4 +662,35 @@ public abstract class SpellAbilityEffect { }; } + + protected static void discard(SpellAbility sa, CardZoneTable table, Map discardedMap) { + Set discarders = discardedMap.keySet(); + for (Player p : discarders) { + final CardCollection discardedByPlayer = new CardCollection(); + for (Card card : Lists.newArrayList(discardedMap.get(p))) { // without copying will get concurrent modification exception + if (card == null) { continue; } + if (p.discard(card, sa, table) != null) { + discardedByPlayer.add(card); + } + } + discardedMap.put(p, discardedByPlayer); + } + + for (Player p : discarders) { + CardCollectionView discardedByPlayer = discardedMap.get(p); + if (!discardedByPlayer.isEmpty()) { + boolean firstDiscard = p.getNumDiscardedThisTurn() - discardedByPlayer.size() == 0; + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Player, p); + runParams.put(AbilityKey.Cards, discardedByPlayer); + runParams.put(AbilityKey.Cause, sa); + runParams.put(AbilityKey.FirstTime, firstDiscard); + p.getGame().getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false); + + if (sa.hasParam("RememberDiscardingPlayers")) { + sa.getHostCard().addRemembered(p); + } + } + } + } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/BalanceEffect.java b/forge-game/src/main/java/forge/game/ability/effects/BalanceEffect.java index cd1ba4363d4..297c411824b 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/BalanceEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/BalanceEffect.java @@ -4,16 +4,17 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import com.google.common.collect.Maps; + import forge.game.Game; -import forge.game.ability.AbilityKey; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.card.CardCollection; +import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardZoneTable; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.collect.FCollectionView; @@ -38,6 +39,7 @@ public class BalanceEffect extends SpellAbilityEffect { final FCollectionView players = game.getPlayersInTurnOrder(); final List validCards = new ArrayList<>(players.size()); + Map discardedMap = Maps.newHashMap(); for(int i = 0; i < players.size(); i++) { // Find the minimum of each Valid per player @@ -46,37 +48,26 @@ public class BalanceEffect extends SpellAbilityEffect { } CardZoneTable table = new CardZoneTable(); - for(int i = 0; i < players.size(); i++) { + for (int i = 0; i < players.size(); i++) { Player p = players.get(i); int numToBalance = validCards.get(i).size() - min; if (numToBalance == 0) { continue; } if (zone.equals(ZoneType.Hand)) { - boolean firstDiscard = p.getNumDiscardedThisTurn() == 0; - final CardCollection discardedByPlayer = new CardCollection(); - for (Card card : p.getController().chooseCardsToDiscardFrom(p, sa, validCards.get(i), numToBalance, numToBalance)) { - if ( null == card ) continue; - if (p.discard(card, sa, table) != null) { - discardedByPlayer.add(card); - } - } - - if (!discardedByPlayer.isEmpty()) { - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, p); - runParams.put(AbilityKey.Cards, discardedByPlayer); - runParams.put(AbilityKey.Cause, sa); - runParams.put(AbilityKey.FirstTime, firstDiscard); - game.getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false); - } + discardedMap.put(p, p.getController().chooseCardsToDiscardFrom(p, sa, validCards.get(i), numToBalance, numToBalance)); } else { // Battlefield - for(Card card : p.getController().choosePermanentsToSacrifice(sa, numToBalance, numToBalance, validCards.get(i), valid)) { + for (Card card : p.getController().choosePermanentsToSacrifice(sa, numToBalance, numToBalance, validCards.get(i), valid)) { if ( null == card ) continue; game.getAction().sacrifice(card, sa, table); } } } + + if (zone.equals(ZoneType.Hand)) { + discard(sa, table, discardedMap); + } + table.triggerChangesZoneAll(game); } } 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 057daba40f0..1831a0927c7 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 @@ -80,8 +80,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { * @return a {@link java.lang.String} object. */ private static String changeHiddenOriginStackDescription(final SpellAbility sa) { - // TODO build Stack Description will need expansion as more cards are - // added + // TODO build Stack Description will need expansion as more cards are added final StringBuilder sb = new StringBuilder(); final Card host = sa.getHostCard(); @@ -358,8 +357,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } if (destination.equals(ZoneType.Library)) { - if (sa.hasParam("Shuffle")) { // for things like Gaea's - // Blessing + if (sa.hasParam("Shuffle")) { // for things like Gaea's Blessing sb.append("Shuffle").append(targetname); sb.append(" into").append(pronoun).append("owner's library."); @@ -869,28 +867,310 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } } - for (final Player player : fetchers) { + changeZonePlayerInvariant(chooser, sa, fetchers); + } + + private static void changeZonePlayerInvariant(Player chooser, SpellAbility sa, List fetchers) { + final Card source = sa.getHostCard(); + final Game game = source.getGame(); + final boolean defined = sa.hasParam("Defined"); + final String changeType = sa.getParam("ChangeType"); + Map HiddenOriginChoicesMap = Maps.newHashMap(); + + for (Player player : fetchers) { Player decider = chooser; if (decider == null) { decider = player; } - changeZonePlayerInvariant(decider, sa, player); - } - } - private static void changeZonePlayerInvariant(Player decider, SpellAbility sa, Player player) { - final Game game = player.getGame(); - - if (sa.usesTargeting()) { - final List players = Lists.newArrayList(sa.getTargets().getTargetPlayers()); - player = sa.hasParam("DefinedPlayer") ? player : players.get(0); - if (players.contains(player) && !player.canBeTargetedBy(sa)) { - return; + if (sa.usesTargeting()) { + final List players = Lists.newArrayList(sa.getTargets().getTargetPlayers()); + player = sa.hasParam("DefinedPlayer") ? player : players.get(0); + if (players.contains(player) && !player.canBeTargetedBy(sa)) { + return; + } } + + List origin = Lists.newArrayList(); + if (sa.hasParam("Origin")) { + origin = ZoneType.listValueOf(sa.getParam("Origin")); + } + ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); + + if (sa.hasParam("OriginChoice")) { + // Currently only used for Mishra, but may be used by other things + // Improve how this message reacts for other cards + final List alt = ZoneType.listValueOf(sa.getParam("OriginAlternative")); + CardCollectionView altFetchList = AbilityUtils.filterListByType(player.getCardsIn(alt), sa.getParam("ChangeType"), sa); + + final StringBuilder sb = new StringBuilder(); + sb.append(sa.getParam("AlternativeMessage")).append(" "); + sb.append(altFetchList.size()).append(" " + Localizer.getInstance().getMessage("lblCardMatchSearchingTypeInAlternateZones")); + + if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneFromAltSource, sb.toString())) { + origin = alt; + } + } + + // this needs to be zero indexed. Top = 0, Third = 2 + int libraryPos = sa.hasParam("LibraryPosition") ? AbilityUtils.calculateAmount(source, sa.getParam("LibraryPosition"), sa) : 0; + + if (sa.hasParam("DestinationAlternative")) { + final StringBuilder sb = new StringBuilder(); + sb.append(sa.getParam("AlternativeDestinationMessage")); + + if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneToAltDestination, sb.toString())) { + destination = ZoneType.smartValueOf(sa.getParam("DestinationAlternative")); + libraryPos = sa.hasParam("LibraryPositionAlternative") ? Integer.parseInt(sa.getParam("LibraryPositionAlternative")) : 0; + } + } + + int changeNum = sa.hasParam("ChangeNum") ? AbilityUtils.calculateAmount(source, sa.getParam("ChangeNum"), sa) : 1; + + final boolean optional = sa.hasParam("Optional"); + if (optional) { + String prompt; + if (defined) { + prompt = Localizer.getInstance().getMessage("lblPutThatCardFromPlayerOriginToDestination", "{player's}", Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME).toLowerCase(), destination.getTranslatedName().toLowerCase()); + } + else { + 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)) { + return; + } + } + + CardCollection fetchList; + boolean shuffleMandatory = true; + boolean searchedLibrary = false; + if (defined) { + fetchList = new CardCollection(AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)); + if (!sa.hasParam("ChangeNum")) { + changeNum = fetchList.size(); + } + } + else if (!origin.contains(ZoneType.Library) && !origin.contains(ZoneType.Hand) + && !sa.hasParam("DefinedPlayer")) { + fetchList = new CardCollection(player.getGame().getCardsIn(origin)); + } + else { + fetchList = new CardCollection(player.getCardsIn(origin)); + if (origin.contains(ZoneType.Library) && !sa.hasParam("NoLooking")) { + searchedLibrary = true; + + if (decider.hasKeyword("LimitSearchLibrary")) { // Aven Mindcensor + fetchList.removeAll(player.getCardsIn(ZoneType.Library)); + final int fetchNum = Math.min(player.getCardsIn(ZoneType.Library).size(), 4); + if (fetchNum == 0) { + searchedLibrary = false; + } + else { + fetchList.addAll(player.getCardsIn(ZoneType.Library, fetchNum)); + } + } + if (!decider.canSearchLibraryWith(sa, player)) { + fetchList.removeAll(player.getCardsIn(ZoneType.Library)); + // "if you do/sb does, shuffle" is not mandatory (usually a triggered ability), should has this param. + // "then shuffle" is mandatory + shuffleMandatory = !sa.hasParam("ShuffleNonMandatory"); + searchedLibrary = false; + } + } + } + + //determine list of all cards to reveal to player in addition to those that can be chosen + DelayedReveal delayedReveal = null; + if (!defined && !sa.hasParam("AlreadyRevealed")) { + if (origin.contains(ZoneType.Library) && searchedLibrary) { + final int fetchNum = Math.min(player.getCardsIn(ZoneType.Library).size(), 4); + CardCollectionView shown = !decider.hasKeyword("LimitSearchLibrary") ? player.getCardsIn(ZoneType.Library) : player.getCardsIn(ZoneType.Library, fetchNum); + // Look at whole library before moving onto choosing a card + delayedReveal = new DelayedReveal(shown, ZoneType.Library, PlayerView.get(player), CardTranslation.getTranslatedName(source.getName()) + " - " + Localizer.getInstance().getMessage("lblLookingCardIn") + " "); + } + else if (origin.contains(ZoneType.Hand) && player.isOpponentOf(decider)) { + delayedReveal = new DelayedReveal(player.getCardsIn(ZoneType.Hand), ZoneType.Hand, PlayerView.get(player), CardTranslation.getTranslatedName(source.getName()) + " - " + Localizer.getInstance().getMessage("lblLookingCardIn") + " "); + } + } + + Long controlTimestamp = null; + if (searchedLibrary) { + if (decider.equals(player)) { + Map.Entry searchControlPlayer = player.getControlledWhileSearching(); + if (searchControlPlayer != null) { + controlTimestamp = searchControlPlayer.getKey(); + player.addController(controlTimestamp, searchControlPlayer.getValue()); + } + + decider.incLibrarySearched(); + // should only count the number of searching player's own library + // Panglacial Wurm + CardCollection canCastWhileSearching = CardLists.getKeyword(fetchList, + "While you're searching your library, you may cast CARDNAME from your library."); + for (final Card tgtCard : canCastWhileSearching) { + List sas = AbilityUtils.getBasicSpellsFromPlayEffect(tgtCard, decider); + if (sas.isEmpty()) { + continue; + } + SpellAbility tgtSA = decider.getController().getAbilityToPlay(tgtCard, sas); + if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())))) { + continue; + } + tgtSA.setSVar("IsCastFromPlayEffect", "True"); + // if played, that card cannot be found + if (decider.getController().playSaFromPlayEffect(tgtSA)) { + fetchList.remove(tgtCard); + } + //some kind of reset here? + } + } + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Player, decider); + runParams.put(AbilityKey.Target, Lists.newArrayList(player)); + decider.getGame().getTriggerHandler().runTrigger(TriggerType.SearchedLibrary, runParams, false); + } + + if (!defined && changeType != null) { + fetchList = (CardCollection)AbilityUtils.filterListByType(fetchList, sa.getParam("ChangeType"), sa); + } + + if (sa.hasParam("NoShuffle")) { + shuffleMandatory = false; + } + + if (sa.hasParam("Unimprint")) { + source.clearImprintedCards(); + } + + String selectPrompt = sa.hasParam("SelectPrompt") ? sa.getParam("SelectPrompt") : MessageUtil.formatMessage(Localizer.getInstance().getMessage("lblSelectCardFromPlayerZone", "{player's}", Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME).toLowerCase()), decider, player); + final String totalcmc = sa.getParam("WithTotalCMC"); + int totcmc = AbilityUtils.calculateAmount(source, totalcmc, sa); + + fetchList.sort(); + + CardCollection chosenCards = new CardCollection(); + // only multi-select if player can select more than one + if (changeNum > 1 && allowMultiSelect(decider, sa)) { + List selectedCards; + if (! sa.hasParam("SelectPrompt")) { + // new default messaging for multi select + if (fetchList.size() > changeNum) { + //Select up to %changeNum cards from %players %origin + selectPrompt = MessageUtil.formatMessage(Localizer.getInstance().getMessage("lblSelectUpToNumCardFromPlayerZone", String.valueOf(changeNum), "{player's}", Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME).toLowerCase()), decider, player); + } else { + selectPrompt = MessageUtil.formatMessage(Localizer.getInstance().getMessage("lblSelectCardsFromPlayerZone", "{player's}", Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME).toLowerCase()), decider, player); + } + } + // ensure that selection is within maximum allowed changeNum + do { + selectedCards = decider.getController().chooseCardsForZoneChange(destination, origin, sa, fetchList, 0, changeNum, delayedReveal, selectPrompt, decider); + } while (selectedCards != null && selectedCards.size() > changeNum); + if (selectedCards != null) { + chosenCards.addAll(selectedCards); + } + // maybe prompt the user if they selected fewer than the maximum possible? + } else { + // one at a time + for (int i = 0; i < changeNum && destination != null; i++) { + if (sa.hasParam("DifferentNames")) { + for (Card c : chosenCards) { + fetchList = CardLists.filter(fetchList, Predicates.not(CardPredicates.sharesNameWith(c))); + } + } + if (sa.hasParam("DifferentCMC")) { + for (Card c : chosenCards) { + fetchList = CardLists.filter(fetchList, Predicates.not(CardPredicates.sharesCMCWith(c))); + } + } + if (sa.hasParam("ShareLandType")) { + // After the first card is chosen, check if the land type is shared + for (final Card card : chosenCards) { + fetchList = CardLists.filter(fetchList, new Predicate() { + @Override + public boolean apply(final Card c) { + return c.sharesLandTypeWith(card); + } + + }); + } + } + if (totalcmc != null) { + if (totcmc >= 0) { + fetchList = CardLists.getValidCards(fetchList, "Card.cmcLE" + totcmc, source.getController(), source, sa); + } + } + + // If we're choosing multiple cards, only need to show the reveal dialog the first time through. + boolean shouldReveal = (i == 0); + Card c = null; + if (sa.hasParam("AtRandom")) { + if (shouldReveal && delayedReveal != null) { + decider.getController().reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix()); + } + c = Aggregates.random(fetchList); + } else if (defined && !sa.hasParam("ChooseFromDefined")) { + c = Iterables.getFirst(fetchList, null); + } else { + String title = selectPrompt; + if (changeNum > 1) { //indicate progress if multiple cards being chosen + title += " (" + (i + 1) + " / " + changeNum + ")"; + } + c = decider.getController().chooseSingleCardForZoneChange(destination, origin, sa, fetchList, shouldReveal ? delayedReveal : null, title, !sa.hasParam("Mandatory"), decider); + } + + if (c == null) { + final int num = Math.min(fetchList.size(), changeNum - i); + String message = Localizer.getInstance().getMessage("lblCancelSearchUpToSelectNumCards", String.valueOf(num)); + + if (fetchList.isEmpty() || decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message)) { + break; + } + i--; + continue; + } + + fetchList.remove(c); + if (delayedReveal != null) { + delayedReveal.remove(CardView.get(c)); + } + chosenCards.add(c); + + if (totalcmc != null) { + totcmc -= c.getCMC(); + } + } + } + + if (sa.hasParam("ShuffleChangedPile")) { + CardLists.shuffle(chosenCards); + } + // do not shuffle the library once we have placed a fetched card on top. + if (origin.contains(ZoneType.Library) && (destination == ZoneType.Library) && !"False".equals(sa.getParam("Shuffle"))) { + player.shuffle(sa); + } + + // remove Controlled While Searching + if (controlTimestamp != null) { + player.removeController(controlTimestamp); + } + + HiddenOriginChoices choices = new HiddenOriginChoices(); + choices.searchedLibrary = searchedLibrary; + choices.shuffleMandatory = shuffleMandatory; + choices.chosenCards = chosenCards; + choices.libraryPos = libraryPos; + choices.origin = origin; + choices.destination = destination; + HiddenOriginChoicesMap.put(player, choices); } + final boolean remember = sa.hasParam("RememberChanged"); + final boolean forget = sa.hasParam("ForgetChanged"); + final boolean champion = sa.hasParam("Champion"); + final boolean imprint = sa.hasParam("Imprint"); + final SpellAbility root = sa.getRootAbility(); - SpellAbility cause = sa; if (root.isReplacementAbility()) { SpellAbility replacingObject = (SpellAbility) root.getReplacingObject(AbilityKey.Cause); @@ -899,520 +1179,269 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } } - List origin = Lists.newArrayList(); - if (sa.hasParam("Origin")) { - origin = ZoneType.listValueOf(sa.getParam("Origin")); - } - ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); - final Card source = sa.getHostCard(); - - // this needs to be zero indexed. Top = 0, Third = 2 - int libraryPos = sa.hasParam("LibraryPosition") ? AbilityUtils.calculateAmount(source, sa.getParam("LibraryPosition"), sa) : 0; - - if (sa.hasParam("OriginChoice")) { - // Currently only used for Mishra, but may be used by other things - // Improve how this message reacts for other cards - final List alt = ZoneType.listValueOf(sa.getParam("OriginAlternative")); - CardCollectionView altFetchList = AbilityUtils.filterListByType(player.getCardsIn(alt), sa.getParam("ChangeType"), sa); - - final StringBuilder sb = new StringBuilder(); - sb.append(sa.getParam("AlternativeMessage")).append(" "); - sb.append(altFetchList.size()).append(" " + Localizer.getInstance().getMessage("lblCardMatchSearchingTypeInAlternateZones")); - - if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneFromAltSource, sb.toString())) { - origin = alt; + for (Player player : fetchers) { + boolean searchedLibrary = HiddenOriginChoicesMap.get(player).searchedLibrary; + boolean shuffleMandatory = HiddenOriginChoicesMap.get(player).shuffleMandatory; + CardCollection chosenCards = HiddenOriginChoicesMap.get(player).chosenCards; + int libraryPos = HiddenOriginChoicesMap.get(player).libraryPos; + List origin = HiddenOriginChoicesMap.get(player).origin; + ZoneType destination = HiddenOriginChoicesMap.get(player).destination; + CardCollection movedCards = new CardCollection(); + long ts = game.getNextTimestamp(); + final CardZoneTable triggerList = new CardZoneTable(); + boolean combatChanged = false; + Player decider = chooser; + if (decider == null) { + decider = player; } - } - if (sa.hasParam("DestinationAlternative")) { - final StringBuilder sb = new StringBuilder(); - sb.append(sa.getParam("AlternativeDestinationMessage")); - - if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneToAltDestination, sb.toString())) { - destination = ZoneType.smartValueOf(sa.getParam("DestinationAlternative")); - libraryPos = sa.hasParam("LibraryPositionAlternative") ? Integer.parseInt(sa.getParam("LibraryPositionAlternative")) : 0; - } - } - - final boolean defined = sa.hasParam("Defined"); - int changeNum = sa.hasParam("ChangeNum") ? AbilityUtils.calculateAmount(source, sa.getParam("ChangeNum"), sa) : 1; - - final boolean optional = sa.hasParam("Optional"); - if (optional) { - String prompt; - if (defined) { - prompt = Localizer.getInstance().getMessage("lblPutThatCardFromPlayerOriginToDestination", "{player's}", Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME).toLowerCase(), destination.getTranslatedName().toLowerCase()); - } - else { - 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)) { - return; - } - } - - String changeType = sa.getParam("ChangeType"); - - CardCollection fetchList; - boolean shuffleMandatory = true; - boolean searchedLibrary = false; - if (defined) { - fetchList = new CardCollection(AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)); - if (!sa.hasParam("ChangeNum")) { - changeNum = fetchList.size(); - } - } - else if (!origin.contains(ZoneType.Library) && !origin.contains(ZoneType.Hand) - && !sa.hasParam("DefinedPlayer")) { - fetchList = new CardCollection(player.getGame().getCardsIn(origin)); - } - else { - fetchList = new CardCollection(player.getCardsIn(origin)); - if (origin.contains(ZoneType.Library) && !sa.hasParam("NoLooking")) { - searchedLibrary = true; - - if (decider.hasKeyword("LimitSearchLibrary")) { // Aven Mindcensor - fetchList.removeAll(player.getCardsIn(ZoneType.Library)); - final int fetchNum = Math.min(player.getCardsIn(ZoneType.Library).size(), 4); - if (fetchNum == 0) { - searchedLibrary = false; - } - else { - fetchList.addAll(player.getCardsIn(ZoneType.Library, fetchNum)); - } + for (final Card c : chosenCards) { + Card movedCard = null; + final Zone originZone = game.getZoneOf(c); + Map moveParams = Maps.newEnumMap(AbilityKey.class); + moveParams.put(AbilityKey.FoundSearchingLibrary, searchedLibrary); + if (destination.equals(ZoneType.Library)) { + movedCard = game.getAction().moveToLibrary(c, libraryPos, cause, moveParams); } - if (!decider.canSearchLibraryWith(sa, player)) { - fetchList.removeAll(player.getCardsIn(ZoneType.Library)); - // "if you do/sb does, shuffle" is not mandatory (usually a triggered ability), should has this param. - // "then shuffle" is mandatory - shuffleMandatory = !sa.hasParam("ShuffleNonMandatory"); - searchedLibrary = false; - } - } - } - - //determine list of all cards to reveal to player in addition to those that can be chosen - DelayedReveal delayedReveal = null; - if (!defined && !sa.hasParam("AlreadyRevealed")) { - if (origin.contains(ZoneType.Library) && searchedLibrary) { - final int fetchNum = Math.min(player.getCardsIn(ZoneType.Library).size(), 4); - CardCollectionView shown = !decider.hasKeyword("LimitSearchLibrary") ? player.getCardsIn(ZoneType.Library) : player.getCardsIn(ZoneType.Library, fetchNum); - // Look at whole library before moving onto choosing a card - delayedReveal = new DelayedReveal(shown, ZoneType.Library, PlayerView.get(player), CardTranslation.getTranslatedName(source.getName()) + " - " + Localizer.getInstance().getMessage("lblLookingCardIn") + " "); - } - else if (origin.contains(ZoneType.Hand) && player.isOpponentOf(decider)) { - delayedReveal = new DelayedReveal(player.getCardsIn(ZoneType.Hand), ZoneType.Hand, PlayerView.get(player), CardTranslation.getTranslatedName(source.getName()) + " - " + Localizer.getInstance().getMessage("lblLookingCardIn") + " "); - } - } - - Long controlTimestamp = null; - if (searchedLibrary) { - if (decider.equals(player)) { - Map.Entry searchControlPlayer = player.getControlledWhileSearching(); - if (searchControlPlayer != null) { - controlTimestamp = searchControlPlayer.getKey(); - player.addController(controlTimestamp, searchControlPlayer.getValue()); - } - - decider.incLibrarySearched(); - // should only count the number of searching player's own library - // Panglacial Wurm - CardCollection canCastWhileSearching = CardLists.getKeyword(fetchList, - "While you're searching your library, you may cast CARDNAME from your library."); - for (final Card tgtCard : canCastWhileSearching) { - List sas = AbilityUtils.getBasicSpellsFromPlayEffect(tgtCard, decider); - if (sas.isEmpty()) { - continue; + else if (destination.equals(ZoneType.Battlefield)) { + if (sa.hasParam("Tapped")) { + c.setTapped(true); + } else if (sa.hasParam("Untapped")) { + c.setTapped(false); } - SpellAbility tgtSA = decider.getController().getAbilityToPlay(tgtCard, sas); - if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())))) { - continue; + if (sa.hasAdditionalAbility("AnimateSubAbility")) { + // need LKI before Animate does apply + moveParams.put(AbilityKey.CardLKI, CardUtil.getLKICopy(c)); + + source.addRemembered(c); + AbilityUtils.resolve(sa.getAdditionalAbility("AnimateSubAbility")); + source.removeRemembered(c); } - tgtSA.setSVar("IsCastFromPlayEffect", "True"); - // if played, that card cannot be found - if (decider.getController().playSaFromPlayEffect(tgtSA)) { - fetchList.remove(tgtCard); + if (sa.hasParam("GainControl")) { + Player newController = sa.getActivatingPlayer(); + if (sa.hasParam("NewController")) { + newController = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("NewController"), sa).get(0); + } + c.setController(newController, game.getNextTimestamp()); } - //some kind of reset here? - } - } - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, decider); - runParams.put(AbilityKey.Target, Lists.newArrayList(player)); - decider.getGame().getTriggerHandler().runTrigger(TriggerType.SearchedLibrary, runParams, false); - } - - if (!defined && changeType != null) { - fetchList = (CardCollection)AbilityUtils.filterListByType(fetchList, sa.getParam("ChangeType"), sa); - } - - if (sa.hasParam("NoShuffle")) { - shuffleMandatory = false; - } - - if (sa.hasParam("Unimprint")) { - source.clearImprintedCards(); - } - - final boolean remember = sa.hasParam("RememberChanged"); - final boolean champion = sa.hasParam("Champion"); - final boolean forget = sa.hasParam("ForgetChanged"); - final boolean imprint = sa.hasParam("Imprint"); - String selectPrompt = sa.hasParam("SelectPrompt") ? sa.getParam("SelectPrompt") : MessageUtil.formatMessage(Localizer.getInstance().getMessage("lblSelectCardFromPlayerZone", "{player's}", Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME).toLowerCase()), decider, player); - final String totalcmc = sa.getParam("WithTotalCMC"); - int totcmc = AbilityUtils.calculateAmount(source, totalcmc, sa); - - fetchList.sort(); - - CardCollection chosenCards = new CardCollection(); - // only multi-select if player can select more than one - if (changeNum > 1 && allowMultiSelect(decider, sa)) { - List selectedCards; - if (! sa.hasParam("SelectPrompt")) { - // new default messaging for multi select - if (fetchList.size() > changeNum) { - //Select up to %changeNum cards from %players %origin - selectPrompt = MessageUtil.formatMessage(Localizer.getInstance().getMessage("lblSelectUpToNumCardFromPlayerZone", String.valueOf(changeNum), "{player's}", Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME).toLowerCase()), decider, player); - } else { - selectPrompt = MessageUtil.formatMessage(Localizer.getInstance().getMessage("lblSelectCardsFromPlayerZone", "{player's}", Lang.joinHomogenous(origin, ZoneType.Accessors.GET_TRANSLATED_NAME).toLowerCase()), decider, player); - } - } - // ensure that selection is within maximum allowed changeNum - do { - selectedCards = decider.getController().chooseCardsForZoneChange(destination, origin, sa, fetchList, 0, changeNum, delayedReveal, selectPrompt, decider); - } while (selectedCards != null && selectedCards.size() > changeNum); - if (selectedCards != null) { - chosenCards.addAll(selectedCards); - } - // maybe prompt the user if they selected fewer than the maximum possible? - } else { - // one at a time - for (int i = 0; i < changeNum && destination != null; i++) { - if (sa.hasParam("DifferentNames")) { - for (Card c : chosenCards) { - fetchList = CardLists.filter(fetchList, Predicates.not(CardPredicates.sharesNameWith(c))); + if (sa.hasParam("WithCounters")) { + String[] parse = sa.getParam("WithCounters").split("_"); + c.addEtbCounter(CounterType.getType(parse[0]), Integer.parseInt(parse[1]), player); } - } - if (sa.hasParam("DifferentCMC")) { - for (Card c : chosenCards) { - fetchList = CardLists.filter(fetchList, Predicates.not(CardPredicates.sharesCMCWith(c))); + + if (sa.hasParam("WithCountersType")) { + CounterType cType = CounterType.getType(sa.getParam("WithCountersType")); + int cAmount = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("WithCountersAmount", "1"), sa); + c.addEtbCounter(cType, cAmount, player); } - } - if (sa.hasParam("ShareLandType")) { - // After the first card is chosen, check if the land type is shared - for (final Card card : chosenCards) { - fetchList = CardLists.filter(fetchList, new Predicate() { - @Override - public boolean apply(final Card c) { - return c.sharesLandTypeWith(card); + if (sa.hasParam("Transformed")) { + if (c.isDoubleFaced()) { + c.changeCardState("Transform", null, sa); + } else { + // If it can't Transform, don't change zones. + continue; + } + } + + if (sa.hasParam("AttachedTo")) { + CardCollection list = AbilityUtils.getDefinedCards(source, sa.getParam("AttachedTo"), sa); + if (list.isEmpty()) { + list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("AttachedTo"), c.getController(), c, sa); + } + if (!list.isEmpty()) { + Card attachedTo = null; + if (list.size() > 1) { + String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName())); + Map params = Maps.newHashMap(); + params.put("Attach", c); + attachedTo = decider.getController().chooseSingleEntityForEffect(list, sa, title, params); + } + else { + attachedTo = list.get(0); } - }); + if (c.isAttachment()) { + c.attachToEntity(attachedTo); + } + } + else { // When it should enter the battlefield attached to an illegal permanent it fails + continue; + } } - } - if (totalcmc != null) { - if (totcmc >= 0) { - fetchList = CardLists.getValidCards(fetchList, "Card.cmcLE" + totcmc, source.getController(), source, sa); - } - } - // If we're choosing multiple cards, only need to show the reveal dialog the first time through. - boolean shouldReveal = (i == 0); - Card c = null; - if (sa.hasParam("AtRandom")) { - if (shouldReveal && delayedReveal != null) { - decider.getController().reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix()); - } - c = Aggregates.random(fetchList); - } else if (defined && !sa.hasParam("ChooseFromDefined")) { - c = Iterables.getFirst(fetchList, null); - } else { - String title = selectPrompt; - if (changeNum > 1) { //indicate progress if multiple cards being chosen - title += " (" + (i + 1) + " / " + changeNum + ")"; - } - c = decider.getController().chooseSingleCardForZoneChange(destination, origin, sa, fetchList, shouldReveal ? delayedReveal : null, title, !sa.hasParam("Mandatory"), decider); - } - - if (c == null) { - final int num = Math.min(fetchList.size(), changeNum - i); - String message = Localizer.getInstance().getMessage("lblCancelSearchUpToSelectNumCards", String.valueOf(num)); - - if (fetchList.isEmpty() || decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message)) { - break; - } - i--; - continue; - } - - fetchList.remove(c); - if (delayedReveal != null) { - delayedReveal.remove(CardView.get(c)); - } - chosenCards.add(c); - - if (totalcmc != null) { - totcmc -= c.getCMC(); - } - } - } - - if (sa.hasParam("ShuffleChangedPile")) { - CardLists.shuffle(chosenCards); - } - // do not shuffle the library once we have placed a fetched - // card on top. - if (origin.contains(ZoneType.Library) && (destination == ZoneType.Library) && !"False".equals(sa.getParam("Shuffle"))) { - player.shuffle(sa); - } - - CardCollection movedCards = new CardCollection(); - long ts = game.getNextTimestamp(); - final CardZoneTable triggerList = new CardZoneTable(); - boolean combatChanged = false; - for (final Card c : chosenCards) { - Card movedCard = null; - final Zone originZone = game.getZoneOf(c); - Map moveParams = Maps.newEnumMap(AbilityKey.class); - moveParams.put(AbilityKey.FoundSearchingLibrary, searchedLibrary); - if (destination.equals(ZoneType.Library)) { - movedCard = game.getAction().moveToLibrary(c, libraryPos, cause, moveParams); - } - else if (destination.equals(ZoneType.Battlefield)) { - if (sa.hasParam("Tapped")) { - c.setTapped(true); - } else if (sa.hasParam("Untapped")) { - c.setTapped(false); - } - if (sa.hasAdditionalAbility("AnimateSubAbility")) { - // need LKI before Animate does apply - moveParams.put(AbilityKey.CardLKI, CardUtil.getLKICopy(c)); - - source.addRemembered(c); - AbilityUtils.resolve(sa.getAdditionalAbility("AnimateSubAbility")); - source.removeRemembered(c); - } - if (sa.hasParam("GainControl")) { - Player newController = sa.getActivatingPlayer(); - if (sa.hasParam("NewController")) { - newController = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("NewController"), sa).get(0); - } - c.setController(newController, game.getNextTimestamp()); - } - if (sa.hasParam("WithCounters")) { - String[] parse = sa.getParam("WithCounters").split("_"); - c.addEtbCounter(CounterType.getType(parse[0]), Integer.parseInt(parse[1]), player); - } - - if (sa.hasParam("WithCountersType")) { - CounterType cType = CounterType.getType(sa.getParam("WithCountersType")); - int cAmount = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("WithCountersAmount", "1"), sa); - c.addEtbCounter(cType, cAmount, player); - } - if (sa.hasParam("Transformed")) { - if (c.isDoubleFaced()) { - c.changeCardState("Transform", null, sa); - } else { - // If it can't Transform, don't change zones. - continue; - } - } - - if (sa.hasParam("AttachedTo")) { - CardCollection list = AbilityUtils.getDefinedCards(source, sa.getParam("AttachedTo"), sa); - if (list.isEmpty()) { - list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("AttachedTo"), c.getController(), c, sa); - } - if (!list.isEmpty()) { - Card attachedTo = null; - if (list.size() > 1) { - String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName())); + if (sa.hasParam("AttachedToPlayer")) { + FCollectionView list = AbilityUtils.getDefinedPlayers(source, sa.getParam("AttachedToPlayer"), sa); + if (!list.isEmpty()) { + String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName())); Map params = Maps.newHashMap(); params.put("Attach", c); - attachedTo = decider.getController().chooseSingleEntityForEffect(list, sa, title, params); - } - else { - attachedTo = list.get(0); - } - - if (c.isAttachment()) { + Player attachedTo = player.getController().chooseSingleEntityForEffect(list, sa, title, params); c.attachToEntity(attachedTo); } + else { // When it should enter the battlefield attached to an illegal permanent it fails + continue; + } } - else { // When it should enter the battlefield attached to an illegal permanent it fails - continue; + + if (addToCombat(c, c.getController(), sa, "Attacking", "Blocking")) { + combatChanged = true; + } + + // need to be facedown before it hits the battlefield in case of Replacement Effects or Trigger + if (sa.hasParam("FaceDown")) { + c.turnFaceDown(true); + + // set New Pt doesn't work because this values need to be copyable for clone effects + if (sa.hasParam("FaceDownPower")) { + c.setBasePower(AbilityUtils.calculateAmount( + source, sa.getParam("FaceDownPower"), sa)); + } + if (sa.hasParam("FaceDownToughness")) { + c.setBaseToughness(AbilityUtils.calculateAmount( + source, sa.getParam("FaceDownToughness"), sa)); + } + + if (sa.hasParam("FaceDownAddType")) { + c.addType(Arrays.asList(sa.getParam("FaceDownAddType").split(" & "))); + } + + if (sa.hasParam("FaceDownPower") || sa.hasParam("FaceDownToughness") + || sa.hasParam("FaceDownAddType")) { + final GameCommand unanimate = new GameCommand() { + private static final long serialVersionUID = 8853789549297846163L; + + @Override + public void run() { + c.clearStates(CardStateName.FaceDown, true); + } + }; + + c.addFaceupCommand(unanimate); + } + } + movedCard = game.getAction().moveToPlay(c, c.getController(), cause, moveParams); + if (sa.hasParam("Tapped")) { + movedCard.setTapped(true); + } else if (sa.hasParam("Untapped")) { + c.setTapped(false); + } + + movedCard.setTimestamp(ts); + } + else if (destination.equals(ZoneType.Exile)) { + movedCard = game.getAction().exile(c, sa, moveParams); + if (!c.isToken()) { + Card host = sa.getOriginalHost(); + if (host == null) { + host = sa.getHostCard(); + } + movedCard.setExiledWith(host); + movedCard.setExiledBy(host.getController()); + } + if (sa.hasParam("ExileFaceDown")) { + movedCard.turnFaceDown(true); + } + if (sa.hasParam("Foretold")) { + movedCard.setForetold(true); + movedCard.setForetoldThisTurn(true); + movedCard.setForetoldByEffect(true); + // look at the exiled card + movedCard.addMayLookTemp(sa.getActivatingPlayer()); + } + } + else { + movedCard = game.getAction().moveTo(destination, c, 0, cause, moveParams); + } + + movedCards.add(movedCard); + + if (originZone != null) { + triggerList.put(originZone.getZoneType(), movedCard.getZone().getZoneType(), movedCard); + + if (c.getMeldedWith() != null) { + Card meld = game.getCardState(c.getMeldedWith(), null); + if (meld != null) { + triggerList.put(originZone.getZoneType(), movedCard.getZone().getZoneType(), meld); + } + } + if (c.hasMergedCard()) { + for (final Card card : c.getMergedCards()) { + if (card == c) continue; + triggerList.put(originZone.getZoneType(), movedCard.getZone().getZoneType(), card); + } } } - if (sa.hasParam("AttachedToPlayer")) { - FCollectionView list = AbilityUtils.getDefinedPlayers(source, sa.getParam("AttachedToPlayer"), sa); - if (!list.isEmpty()) { - String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName())); - Map params = Maps.newHashMap(); - params.put("Attach", c); - Player attachedTo = player.getController().chooseSingleEntityForEffect(list, sa, title, params); - c.attachToEntity(attachedTo); - } - else { // When it should enter the battlefield attached to an illegal permanent it fails - continue; - } + if (champion) { + final Map runParams = AbilityKey.mapFromCard(source); + runParams.put(AbilityKey.Championed, c); + game.getTriggerHandler().runTrigger(TriggerType.Championed, runParams, false); } - if (addToCombat(c, c.getController(), sa, "Attacking", "Blocking")) { - combatChanged = true; - } - - // need to be facedown before it hits the battlefield in case of Replacement Effects or Trigger - if (sa.hasParam("FaceDown")) { - c.turnFaceDown(true); - - // set New Pt doesn't work because this values need to be copyable for clone effects - if (sa.hasParam("FaceDownPower")) { - c.setBasePower(AbilityUtils.calculateAmount( - source, sa.getParam("FaceDownPower"), sa)); + if (remember) { + source.addRemembered(movedCard); + // addRememberedFromCardState ? + if (c.getMeldedWith() != null) { + Card meld = game.getCardState(c.getMeldedWith(), null); + if (meld != null) { + source.addRemembered(meld); + } } - if (sa.hasParam("FaceDownToughness")) { - c.setBaseToughness(AbilityUtils.calculateAmount( - source, sa.getParam("FaceDownToughness"), sa)); - } - - if (sa.hasParam("FaceDownAddType")) { - c.addType(Arrays.asList(sa.getParam("FaceDownAddType").split(" & "))); - } - - if (sa.hasParam("FaceDownPower") || sa.hasParam("FaceDownToughness") - || sa.hasParam("FaceDownAddType")) { - final GameCommand unanimate = new GameCommand() { - private static final long serialVersionUID = 8853789549297846163L; - - @Override - public void run() { - c.clearStates(CardStateName.FaceDown, true); - } - }; - - c.addFaceupCommand(unanimate); + if (c.hasMergedCard()) { + for (final Card card : c.getMergedCards()) { + if (card == c) continue; + source.addRemembered(card); + } } } - movedCard = game.getAction().moveToPlay(c, c.getController(), cause, moveParams); - if (sa.hasParam("Tapped")) { - movedCard.setTapped(true); - } else if (sa.hasParam("Untapped")) { - c.setTapped(false); + if (forget) { + source.removeRemembered(movedCard); } - - movedCard.setTimestamp(ts); - } - else if (destination.equals(ZoneType.Exile)) { - movedCard = game.getAction().exile(c, sa, moveParams); - if (!c.isToken()) { - Card host = sa.getOriginalHost(); - if (host == null) { - host = sa.getHostCard(); - } - movedCard.setExiledWith(host); - movedCard.setExiledBy(host.getController()); - } - if (sa.hasParam("ExileFaceDown")) { - movedCard.turnFaceDown(true); - } - if (sa.hasParam("Foretold")) { - movedCard.setForetold(true); - movedCard.setForetoldThisTurn(true); - movedCard.setForetoldByEffect(true); - // look at the exiled card - movedCard.addMayLookTemp(sa.getActivatingPlayer()); - } - } - else { - movedCard = game.getAction().moveTo(destination, c, 0, cause, moveParams); - } - - movedCards.add(movedCard); - - if (originZone != null) { - triggerList.put(originZone.getZoneType(), movedCard.getZone().getZoneType(), movedCard); - - if (c.getMeldedWith() != null) { - Card meld = game.getCardState(c.getMeldedWith(), null); - if (meld != null) { - triggerList.put(originZone.getZoneType(), movedCard.getZone().getZoneType(), meld); - } - } - if (c.hasMergedCard()) { - for (final Card card : c.getMergedCards()) { - if (card == c) continue; - triggerList.put(originZone.getZoneType(), movedCard.getZone().getZoneType(), card); + // for imprinted since this doesn't use Target + if (imprint) { + source.addImprintedCard(movedCard); + if (c.hasMergedCard()) { + for (final Card card : c.getMergedCards()) { + if (card == c) continue; + source.addImprintedCard(card); + } } } } - if (champion) { - final Map runParams = AbilityKey.mapFromCard(source); - runParams.put(AbilityKey.Championed, c); - game.getTriggerHandler().runTrigger(TriggerType.Championed, runParams, false); + if (((!ZoneType.Battlefield.equals(destination) && changeType != null && !defined && !changeType.equals("Card")) + || (sa.hasParam("Reveal") && !movedCards.isEmpty())) && !sa.hasParam("NoReveal")) { + game.getAction().reveal(movedCards, player); } - if (remember) { - source.addRemembered(movedCard); - // addRememberedFromCardState ? - if (c.getMeldedWith() != null) { - Card meld = game.getCardState(c.getMeldedWith(), null); - if (meld != null) { - source.addRemembered(meld); - } - } - if (c.hasMergedCard()) { - for (final Card card : c.getMergedCards()) { - if (card == c) continue; - source.addRemembered(card); - } - } + if ((origin.contains(ZoneType.Library) && !destination.equals(ZoneType.Library) && !defined && shuffleMandatory) + || (sa.hasParam("Shuffle") && "True".equals(sa.getParam("Shuffle")))) { + player.shuffle(sa); } - if (forget) { - source.removeRemembered(movedCard); + + if (sa.hasParam("AtEOT") && !movedCards.isEmpty()) { + registerDelayedTrigger(sa, sa.getParam("AtEOT"), movedCards); } - // for imprinted since this doesn't use Target - if (imprint) { - source.addImprintedCard(movedCard); - if (c.hasMergedCard()) { - for (final Card card : c.getMergedCards()) { - if (card == c) continue; - source.addImprintedCard(card); - } - } + + if (combatChanged) { + game.updateCombatForView(); + game.fireEvent(new GameEventCombatChanged()); + } + triggerList.triggerChangesZoneAll(game); + + if (sa.hasParam("UntilHostLeavesPlay")) { + source.addLeavesPlayCommand(untilHostLeavesPlayCommand(triggerList, source)); } } + } - if (((!ZoneType.Battlefield.equals(destination) && changeType != null && !defined && !changeType.equals("Card")) - || (sa.hasParam("Reveal") && !movedCards.isEmpty())) && !sa.hasParam("NoReveal")) { - game.getAction().reveal(movedCards, player); - } - - if ((origin.contains(ZoneType.Library) && !destination.equals(ZoneType.Library) && !defined && shuffleMandatory) - || (sa.hasParam("Shuffle") && "True".equals(sa.getParam("Shuffle")))) { - player.shuffle(sa); - } - - if (sa.hasParam("AtEOT") && !movedCards.isEmpty()) { - registerDelayedTrigger(sa, sa.getParam("AtEOT"), movedCards); - } - - if (combatChanged) { - game.updateCombatForView(); - game.fireEvent(new GameEventCombatChanged()); - } - triggerList.triggerChangesZoneAll(game); - - if (sa.hasParam("UntilHostLeavesPlay")) { - source.addLeavesPlayCommand(untilHostLeavesPlayCommand(triggerList, source)); - } - - // remove Controlled While Searching - if (controlTimestamp != null) { - player.removeController(controlTimestamp); - } + private static class HiddenOriginChoices { + boolean shuffleMandatory; + boolean searchedLibrary; + CardCollection chosenCards; + int libraryPos; + List origin; + ZoneType destination; } private static boolean allowMultiSelect(Player decider, SpellAbility 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 99968f50479..99af3c9608a 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 @@ -5,10 +5,10 @@ import java.util.Map; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import forge.game.Game; import forge.game.GameActionUtil; -import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; @@ -21,7 +21,6 @@ import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerPredicates; import forge.game.spellability.SpellAbility; -import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.Aggregates; import forge.util.Lang; @@ -112,7 +111,6 @@ public class DiscardEffect extends SpellAbilityEffect { final Game game = source.getGame(); //final boolean anyNumber = sa.hasParam("AnyNumber"); - final List discarded = Lists.newArrayList(); final List targets = getTargetPlayers(sa), discarders; Player firstTarget = null; @@ -127,11 +125,10 @@ public class DiscardEffect extends SpellAbilityEffect { discarders = targets; } - final CardZoneTable table = new CardZoneTable(); + Map discardedMap = Maps.newHashMap(); for (final Player p : discarders) { - boolean firstDiscard = p.getNumDiscardedThisTurn() == 0; - final CardCollection discardedByPlayer = new CardCollection(); + CardCollectionView toBeDiscarded = new CardCollection(); if ((mode.equals("RevealTgtChoose") && firstTarget != null) || !sa.usesTargeting() || p.canBeTargetedBy(sa)) { if (sa.hasParam("RememberDiscarder") && p.canDiscardBy(sa)) { source.addRemembered(p); @@ -145,17 +142,10 @@ public class DiscardEffect extends SpellAbilityEffect { boolean runDiscard = !sa.hasParam("Optional") || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, sa.getParam("DiscardMessage")); if (runDiscard) { - CardCollectionView toDiscard = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedCards"), sa); + toBeDiscarded = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedCards"), sa); - if (toDiscard.size() > 1) { - toDiscard = GameActionUtil.orderCardsByTheirOwners(game, toDiscard, ZoneType.Graveyard, sa); - } - - for (final Card c : toDiscard) { - if (p.discard(c, sa, table) != null) { - discarded.add(c); - discardedByPlayer.add(c); - } + if (toBeDiscarded.size() > 1) { + toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); } } } @@ -164,17 +154,10 @@ public class DiscardEffect extends SpellAbilityEffect { if (!p.canDiscardBy(sa)) { continue; } - CardCollectionView toDiscard = p.getCardsIn(ZoneType.Hand); + toBeDiscarded = p.getCardsIn(ZoneType.Hand); - if (toDiscard.size() > 1) { - toDiscard = GameActionUtil.orderCardsByTheirOwners(game, toDiscard, ZoneType.Graveyard, sa); - } - - for(Card c : Lists.newArrayList(toDiscard)) { // without copying will get concurrent modification exception - if (p.discard(c, sa, table) != null) { - discarded.add(c); - discardedByPlayer.add(c); - } + if (toBeDiscarded.size() > 1) { + toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); } } @@ -182,16 +165,9 @@ public class DiscardEffect extends SpellAbilityEffect { if (!p.canDiscardBy(sa)) { continue; } - CardCollectionView dPHand = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), "Card.IsNotRemembered", p, source, sa); - if (dPHand.size() > 1) { - dPHand = GameActionUtil.orderCardsByTheirOwners(game, dPHand, ZoneType.Graveyard, sa); - } - - for (final Card c : dPHand) { - if (p.discard(c, sa, table) != null) { - discarded.add(c); - discardedByPlayer.add(c); - } + toBeDiscarded = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), "Card.IsNotRemembered", p, source, sa); + if (toBeDiscarded.size() > 1) { + toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); } } @@ -222,16 +198,9 @@ public class DiscardEffect extends SpellAbilityEffect { list.remove(disc); } - CardCollectionView toDiscardView = toDiscard; - if (toDiscard.size() > 1) { - toDiscardView = GameActionUtil.orderCardsByTheirOwners(game, toDiscard, ZoneType.Graveyard, sa); - } - - for (Card c : toDiscardView) { - if (p.discard(c, sa, table) != null) { - discarded.add(c); - discardedByPlayer.add(c); - } + toBeDiscarded = toDiscard; + if (toBeDiscarded.size() > 1) { + toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); } } } @@ -239,20 +208,13 @@ public class DiscardEffect extends SpellAbilityEffect { if (!p.canDiscardBy(sa)) { continue; } - if( numCardsInHand > 0 ) { + if (numCardsInHand > 0) { CardCollectionView hand = p.getCardsIn(ZoneType.Hand); hand = CardLists.filter(hand, Presets.NON_TOKEN); - CardCollectionView toDiscard = p.getController().chooseCardsToDiscardUnlessType(Math.min(numCards, numCardsInHand), hand, sa.getParam("UnlessType"), sa); + toBeDiscarded = p.getController().chooseCardsToDiscardUnlessType(Math.min(numCards, numCardsInHand), hand, sa.getParam("UnlessType"), sa); - if (toDiscard.size() > 1) { - toDiscard = GameActionUtil.orderCardsByTheirOwners(game, toDiscard, ZoneType.Graveyard, sa); - } - - for (Card c : toDiscard) { - if (c.getController().discard(c, sa, table) != null) { - discarded.add(c); - discardedByPlayer.add(c); - } + if (toBeDiscarded.size() > 1) { + toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game,toBeDiscarded, ZoneType.Graveyard, sa); } } } @@ -275,18 +237,10 @@ public class DiscardEffect extends SpellAbilityEffect { "X", Integer.toString(AbilityUtils.calculateAmount(source, "X", sa))); } - CardCollectionView dPChHand = CardLists.getValidCards(dPHand, valid.split(","), source.getController(), source, sa); - dPChHand = CardLists.filter(dPChHand, Presets.NON_TOKEN); - if (dPChHand.size() > 1) { - dPChHand = GameActionUtil.orderCardsByTheirOwners(game, dPChHand, ZoneType.Graveyard, sa); - } - - // Reveal cards that will be discarded? - for (final Card c : dPChHand) { - if (p.discard(c, sa, table) != null) { - discarded.add(c); - discardedByPlayer.add(c); - } + toBeDiscarded = CardLists.getValidCards(dPHand, valid.split(","), source.getController(), source, sa); + toBeDiscarded = CardLists.filter(toBeDiscarded, Presets.NON_TOKEN); + if (toBeDiscarded.size() > 1) { + toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); } } else if (mode.equals("RevealYouChoose") || mode.equals("RevealTgtChoose") || mode.equals("TgtChoose")) { CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand); @@ -321,47 +275,27 @@ public class DiscardEffect extends SpellAbilityEffect { int min = sa.hasParam("AnyNumber") || sa.hasParam("Optional") ? 0 : Math.min(validCards.size(), numCards); int max = sa.hasParam("AnyNumber") ? validCards.size() : Math.min(validCards.size(), numCards); - CardCollectionView toBeDiscarded = validCards.isEmpty() ? null : chooser.getController().chooseCardsToDiscardFrom(p, sa, validCards, min, max); + toBeDiscarded = validCards.isEmpty() ? null : chooser.getController().chooseCardsToDiscardFrom(p, sa, validCards, min, max); - if (toBeDiscarded != null) { - if (toBeDiscarded.size() > 1) { - toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); - } + if (toBeDiscarded.size() > 1) { + toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); + } - if (mode.startsWith("Reveal") ) { - p.getController().reveal(toBeDiscarded, ZoneType.Hand, p, Localizer.getInstance().getMessage("lblPlayerHasChosenCardsFrom", chooser.getName())); - } - for (Card card : toBeDiscarded) { - if (card == null) { continue; } - if (p.discard(card, sa, table) != null) { - discarded.add(card); - discardedByPlayer.add(card); - } - } + if (mode.startsWith("Reveal") ) { + p.getController().reveal(toBeDiscarded, ZoneType.Hand, p, Localizer.getInstance().getMessage("lblPlayerHasChosenCardsFrom", chooser.getName())); } } } - - if (!discardedByPlayer.isEmpty()) { - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, p); - runParams.put(AbilityKey.Cards, discardedByPlayer); - runParams.put(AbilityKey.Cause, sa); - runParams.put(AbilityKey.FirstTime, firstDiscard); - game.getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false); - if (sa.hasParam("RememberDiscardingPlayers")) { - source.addRemembered(p); - } - } + discardedMap.put(p, toBeDiscarded); } + discard(sa, table, discardedMap); + if (sa.hasParam("RememberDiscarded")) { - for (final Card c : discarded) { - source.addRemembered(c); - } + source.addRemembered(discardedMap.values()); } // run trigger if something got milled - table.triggerChangesZoneAll(source.getGame()); + table.triggerChangesZoneAll(game); } // discardResolve() } diff --git a/forge-gui/res/cardsfolder/s/scheming_symmetry.txt b/forge-gui/res/cardsfolder/s/scheming_symmetry.txt index ae40285e223..83a13f9e4b9 100644 --- a/forge-gui/res/cardsfolder/s/scheming_symmetry.txt +++ b/forge-gui/res/cardsfolder/s/scheming_symmetry.txt @@ -1,7 +1,6 @@ Name:Scheming Symmetry ManaCost:B Types:Sorcery -A:SP$ RepeatEach | Cost$ B | ValidTgts$ Player | TargetMin$ 2 | TargetMax$ 2 | RepeatPlayers$ Targeted | RepeatSubAbility$ DBChange | StackDescription$ SpellDescription | SpellDescription$ Choose two target players. Each of them searches their library for a card, then shuffles their library and puts that card on top of it. -SVar:DBChange:DB$ ChangeZone | DefinedPlayer$ Remembered | Chooser$ Remembered | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | Mandatory$ True | ChangeType$ Card | ChangeNum$ 1 +A:SP$ ChangeZone | Cost$ B | DefinedPlayer$ Targeted | ValidTgts$ Player | TargetMin$ 2 | TargetMax$ 2 | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Card | ChangeNum$ 1 | StackDescription$ SpellDescription | SpellDescription$ Choose two target players. Each of them searches their library for a card, then shuffles their library and puts that card on top of it. AI:RemoveDeck:All Oracle:Choose two target players. Each of them searches their library for a card, then shuffles their library and puts that card on top of it.