diff --git a/Mage.Client/src/main/java/mage/client/cards/CardArea.java b/Mage.Client/src/main/java/mage/client/cards/CardArea.java index d49afeb9b03..82583d73d94 100644 --- a/Mage.Client/src/main/java/mage/client/cards/CardArea.java +++ b/Mage.Client/src/main/java/mage/client/cards/CardArea.java @@ -18,10 +18,8 @@ import javax.swing.*; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.Comparator; +import java.util.*; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; /** @@ -258,7 +256,7 @@ public class CardArea extends JPanel implements CardEventProducer { this.reloaded = false; } - public void selectCards(List selected) { + public void selectCards(Set selected) { for (Component component : cardArea.getComponents()) { if (component instanceof MageCard) { MageCard mageCard = (MageCard) component; @@ -269,7 +267,7 @@ public class CardArea extends JPanel implements CardEventProducer { } } - public void markCards(List marked) { + public void markCards(Set marked) { for (Component component : cardArea.getComponents()) { if (component instanceof MageCard) { MageCard mageCard = (MageCard) component; diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/DeckImportClipboardDialog.java b/Mage.Client/src/main/java/mage/client/deckeditor/DeckImportClipboardDialog.java index f08bdbd4366..11347eb59ce 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/DeckImportClipboardDialog.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/DeckImportClipboardDialog.java @@ -1,5 +1,6 @@ package mage.client.deckeditor; +import mage.cards.decks.importer.MtgaImporter; import mage.client.MageFrame; import mage.client.dialog.MageDialog; import mage.util.DeckUtil; @@ -20,11 +21,18 @@ import java.util.Optional; public class DeckImportClipboardDialog extends MageDialog { private static final String FORMAT_TEXT = - "// Example:\n" + - "//1 Library of Congress\n" + - "//1 Cryptic Gateway\n" + - "//1 Azami, Lady of Scrolls\n" + - "// NB: This is slow as, and will lock your screen :)\n" + + "// MTGO format example:\n" + + "// 1 Library of Congress\n" + + "// 3 Cryptic Gateway\n" + + "//\n" + + "// MTGA, moxfield, archidekt format example:\n" + + "// Deck\n" + + "// 4 Accumulated Knowledge (A25) 40\n" + + "// 2 Adarkar Wastes (EOC) 147\n" + + "// Commander\n" + + "// 1 Grizzly Bears\n" + + "//\n" + + "// Importing deck can take some time to finish\n" + "\n" + "// Your current clipboard:\n" + "\n"; @@ -68,17 +76,19 @@ public class DeckImportClipboardDialog extends MageDialog { } private void onOK() { - String decklist = editData.getText(); - decklist = decklist.replace(FORMAT_TEXT, ""); + String importData = editData.getText(); + importData = importData.replace(FORMAT_TEXT, "").trim(); + // find possible data format String tempDeckPath; - // This dialog also accepts a paste in .mtga format - if (decklist.startsWith("Deck\n")) { // An .mtga list always starts with the first line being "Deck". This kind of paste is processed as .mtga - tempDeckPath = DeckUtil.writeTextToTempFile("cbimportdeck", ".mtga", decklist); + if (MtgaImporter.isMTGA(importData)) { + // MTGA or Moxfield + tempDeckPath = DeckUtil.writeTextToTempFile("cbimportdeck", ".mtga", importData); } else { - // If the paste is not .mtga format, it's processed as plaintext - tempDeckPath = DeckUtil.writeTextToTempFile(decklist); + // text + tempDeckPath = DeckUtil.writeTextToTempFile(importData); } + if (this.callback != null) { callback.onImportDone(tempDeckPath); } diff --git a/Mage.Client/src/main/java/mage/client/dialog/ShowCardsDialog.java b/Mage.Client/src/main/java/mage/client/dialog/ShowCardsDialog.java index 34090e0ff8c..0b366daa334 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/ShowCardsDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/ShowCardsDialog.java @@ -100,11 +100,11 @@ cardArea.loadCards(showCards, bigCard, gameId); if (options != null) { if (options.containsKey("chosenTargets")) { - java.util.List chosenCards = (java.util.List) options.get("chosenTargets"); + java.util.Set chosenCards = (java.util.Set) options.get("chosenTargets"); cardArea.selectCards(chosenCards); } if (options.containsKey("possibleTargets")) { - java.util.List choosableCards = (java.util.List) options.get("possibleTargets"); + java.util.Set choosableCards = (java.util.Set) options.get("possibleTargets"); cardArea.markCards(choosableCards); } if (options.containsKey("queryType") && options.get("queryType") == QueryType.PICK_ABILITY) { diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 90074e9331b..662038293cb 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -218,17 +218,9 @@ public final class GamePanel extends javax.swing.JPanel { }); } - public List getPossibleTargets() { - if (options != null && options.containsKey("possibleTargets")) { - return (List) options.get("possibleTargets"); - } else { - return Collections.emptyList(); - } - } - public Set getChosenTargets() { if (options != null && options.containsKey("chosenTargets")) { - return new HashSet<>((List) options.get("chosenTargets")); + return (Set) options.get("chosenTargets"); } else { return Collections.emptySet(); } diff --git a/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java b/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java index ef4e38fba03..6b2ebad8a66 100644 --- a/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java +++ b/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java @@ -10,10 +10,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.URL; +import java.net.*; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; @@ -93,7 +90,9 @@ public class XmageURLConnection { initDefaultProxy(); try { - URL url = new URL(this.url); + // convert utf8 url to ascii format (e.g. url encode) + URI uri = new URI(this.url); + URL url = new URL(uri.toASCIIString()); // proxy settings if (this.proxy != null) { @@ -107,7 +106,7 @@ public class XmageURLConnection { this.connection.setReadTimeout(CONNECTION_READING_TIMEOUT_MS); initDefaultHeaders(); - } catch (IOException e) { + } catch (IOException | URISyntaxException e) { this.connection = null; } } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java index 18de4e36e2d..522ae5632af 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java @@ -107,7 +107,7 @@ public class GathererSets implements Iterable { "JMP", "2XM", "ZNR", "KLR", "CMR", "KHC", "KHM", "TSR", "STX", "STA", "C21", "MH2", "AFR", "AFC", "J21", "MID", "MIC", "VOW", "VOC", "YMID", "NEC", "YNEO", "NEO", "SNC", "NCC", "CLB", "2X2", "DMU", "DMC", "40K", "GN3", - "UNF", "BRO", "BRC", "BOT", "J22", "DMR", "ONE", "ONC", + "UNF", "BRO", "BRC", "BOT", "J22", "DMR", "ONE", "ONC", "SCH", "MOM", "MOC", "MUL", "MAT", "LTR", "CMM", "WOE", "WHO", "RVR", "WOT", "WOC", "SPG", "LCI", "LCC", "REX", "PIP", "MKM", "MKC", "CLU", "OTJ", "OTC", "OTP", "BIG", "MH3", "M3C", "ACR", "BLB", "BLC", "DSK", "DSC", diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java index 29b20a0d26b..2e0cb302e98 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java @@ -515,6 +515,7 @@ public class ScryfallImageSupportCards { add("SLX"); // Universes Within add("CLB"); // Commander Legends: Battle for Baldur's Gate add("2X2"); // Double Masters 2022 + add("SCH"); // Store Championships add("DMU"); // Dominaria United add("DMC"); // Dominaria United Commander add("YDMU"); // Alchemy: Dominaria @@ -569,9 +570,11 @@ public class ScryfallImageSupportCards { add("BIG"); // The Big Score add("MH3"); // Modern Horizons 3 add("M3C"); // Modern Horizons 3 Commander + add("H2R"); // Modern Horizons 2 Timeshifts add("ACR"); // Assassin's Creed add("BLB"); // Bloomburrow add("BLC"); // Bloomburrow Commander + add("PCBB"); // Cowboy Bebop add("MB2"); // Mystery Booster 2 add("DSK"); // Duskmourn: House of Horror add("DSC"); // Duskmourn: House of Horror Commander @@ -579,21 +582,25 @@ public class ScryfallImageSupportCards { add("J25"); // Foundations Jumpstart add("PIO"); // Pioneer Masters add("PW25"); // Wizards Play Network 2025 + add("PSPL"); // Spotlight Series add("INR"); // Innistrad Remastered add("PF25"); // MagicFest 2025 add("DFT"); // Aetherdrift add("DRC"); // Aetherdrift Commander + add("PLG25"); // Love Your LGS 2025 add("TDM"); // Tarkir: Dragonstorm add("TDC"); // Tarkir: Dragonstorm Commander add("FIN"); // Final Fantasy add("FIC"); // Final Fantasy Commander add("FCA"); // Final Fantasy: Through the Ages + add("PSS5"); // FIN Standard Showdown add("EOE"); // Edge of Eternities add("EOC"); // Edge of Eternities Commander add("EOS"); // Edge of Eternities: Stellar Sights add("SPM"); // Marvel's Spider-Man add("SPE"); // Marvel's Spider-Man Eternal add("TLA"); // Avatar: The Last Airbender + add("TLE"); // Avatar: The Last Airbender Eternal // Custom sets using Scryfall images - must provide a direct link for each card in directDownloadLinks add("CALC"); // Custom Alchemized versions of existing cards diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java index 8f9b6def8be..a707edc60c9 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java @@ -837,6 +837,8 @@ public class ScryfallImageSupportTokens { put("SLD/Hydra", "https://api.scryfall.com/cards/sld/1334?format=image"); put("SLD/Icingdeath, Frost Tongue", "https://api.scryfall.com/cards/sld/1018?format=image"); put("SLD/Marit Lage", "https://api.scryfall.com/cards/sld/1681?format=image"); + put("SLD/Mechtitan/1", "https://api.scryfall.com/cards/sld/1969?format=image"); + put("SLD/Mechtitan/2", "https://api.scryfall.com/cards/sld/1969/en?format=image&face=back"); put("SLD/Mechtitan", "https://api.scryfall.com/cards/sld/1969?format=image"); put("SLD/Myr", "https://api.scryfall.com/cards/sld/2101?format=image"); put("SLD/Saproling", "https://api.scryfall.com/cards/sld/1139?format=image"); diff --git a/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java b/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java index f90f7e8df0a..d81812d92b8 100644 --- a/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java +++ b/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java @@ -43,6 +43,12 @@ public class DownloaderTest { Assert.assertTrue("must have text data (redirect to login page)", s.contains("Sign in to GitHub")); } + @Test + public void test_DownloadText_ScryfallUtf8() { + String s = XmageURLConnection.downloadText("https://api.scryfall.com/cards/sld/379★/en"); + Assert.assertTrue("must have text data (utf8 url must work)", s.contains("Zndrsplt, Eye of Wisdom")); + } + @Test public void test_DownloadFile_ByHttp() throws IOException { // use any public image here diff --git a/Mage.Common/src/main/java/mage/utils/SystemUtil.java b/Mage.Common/src/main/java/mage/utils/SystemUtil.java index 408e0b0de29..c8144e49def 100644 --- a/Mage.Common/src/main/java/mage/utils/SystemUtil.java +++ b/Mage.Common/src/main/java/mage/utils/SystemUtil.java @@ -785,7 +785,7 @@ public final class SystemUtil { * @return added cards list */ private static Set addNewCardsToGame(Game game, String cardName, String setCode, int amount, Player owner) { - CardInfo cardInfo = CardLookup.instance.lookupCardInfo(cardName, setCode).orElse(null); + CardInfo cardInfo = CardLookup.instance.lookupCardInfo(cardName, setCode, null); if (cardInfo == null || amount <= 0) { return null; } diff --git a/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java index 2ae8d4cae79..27c89d98863 100644 --- a/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java +++ b/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java @@ -7,6 +7,7 @@ import mage.game.Game; import mage.players.Player; import mage.target.Target; import mage.target.TargetPermanent; +import mage.target.TargetPlayer; import mage.target.common.TargetPermanentOrPlayer; /** @@ -76,6 +77,10 @@ abstract class BaseTestableDialog implements TestableDialog { return createAnyTarget(min, max, false); } + static Target createPlayerTarget(int min, int max, boolean notTarget) { + return new TargetPlayer(min, max, notTarget); + } + private static Target createAnyTarget(int min, int max, boolean notTarget) { return new TargetPermanentOrPlayer(min, max).withNotTarget(notTarget); } diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java index 017aa055b27..3a949ee5350 100644 --- a/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java +++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java @@ -126,6 +126,19 @@ class ChooseTargetTestableDialog extends BaseTestableDialog { runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1-3", createImpossibleTarget(1, 3)).aiMustChoose(false, 0)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 2-3", createImpossibleTarget(2, 3)).aiMustChoose(false, 0)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible max", createImpossibleTarget(0, Integer.MAX_VALUE)).aiMustChoose(false, 0)); + // + // additional tests for 2 possible options limitation + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 0", createPlayerTarget(0, 0, notTarget)).aiMustChoose(false, 0)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 0-1", createPlayerTarget(0, 1, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 0-2", createPlayerTarget(0, 2, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 0-5", createPlayerTarget(0, 5, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 1", createPlayerTarget(1, 1, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 1-2", createPlayerTarget(1, 2, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 1-5", createPlayerTarget(1, 5, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 2", createPlayerTarget(2, 2, notTarget)).aiMustChoose(true, 2)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 2-5", createPlayerTarget(2, 5, notTarget)).aiMustChoose(true, 2)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 3", createPlayerTarget(3, 3, notTarget)).aiMustChoose(false, 0)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 3-5", createPlayerTarget(3, 5, notTarget)).aiMustChoose(false, 0)); } } } diff --git a/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java b/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java index 0970d8ecc94..f4805f2184e 100644 --- a/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java +++ b/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java @@ -8,10 +8,7 @@ import mage.constants.Outcome; import mage.game.Game; import mage.players.Player; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; /** @@ -120,10 +117,8 @@ public class TestableDialogsRunner { choice = prepareSelectDialogChoice(needGroup); player.choose(Outcome.Benefit, choice, game); if (choice.getChoiceKey() != null) { - int needIndex = Integer.parseInt(choice.getChoiceKey()); - if (needIndex < this.dialogs.size()) { - needDialog = this.dialogs.get(needIndex); - } + int needRegNumber = Integer.parseInt(choice.getChoiceKey()); + needDialog = this.dialogs.getOrDefault(needRegNumber, null); } } if (needDialog == null) { @@ -144,15 +139,20 @@ public class TestableDialogsRunner { Choice choice = new ChoiceImpl(false); choice.setMessage("Choose dialogs group to run"); + // use min reg number for groups + Map groupNumber = new HashMap<>(); + this.dialogs.values().forEach(dialog -> { + groupNumber.put(dialog.getGroup(), Math.min(groupNumber.getOrDefault(dialog.getGroup(), Integer.MAX_VALUE), dialog.getRegNumber())); + }); + // main groups - int recNumber = 0; for (int i = 0; i < groups.size(); i++) { - recNumber++; String group = groups.get(i); + Integer groupMinNumber = groupNumber.getOrDefault(group, 0); choice.withItem( String.valueOf(i), - String.format("%02d. %s", recNumber, group), - recNumber, + String.format("%02d. %s", groupMinNumber, group), + groupMinNumber, ChoiceHintType.TEXT, String.join("
", group) ); @@ -186,18 +186,15 @@ public class TestableDialogsRunner { private Choice prepareSelectDialogChoice(String needGroup) { Choice choice = new ChoiceImpl(false); choice.setMessage("Choose game dialog to run from " + needGroup); - int recNumber = 0; - for (int i = 0; i < this.dialogs.size(); i++) { - TestableDialog dialog = this.dialogs.get(i); + for (TestableDialog dialog : this.dialogs.values()) { if (!dialog.getGroup().equals(needGroup)) { continue; } - recNumber++; String info = String.format("%s - %s - %s", dialog.getGroup(), dialog.getName(), dialog.getDescription()); choice.withItem( - String.valueOf(i), - String.format("%02d. %s", recNumber, info), - recNumber, + String.valueOf(dialog.getRegNumber()), + String.format("%02d. %s", dialog.getRegNumber(), info), + dialog.getRegNumber(), ChoiceHintType.TEXT, String.join("
", info) ); diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index f25152f9a90..5ab8241d41c 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -219,6 +219,7 @@ public class ComputerPlayer6 extends ComputerPlayer { } // Condition to stop deeper simulation if (SimulationNode2.nodeCount > MAX_SIMULATED_NODES_PER_ERROR) { + // how-to fix: make sure you are disabled debug mode by COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = false throw new IllegalStateException("AI ERROR: too much nodes (possible actions)"); } if (depth <= 0 @@ -398,20 +399,23 @@ public class ComputerPlayer6 extends ComputerPlayer { } protected void resolve(SimulationNode2 node, int depth, Game game) { - StackObject stackObject = game.getStack().getFirst(); + StackObject stackObject = game.getStack().getFirstOrNull(); + if (stackObject == null) { + throw new IllegalStateException("Catch empty stack on resolve (something wrong with sim code)"); + } if (stackObject instanceof StackAbility) { // AI hint for search effects (calc all possible cards for best score) SearchEffect effect = getSearchEffect((StackAbility) stackObject); if (effect != null && stackObject.getControllerId().equals(playerId)) { Target target = effect.getTarget(); - if (!target.isChoiceCompleted(getId(), (StackAbility) stackObject, game)) { + if (!target.isChoiceCompleted(getId(), (StackAbility) stackObject, game, null)) { for (UUID targetId : target.possibleTargets(stackObject.getControllerId(), stackObject.getStackAbility(), game)) { Game sim = game.createSimulationForAI(); StackAbility newAbility = (StackAbility) stackObject.copy(); SearchEffect newEffect = getSearchEffect(newAbility); newEffect.getTarget().addTarget(targetId, newAbility, sim); - sim.getStack().push(newAbility); + sim.getStack().push(sim, newAbility); SimulationNode2 newNode = new SimulationNode2(node, sim, depth, stackObject.getControllerId()); node.children.add(newNode); newNode.getTargets().add(targetId); @@ -498,7 +502,7 @@ public class ComputerPlayer6 extends ComputerPlayer { } logger.warn("Possible freeze chain:"); if (root != null && chain.isEmpty()) { - logger.warn(" - unknown use case"); // maybe can't finish any calc, maybe related to target options, I don't know + logger.warn(" - unknown use case (too many possible targets?)"); // maybe can't finish any calc, maybe related to target options, I don't know } chain.forEach(s -> { logger.warn(" - " + s); @@ -639,7 +643,7 @@ public class ComputerPlayer6 extends ComputerPlayer { return "unknown"; }) .collect(Collectors.joining(", ")); - logger.info(String.format("Sim Prio [%d] -> with choices (TODO): [%d] (%s)", + logger.info(String.format("Sim Prio [%d] -> with possible choices: [%d] (%s)", depth, currentNode.getDepth(), printDiffScore(currentScore - prevScore), @@ -648,14 +652,18 @@ public class ComputerPlayer6 extends ComputerPlayer { } else if (!currentNode.getChoices().isEmpty()) { // ON CHOICES String choicesInfo = String.join(", ", currentNode.getChoices()); - logger.info(String.format("Sim Prio [%d] -> with choices (TODO): [%d] (%s)", + logger.info(String.format("Sim Prio [%d] -> with possible choices (must not see that code): [%d] (%s)", depth, currentNode.getDepth(), printDiffScore(currentScore - prevScore), choicesInfo) ); } else { - throw new IllegalStateException("AI CALC ERROR: unknown calculation result (no abilities, no targets, no choices)"); + logger.info(String.format("Sim Prio [%d] -> with do nothing: [%d]", + depth, + currentNode.getDepth(), + printDiffScore(currentScore - prevScore)) + ); } } } @@ -886,10 +894,10 @@ public class ComputerPlayer6 extends ComputerPlayer { } UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); - if (!target.isChoiceCompleted(abilityControllerId, source, game)) { + if (!target.isChoiceCompleted(abilityControllerId, source, game, cards)) { for (UUID targetId : targets) { target.addTarget(targetId, source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, cards)) { targets.clear(); return true; } @@ -906,10 +914,10 @@ public class ComputerPlayer6 extends ComputerPlayer { } UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); - if (!target.isChoiceCompleted(abilityControllerId, source, game)) { + if (!target.isChoiceCompleted(abilityControllerId, source, game, cards)) { for (UUID targetId : targets) { target.add(targetId, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, cards)) { targets.clear(); return true; } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java index 4452e3e682e..9d7347f7d2b 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java @@ -105,7 +105,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer { return list; } - protected void simulateOptions(Game game) { + private void simulateOptions(Game game) { List playables = game.getPlayer(playerId).getPlayable(game, isSimulatedPlayer); for (ActivatedAbility ability : playables) { if (ability.isManaAbility()) { @@ -176,6 +176,10 @@ public final class SimulatedPlayer2 extends ComputerPlayer { return options; } + // remove invalid targets + // TODO: is it useless cause it already filtered before? + options.removeIf(option -> !option.getTargets().isChosen(game)); + if (AI_SIMULATE_ALL_BAD_AND_GOOD_TARGETS) { return options; } @@ -314,14 +318,17 @@ public final class SimulatedPlayer2 extends ComputerPlayer { Ability ability = source.copy(); List options = getPlayableOptions(ability, game); if (options.isEmpty()) { + // no options - activate as is logger.debug("simulating -- triggered ability:" + ability); - game.getStack().push(new StackAbility(ability, playerId)); + game.getStack().push(game, new StackAbility(ability, playerId)); if (ability.activate(game, false) && ability.isUsesStack()) { game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId())); } game.applyEffects(); game.getPlayers().resetPassed(); } else { + // many options - activate and add to sims tree + // TODO: AI run all sims, but do not use best option for triggers yet SimulationNode2 parent = (SimulationNode2) game.getCustomData(); int depth = parent.getDepth() - 1; if (depth == 0) { @@ -337,16 +344,16 @@ public final class SimulatedPlayer2 extends ComputerPlayer { protected void addAbilityNode(SimulationNode2 parent, Ability ability, int depth, Game game) { Game sim = game.createSimulationForAI(); - sim.getStack().push(new StackAbility(ability, playerId)); + sim.getStack().push(sim, new StackAbility(ability, playerId)); if (ability.activate(sim, false) && ability.isUsesStack()) { - game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId())); + sim.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId())); } sim.applyEffects(); SimulationNode2 newNode = new SimulationNode2(parent, sim, depth, playerId); logger.debug("simulating -- node #:" + SimulationNode2.getCount() + " triggered ability option"); for (Target target : ability.getTargets()) { for (UUID targetId : target.getTargets()) { - newNode.getTargets().add(targetId); // save for info only (real targets in newNode.ability already) + newNode.getTargets().add(targetId); // save for info only (real targets in newNode.game.stack already) } } parent.children.add(newNode); diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 20a8114e4f2..441a45bbfb4 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -142,17 +142,27 @@ public class ComputerPlayer extends PlayerImpl { UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); // nothing to choose, e.g. X=0 - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, fromCards)) { return false; } - // default logic for any targets PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game); possibleTargetsSelector.findNewTargets(fromCards); + + // nothing to choose, e.g. no valid targets + if (!possibleTargetsSelector.hasAnyTargets()) { + return false; + } + + // can't choose + if (!possibleTargetsSelector.hasMinNumberOfTargets()) { + return false; + } + // good targets -- choose as much as possible for (MageItem item : possibleTargetsSelector.getGoodTargets()) { target.add(item.getId(), game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, fromCards)) { return true; } } @@ -226,7 +236,7 @@ public class ComputerPlayer extends PlayerImpl { UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); // nothing to choose, e.g. X=0 - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return false; } @@ -238,6 +248,11 @@ public class ComputerPlayer extends PlayerImpl { return false; } + // can't choose + if (!possibleTargetsSelector.hasMinNumberOfTargets()) { + return false; + } + // KILL PRIORITY if (outcome == Outcome.Damage) { // opponent first @@ -251,7 +266,7 @@ public class ComputerPlayer extends PlayerImpl { int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game); if (leftLife > 0 && leftLife <= target.getAmountRemaining()) { target.addTarget(item.getId(), leftLife, source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -268,7 +283,7 @@ public class ComputerPlayer extends PlayerImpl { int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game); if (leftLife > 0 && leftLife <= target.getAmountRemaining()) { target.addTarget(item.getId(), leftLife, source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -283,7 +298,7 @@ public class ComputerPlayer extends PlayerImpl { continue; } target.addTarget(item.getId(), target.getAmountRemaining(), source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -303,7 +318,7 @@ public class ComputerPlayer extends PlayerImpl { int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game); if (leftLife > 1) { target.addTarget(item.getId(), Math.min(leftLife - 1, target.getAmountRemaining()), source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -322,7 +337,7 @@ public class ComputerPlayer extends PlayerImpl { return !target.getTargets().isEmpty(); } target.addTarget(item.getId(), target.getAmountRemaining(), source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -339,7 +354,7 @@ public class ComputerPlayer extends PlayerImpl { continue; } target.addTarget(item.getId(), target.getAmountRemaining(), source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -355,7 +370,7 @@ public class ComputerPlayer extends PlayerImpl { return !target.getTargets().isEmpty(); } target.addTarget(item.getId(), target.getAmountRemaining(), source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java index c3d9f25ea22..cd3184b2c55 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java @@ -47,7 +47,6 @@ public class PossibleTargetsSelector { // collect new valid targets List found = target.possibleTargets(abilityControllerId, source, game, fromTargetsList).stream() .filter(id -> !target.contains(id)) - .filter(id -> target.canTarget(abilityControllerId, id, source, game)) .map(id -> { Player player = game.getPlayer(id); if (player != null) { @@ -137,6 +136,10 @@ public class PossibleTargetsSelector { } } + public List getAny() { + return this.any; + } + public static boolean isMyItem(UUID abilityControllerId, MageItem item) { if (item instanceof Player) { return item.getId().equals(abilityControllerId); @@ -181,7 +184,12 @@ public class PossibleTargetsSelector { return false; } - boolean hasAnyTargets() { + public boolean hasAnyTargets() { return !this.any.isEmpty(); } + + public boolean hasMinNumberOfTargets() { + return this.target.getMinNumberOfTargets() == 0 + || this.any.size() >= this.target.getMinNumberOfTargets(); + } } \ No newline at end of file diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java index f3dd92bb7be..0649b72732e 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java @@ -121,7 +121,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer { } } if (ability.isUsesStack()) { - game.getStack().push(new StackAbility(ability, playerId)); + game.getStack().push(game, new StackAbility(ability, playerId)); if (ability.activate(game, false)) { game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId())); actionCount++; @@ -187,8 +187,8 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer { abort = true; } - protected boolean chooseRandom(Target target, Game game) { - Set possibleTargets = target.possibleTargets(playerId, game); + private boolean chooseRandom(Target target, Ability source, Game game) { + Set possibleTargets = target.possibleTargets(playerId, source, game); if (possibleTargets.isEmpty()) { return false; } @@ -233,7 +233,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer { @Override public boolean choose(Outcome outcome, Target target, Ability source, Game game) { if (this.isHuman()) { - return chooseRandom(target, game); + return chooseRandom(target, source, game); } return super.choose(outcome, target, source, game); } @@ -241,7 +241,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer { @Override public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map options) { if (this.isHuman()) { - return chooseRandom(target, game); + return chooseRandom(target, source, game); } return super.choose(outcome, target, source, game, options); } @@ -252,7 +252,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer { if (cards.isEmpty()) { return false; } - Set possibleTargets = target.possibleTargets(playerId, cards, source, game); + Set possibleTargets = target.possibleTargets(playerId, source, game, cards); if (possibleTargets.isEmpty()) { return false; } diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index cd7b6569cf8..0b3e78aa195 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -3,7 +3,6 @@ package mage.player.human; import mage.MageIdentifier; import mage.MageObject; import mage.abilities.*; -import mage.abilities.costs.VariableCost; import mage.abilities.costs.common.SacrificeSourceCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCost; @@ -639,8 +638,8 @@ public class HumanPlayer extends PlayerImpl { // Check check if the spell being paid for cares about the color of mana being paid // See: https://github.com/magefree/mage/issues/9070 boolean caresAboutManaColor = false; - if (!game.getStack().isEmpty() && game.getStack().getFirst() instanceof Spell) { - Spell spellBeingCast = (Spell) game.getStack().getFirst(); + if (game.getStack().getFirstOrNull() instanceof Spell) { + Spell spellBeingCast = (Spell) game.getStack().getFirstOrNull(); if (!spellBeingCast.isResolving() && spellBeingCast.getControllerId().equals(this.getId())) { CardImpl card = (CardImpl) game.getCard(spellBeingCast.getSourceId()); caresAboutManaColor = card.caresAboutManaColor(game); @@ -697,7 +696,7 @@ public class HumanPlayer extends PlayerImpl { } // stop on completed, e.g. X=0 - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return false; } @@ -729,7 +728,7 @@ public class HumanPlayer extends PlayerImpl { // continue to next target (example: auto-choose must fill min/max = 2 from 2 possible cards) } else { // manual choose - options.put("chosenTargets", (Serializable) target.getTargets()); + options.put("chosenTargets", new HashSet<>(target.getTargets())); prepareForResponse(game); if (!isExecutingMacro()) { @@ -747,9 +746,9 @@ public class HumanPlayer extends PlayerImpl { continue; } - if (possibleTargets.contains(responseId) && target.canTarget(getId(), responseId, source, game)) { + if (possibleTargets.contains(responseId)) { target.add(responseId, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { break; } } @@ -794,7 +793,7 @@ public class HumanPlayer extends PlayerImpl { // manual choice if (responseId == null) { - options.put("chosenTargets", (Serializable) target.getTargets()); + options.put("chosenTargets", new HashSet<>(target.getTargets())); prepareForResponse(game); if (!isExecutingMacro()) { @@ -814,11 +813,9 @@ public class HumanPlayer extends PlayerImpl { // add new target if (possibleTargets.contains(responseId)) { - if (target.canTarget(abilityControllerId, responseId, source, game)) { - target.addTarget(responseId, source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { - return true; - } + target.addTarget(responseId, source, game); + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { + return true; } } } else { @@ -864,13 +861,7 @@ public class HumanPlayer extends PlayerImpl { UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId()); while (canRespond()) { - - List possibleTargets = new ArrayList<>(); - for (UUID cardId : cards) { - if (target.canTarget(abilityControllerId, cardId, source, cards, game)) { - possibleTargets.add(cardId); - } - } + Set possibleTargets = target.possibleTargets(abilityControllerId, source, game, cards); boolean required = target.isRequired(source != null ? source.getSourceId() : null, game); int count = cards.count(target.getFilter(), abilityControllerId, source, game); @@ -880,7 +871,7 @@ public class HumanPlayer extends PlayerImpl { } // if nothing to choose then show dialog (user must see non selectable items and click on any of them) - // TODO: need research - is it used? + // TODO: need research - is it used (test player and AI player don't see empty dialogs)? if (required && possibleTargets.isEmpty()) { required = false; } @@ -894,7 +885,7 @@ public class HumanPlayer extends PlayerImpl { } else { // manual choose Map options = getOptions(target, null); - options.put("chosenTargets", (Serializable) target.getTargets()); + options.put("chosenTargets", new HashSet<>(target.getTargets())); if (!possibleTargets.isEmpty()) { options.put("possibleTargets", (Serializable) possibleTargets); } @@ -916,9 +907,9 @@ public class HumanPlayer extends PlayerImpl { continue; } - if (possibleTargets.contains(responseId) && target.canTarget(getId(), responseId, source, cards, game)) { + if (possibleTargets.contains(responseId)) { target.add(responseId, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, cards)) { return true; } } @@ -962,12 +953,8 @@ public class HumanPlayer extends PlayerImpl { required = false; } - List possibleTargets = new ArrayList<>(); - for (UUID cardId : cards) { - if (target.canTarget(abilityControllerId, cardId, source, cards, game)) { - possibleTargets.add(cardId); - } - } + Set possibleTargets = target.possibleTargets(abilityControllerId, source, game, cards); + // if nothing to choose then show dialog (user must see non-selectable items and click on any of them) if (possibleTargets.isEmpty()) { required = false; @@ -977,7 +964,7 @@ public class HumanPlayer extends PlayerImpl { if (responseId == null) { Map options = getOptions(target, null); - options.put("chosenTargets", (Serializable) target.getTargets()); + options.put("chosenTargets", new HashSet<>(target.getTargets())); if (!possibleTargets.isEmpty()) { options.put("possibleTargets", (Serializable) possibleTargets); @@ -995,9 +982,9 @@ public class HumanPlayer extends PlayerImpl { if (responseId != null) { if (target.contains(responseId)) { // if already included remove it target.remove(responseId); - } else if (target.canTarget(abilityControllerId, responseId, source, cards, game)) { + } else if (possibleTargets.contains(responseId)) { target.addTarget(responseId, source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, cards)) { return true; } } @@ -1054,9 +1041,9 @@ public class HumanPlayer extends PlayerImpl { // 1. Select targets // TODO: rework to use existing chooseTarget instead custom select? while (canRespond()) { - Set possibleTargetIds = target.possibleTargets(abilityControllerId, source, game); + Set possibleTargets = target.possibleTargets(abilityControllerId, source, game); boolean required = target.isRequired(source.getSourceId(), game); - if (possibleTargetIds.isEmpty() + if (possibleTargets.isEmpty() || target.getSize() >= target.getMinNumberOfTargets()) { required = false; } @@ -1065,12 +1052,6 @@ public class HumanPlayer extends PlayerImpl { // responseId is null if a choice couldn't be automatically made if (responseId == null) { - List possibleTargets = new ArrayList<>(); - for (UUID targetId : possibleTargetIds) { - if (target.canTarget(abilityControllerId, targetId, source, game)) { - possibleTargets.add(targetId); - } - } // if nothing to choose then show dialog (user must see non selectable items and click on any of them) if (required && possibleTargets.isEmpty()) { required = false; @@ -1078,7 +1059,7 @@ public class HumanPlayer extends PlayerImpl { // selected Map options = getOptions(target, null); - options.put("chosenTargets", (Serializable) target.getTargets()); + options.put("chosenTargets", new HashSet<>(target.getTargets())); if (!possibleTargets.isEmpty()) { options.put("possibleTargets", (Serializable) possibleTargets); } @@ -1087,7 +1068,7 @@ public class HumanPlayer extends PlayerImpl { if (!isExecutingMacro()) { String multiType = multiAmountType == MultiAmountType.DAMAGE ? " to divide %d damage" : " to distribute %d counters"; String message = target.getMessage(game) + String.format(multiType, amountTotal); - game.fireSelectTargetEvent(playerId, new MessageToClient(message, getRelatedObjectName(source, game)), possibleTargetIds, required, options); + game.fireSelectTargetEvent(playerId, new MessageToClient(message, getRelatedObjectName(source, game)), possibleTargets, required, options); } waitForResponse(game); @@ -1098,9 +1079,7 @@ public class HumanPlayer extends PlayerImpl { if (target.contains(responseId)) { // unselect target.remove(responseId); - } else if (possibleTargetIds.contains(responseId) - && target.canTarget(abilityControllerId, responseId, source, game) - && target.getSize() < amountTotal) { + } else if (possibleTargets.contains(responseId) && target.getSize() < amountTotal) { // select target.addTarget(responseId, source, game); } @@ -2094,32 +2073,33 @@ public class HumanPlayer extends PlayerImpl { if (!canCallFeedback(game)) { return; } - TargetAttackingCreature target = new TargetAttackingCreature(); - // TODO: add canRespond cycle? + // no need in cycle, cause parent selectBlockers used it already if (!canRespond()) { return; } UUID responseId = null; - prepareForResponse(game); if (!isExecutingMacro()) { // possible attackers to block - Set attackers = target.possibleTargets(playerId, null, game); + TargetAttackingCreature target = new TargetAttackingCreature(); Permanent blocker = game.getPermanent(blockerId); - Set possibleTargets = new HashSet<>(); - for (UUID attackerId : attackers) { + Set allAttackers = target.possibleTargets(playerId, null, game); + Set possibleAttackersToBlock = new HashSet<>(); + for (UUID attackerId : allAttackers) { CombatGroup group = game.getCombat().findGroup(attackerId); if (group != null && blocker != null && group.canBlock(blocker, game)) { - possibleTargets.add(attackerId); + possibleAttackersToBlock.add(attackerId); } } - if (possibleTargets.size() == 1) { - responseId = possibleTargets.stream().iterator().next(); + if (possibleAttackersToBlock.size() == 1) { + // auto-choice + responseId = possibleAttackersToBlock.stream().iterator().next(); } else { + prepareForResponse(game); game.fireSelectTargetEvent(playerId, new MessageToClient("Select attacker to block", getRelatedObjectName(blockerId, game)), - possibleTargets, false, getOptions(target, null)); + possibleAttackersToBlock, false, getOptions(target, null)); waitForResponse(game); } } @@ -2174,7 +2154,7 @@ public class HumanPlayer extends PlayerImpl { break; } - return xValue; + return xValue; } @Override diff --git a/Mage.Sets/src/mage/cards/a/AangsIceberg.java b/Mage.Sets/src/mage/cards/a/AangsIceberg.java new file mode 100644 index 00000000000..03f3008cdaf --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AangsIceberg.java @@ -0,0 +1,58 @@ +package mage.cards.a; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.ExileUntilSourceLeavesEffect; +import mage.abilities.effects.keyword.ScryEffect; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.keyword.WaterbendAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterNonlandPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AangsIceberg extends CardImpl { + + private static final FilterPermanent filter = new FilterNonlandPermanent("other target nonland permanent"); + + static { + filter.add(AnotherPredicate.instance); + } + + public AangsIceberg(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}"); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // When this enchantment enters, exile up to one other target nonland permanent until this enchantment leaves the battlefield. + Ability ability = new EntersBattlefieldTriggeredAbility(new ExileUntilSourceLeavesEffect()); + ability.addTarget(new TargetPermanent(0, 1, filter)); + this.addAbility(ability); + + // Waterbend {3}: Sacrifice this enchantment. If you do, scry 2. + this.addAbility(new WaterbendAbility(new DoIfCostPaid( + new ScryEffect(2), new SacrificeSourceCost(), null, false + ), new GenericManaCost(3))); + } + + private AangsIceberg(final AangsIceberg card) { + super(card); + } + + @Override + public AangsIceberg copy() { + return new AangsIceberg(this); + } +} diff --git a/Mage.Sets/src/mage/cards/a/AjanisChosen.java b/Mage.Sets/src/mage/cards/a/AjanisChosen.java index c84c947d8ee..534097ade4c 100644 --- a/Mage.Sets/src/mage/cards/a/AjanisChosen.java +++ b/Mage.Sets/src/mage/cards/a/AjanisChosen.java @@ -77,7 +77,7 @@ class AjanisChosenEffect extends OneShotEffect { Permanent oldCreature = game.getPermanent(enchantment.getAttachedTo()); if (oldCreature != null) { boolean canAttach = enchantment.getSpellAbility() == null - || (!enchantment.getSpellAbility().getTargets().isEmpty() && enchantment.getSpellAbility().getTargets().get(0).canTarget(tokenPermanent.getId(), game)); + || (!enchantment.getSpellAbility().getTargets().isEmpty() && enchantment.getSpellAbility().getTargets().get(0).canTarget(tokenPermanent.getId(), source, game)); if (canAttach && controller.chooseUse(Outcome.Neutral, "Attach " + enchantment.getName() + " to the token ?", source, game)) { if (oldCreature.removeAttachment(enchantment.getId(), source, game)) { tokenPermanent.addAttachment(enchantment.getId(), source, game); diff --git a/Mage.Sets/src/mage/cards/a/AllWillBeOne.java b/Mage.Sets/src/mage/cards/a/AllWillBeOne.java index 6ee9aa8e73b..13c0266e07e 100644 --- a/Mage.Sets/src/mage/cards/a/AllWillBeOne.java +++ b/Mage.Sets/src/mage/cards/a/AllWillBeOne.java @@ -12,6 +12,8 @@ import mage.filter.common.FilterCreaturePlayerOrPlaneswalker; import mage.filter.common.FilterPermanentOrPlayer; import mage.game.Game; import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; import mage.target.common.TargetPermanentOrPlayer; import java.util.UUID; @@ -68,6 +70,16 @@ class AllWillBeOneTriggeredAbility extends TriggeredAbilityImpl { if (!isControlledBy(event.getPlayerId())) { return false; } + Player player = game.getPlayer(event.getTargetId()); + if (player == null) { + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); + if (permanent == null) { + permanent = game.getPermanentEntering(event.getTargetId()); + if (permanent == null) { + return false; + } + } + } getEffects().setValue("damage", event.getAmount()); return true; } diff --git a/Mage.Sets/src/mage/cards/a/AncientBrassDragon.java b/Mage.Sets/src/mage/cards/a/AncientBrassDragon.java index 4e748752c45..ca2c720f483 100644 --- a/Mage.Sets/src/mage/cards/a/AncientBrassDragon.java +++ b/Mage.Sets/src/mage/cards/a/AncientBrassDragon.java @@ -108,8 +108,8 @@ class AncientBrassDragonTarget extends TargetCardInGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, xValue, game); } diff --git a/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java b/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java index 535752c85f4..8fb4bcc2cca 100644 --- a/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java +++ b/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java @@ -1,23 +1,16 @@ package mage.cards.a; import mage.MageInt; -import mage.abilities.Ability; +import mage.abilities.common.CantPayLifeOrSacrificeAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.Cost; -import mage.abilities.costs.common.SacrificeTargetCost; -import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.continuous.BoostControlledEffect; -import mage.abilities.effects.common.cost.CostModificationEffectImpl; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.filter.Filter; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; import mage.filter.StaticFilters; -import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.Predicates; -import mage.game.Game; -import mage.players.Player; import java.util.UUID; @@ -39,9 +32,7 @@ public final class AngelOfJubilation extends CardImpl { this.addAbility(new SimpleStaticAbility(new BoostControlledEffect(1, 1, Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT_CREATURES_NON_BLACK, true))); // Players can't pay life or sacrifice creatures to cast spells or activate abilities. - Ability ability = new SimpleStaticAbility(new AngelOfJubilationEffect()); - ability.addEffect(new AngelOfJubilationSacrificeFilterEffect()); - this.addAbility(ability); + this.addAbility(new CantPayLifeOrSacrificeAbility(StaticFilters.FILTER_PERMANENT_CREATURES)); } private AngelOfJubilation(final AngelOfJubilation card) { @@ -53,66 +44,3 @@ public final class AngelOfJubilation extends CardImpl { return new AngelOfJubilation(this); } } - -class AngelOfJubilationEffect extends ContinuousEffectImpl { - - AngelOfJubilationEffect() { - super(Duration.WhileOnBattlefield, Layer.PlayerEffects, SubLayer.NA, Outcome.Detriment); - staticText = "Players can't pay life or sacrifice creatures to cast spells"; - } - - private AngelOfJubilationEffect(final AngelOfJubilationEffect effect) { - super(effect); - } - - @Override - public AngelOfJubilationEffect copy() { - return new AngelOfJubilationEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { - Player player = game.getPlayer(playerId); - player.setPayLifeCostLevel(Player.PayLifeCostLevel.nonSpellnonActivatedAbilities); - player.setCanPaySacrificeCostFilter(new FilterCreaturePermanent()); - } - return true; - } -} - -class AngelOfJubilationSacrificeFilterEffect extends CostModificationEffectImpl { - - AngelOfJubilationSacrificeFilterEffect() { - super(Duration.WhileOnBattlefield, Outcome.Detriment, CostModificationType.SET_COST); - staticText = "or activate abilities"; - } - - protected AngelOfJubilationSacrificeFilterEffect(AngelOfJubilationSacrificeFilterEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source, Ability abilityToModify) { - for (Cost cost : abilityToModify.getCosts()) { - if (cost instanceof SacrificeTargetCost) { - SacrificeTargetCost sacrificeCost = (SacrificeTargetCost) cost; - Filter filter = sacrificeCost.getTargets().get(0).getFilter(); - filter.add(Predicates.not(CardType.CREATURE.getPredicate())); - } - } - return true; - } - - @Override - public boolean applies(Ability abilityToModify, Ability source, Game game) { - return (abilityToModify.isActivatedAbility() || abilityToModify.getAbilityType() == AbilityType.SPELL) - && game.getState().getPlayersInRange(source.getControllerId(), game).contains(abilityToModify.getControllerId()); - } - - @Override - public AngelOfJubilationSacrificeFilterEffect copy() { - return new AngelOfJubilationSacrificeFilterEffect(this); - } - -} diff --git a/Mage.Sets/src/mage/cards/a/AnuridScavenger.java b/Mage.Sets/src/mage/cards/a/AnuridScavenger.java index 9cbf5efe14d..9fbf8962508 100644 --- a/Mage.Sets/src/mage/cards/a/AnuridScavenger.java +++ b/Mage.Sets/src/mage/cards/a/AnuridScavenger.java @@ -84,7 +84,7 @@ class AnuridScavengerCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage.Sets/src/mage/cards/a/AoTheDawnSky.java b/Mage.Sets/src/mage/cards/a/AoTheDawnSky.java index 5813da126bd..6c577cd75e5 100644 --- a/Mage.Sets/src/mage/cards/a/AoTheDawnSky.java +++ b/Mage.Sets/src/mage/cards/a/AoTheDawnSky.java @@ -134,8 +134,8 @@ class AoTheDawnSkyTarget extends TargetCardInLibrary { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 4, game); } diff --git a/Mage.Sets/src/mage/cards/a/AppaSteadfastGuardian.java b/Mage.Sets/src/mage/cards/a/AppaSteadfastGuardian.java new file mode 100644 index 00000000000..e9bd0a841ee --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AppaSteadfastGuardian.java @@ -0,0 +1,70 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.keyword.AirbendTargetEffect; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterNonlandPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.permanent.token.AllyToken; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AppaSteadfastGuardian extends CardImpl { + + private static final FilterPermanent filter = new FilterNonlandPermanent("other target nonland permanents you control"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(TargetController.YOU.getControllerPredicate()); + } + + public AppaSteadfastGuardian(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.BISON); + this.subtype.add(SubType.ALLY); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When Appa enters, airbend any number of other target nonland permanents you control. + Ability ability = new EntersBattlefieldTriggeredAbility(new AirbendTargetEffect()); + ability.addTarget(new TargetPermanent(0, Integer.MAX_VALUE, filter)); + this.addAbility(ability); + + // Whenever you cast a spell from exile, create a 1/1 white Ally creature token. + this.addAbility(new SpellCastControllerTriggeredAbility( + Zone.BATTLEFIELD, new CreateTokenEffect(new AllyToken()), StaticFilters.FILTER_SPELL_A, + false, SetTargetPointer.NONE, Zone.EXILED + )); + } + + private AppaSteadfastGuardian(final AppaSteadfastGuardian card) { + super(card); + } + + @Override + public AppaSteadfastGuardian copy() { + return new AppaSteadfastGuardian(this); + } +} diff --git a/Mage.Sets/src/mage/cards/a/ArdentDustspeaker.java b/Mage.Sets/src/mage/cards/a/ArdentDustspeaker.java index 4b4ace434dc..1c2b4b95b6b 100644 --- a/Mage.Sets/src/mage/cards/a/ArdentDustspeaker.java +++ b/Mage.Sets/src/mage/cards/a/ArdentDustspeaker.java @@ -79,7 +79,7 @@ class ArdentDustspeakerCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage.Sets/src/mage/cards/a/AstralDrift.java b/Mage.Sets/src/mage/cards/a/AstralDrift.java index e181fee3284..72c5956fbdd 100644 --- a/Mage.Sets/src/mage/cards/a/AstralDrift.java +++ b/Mage.Sets/src/mage/cards/a/AstralDrift.java @@ -68,10 +68,7 @@ class AstralDriftTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - if (game.getState().getStack().isEmpty()) { - return false; - } - StackObject item = game.getState().getStack().getFirst(); + StackObject item = game.getState().getStack().getFirstOrNull(); if (!(item instanceof StackAbility && item.getStackAbility() instanceof CyclingAbility)) { return false; diff --git a/Mage.Sets/src/mage/cards/a/AvatarEnthusiasts.java b/Mage.Sets/src/mage/cards/a/AvatarEnthusiasts.java new file mode 100644 index 00000000000..bb1179c66c5 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AvatarEnthusiasts.java @@ -0,0 +1,51 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldAllTriggeredAbility; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AvatarEnthusiasts extends CardImpl { + + private static final FilterPermanent filter = new FilterControlledPermanent(SubType.ALLY, "another Ally you control"); + + static { + filter.add(AnotherPredicate.instance); + } + + public AvatarEnthusiasts(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.PEASANT); + this.subtype.add(SubType.ALLY); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Whenever another Ally you control enters, put a +1/+1 counter on this creature. + this.addAbility(new EntersBattlefieldAllTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), filter + )); + } + + private AvatarEnthusiasts(final AvatarEnthusiasts card) { + super(card); + } + + @Override + public AvatarEnthusiasts copy() { + return new AvatarEnthusiasts(this); + } +} diff --git a/Mage.Sets/src/mage/cards/b/BackFromTheBrink.java b/Mage.Sets/src/mage/cards/b/BackFromTheBrink.java index 7e720d5c3c2..9a905eee13e 100644 --- a/Mage.Sets/src/mage/cards/b/BackFromTheBrink.java +++ b/Mage.Sets/src/mage/cards/b/BackFromTheBrink.java @@ -67,7 +67,7 @@ class BackFromTheBrinkCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage.Sets/src/mage/cards/b/BattleForBretagard.java b/Mage.Sets/src/mage/cards/b/BattleForBretagard.java index ac11200470e..b05074878a6 100644 --- a/Mage.Sets/src/mage/cards/b/BattleForBretagard.java +++ b/Mage.Sets/src/mage/cards/b/BattleForBretagard.java @@ -147,6 +147,7 @@ class BattleForBretagardTarget extends TargetPermanent { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); + Set names = this.getTargets() .stream() .map(game::getPermanent) @@ -159,6 +160,7 @@ class BattleForBretagardTarget extends TargetPermanent { Permanent permanent = game.getPermanent(uuid); return permanent == null || names.contains(permanent.getName()); }); + return possibleTargets; } } diff --git a/Mage.Sets/src/mage/cards/b/BattlefieldScrounger.java b/Mage.Sets/src/mage/cards/b/BattlefieldScrounger.java index 5a914df491f..f03656b0c32 100644 --- a/Mage.Sets/src/mage/cards/b/BattlefieldScrounger.java +++ b/Mage.Sets/src/mage/cards/b/BattlefieldScrounger.java @@ -79,7 +79,7 @@ class BattlefieldScroungerCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage.Sets/src/mage/cards/b/BeguilerOfWills.java b/Mage.Sets/src/mage/cards/b/BeguilerOfWills.java index eb43b1be046..c8d88660c1b 100644 --- a/Mage.Sets/src/mage/cards/b/BeguilerOfWills.java +++ b/Mage.Sets/src/mage/cards/b/BeguilerOfWills.java @@ -66,12 +66,12 @@ class BeguilerOfWillsTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { Permanent permanent = game.getPermanent(id); int count = game.getBattlefield().countAll(this.filter, source.getControllerId(), game); if (permanent != null && permanent.getPower().getValue() <= count) { - return super.canTarget(controllerId, id, source, game); + return super.canTarget(playerId, id, source, game); } return false; } diff --git a/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java b/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java index 067cabe2f2a..3daa5a7fd14 100644 --- a/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java +++ b/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java @@ -12,11 +12,9 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; -import mage.filter.StaticFilters; import mage.game.Game; import mage.target.TargetPermanent; import mage.target.common.TargetControlledCreaturePermanent; -import mage.target.common.TargetCreaturePermanent; import mage.util.CardUtil; import java.util.UUID; @@ -65,7 +63,7 @@ enum BiteDownOnCrimeAdjuster implements CostAdjuster { @Override public void reduceCost(Ability ability, Game game) { if (CollectedEvidenceCondition.instance.apply(game, ability) - || (game.inCheckPlayableState() && collectEvidenceCost.canPay(ability, null, ability.getControllerId(), game))) { + || (game.inCheckPlayableState() && collectEvidenceCost.canPay(ability, ability, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); } } diff --git a/Mage.Sets/src/mage/cards/b/BlazingHope.java b/Mage.Sets/src/mage/cards/b/BlazingHope.java index 4fc35abe721..06f8e7cec1b 100644 --- a/Mage.Sets/src/mage/cards/b/BlazingHope.java +++ b/Mage.Sets/src/mage/cards/b/BlazingHope.java @@ -49,18 +49,18 @@ class BlazingHopeTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { Permanent permanent = game.getPermanent(id); if (permanent != null) { if (!isNotTarget()) { - if (!permanent.canBeTargetedBy(game.getObject(source.getId()), controllerId, source, game) - || !permanent.canBeTargetedBy(game.getObject(source), controllerId, source, game)) { + if (!permanent.canBeTargetedBy(game.getObject(source.getId()), playerId, source, game) + || !permanent.canBeTargetedBy(game.getObject(source), playerId, source, game)) { return false; } } Player controller = game.getPlayer(source.getControllerId()); if (controller != null && permanent.getPower().getValue() >= controller.getLife()) { - return filter.match(permanent, controllerId, source, game); + return filter.match(permanent, playerId, source, game); } } return false; diff --git a/Mage.Sets/src/mage/cards/b/BreakingOfTheFellowship.java b/Mage.Sets/src/mage/cards/b/BreakingOfTheFellowship.java index 6b1449b7d2e..6f0d8f01653 100644 --- a/Mage.Sets/src/mage/cards/b/BreakingOfTheFellowship.java +++ b/Mage.Sets/src/mage/cards/b/BreakingOfTheFellowship.java @@ -1,6 +1,5 @@ package mage.cards.b; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.keyword.TheRingTemptsYouEffect; @@ -8,20 +7,20 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; +import mage.filter.FilterPermanent; import mage.filter.StaticFilters; -import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.Predicates; -import mage.filter.predicate.permanent.ControllerIdPredicate; -import mage.filter.predicate.permanent.PermanentIdPredicate; +import mage.filter.common.FilterOpponentsCreaturePermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.players.Player; import mage.target.TargetPermanent; -import mage.target.common.TargetCreaturePermanent; +import java.util.Set; import java.util.UUID; /** + * TODO: combine with Mutiny + * * @author Susucr */ public final class BreakingOfTheFellowship extends CardImpl { @@ -31,8 +30,8 @@ public final class BreakingOfTheFellowship extends CardImpl { // Target creature an opponent controls deals damage equal to its power to another target creature that player controls. this.getSpellAbility().addEffect(new BreakingOfTheFellowshipEffect()); - this.getSpellAbility().addTarget(new BreakingOfTheFellowshipFirstTarget()); - this.getSpellAbility().addTarget(new TargetPermanent(new FilterCreaturePermanent("another target creature that player controls"))); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE)); + this.getSpellAbility().addTarget(new BreakingOfTheFellowshipSecondTarget()); // The Ring tempts you. this.getSpellAbility().addEffect(new TheRingTemptsYouEffect()); @@ -76,74 +75,45 @@ class BreakingOfTheFellowshipEffect extends OneShotEffect { } return true; } - } -class BreakingOfTheFellowshipFirstTarget extends TargetPermanent { +class BreakingOfTheFellowshipSecondTarget extends TargetPermanent { - public BreakingOfTheFellowshipFirstTarget() { - super(1, 1, StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE, false); + public BreakingOfTheFellowshipSecondTarget() { + super(StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE); } - private BreakingOfTheFellowshipFirstTarget(final BreakingOfTheFellowshipFirstTarget target) { + private BreakingOfTheFellowshipSecondTarget(final BreakingOfTheFellowshipSecondTarget target) { super(target); } @Override - public void addTarget(UUID id, Ability source, Game game, boolean skipEvent) { - super.addTarget(id, source, game, skipEvent); - // Update the second target - UUID firstController = game.getControllerId(id); - if (firstController != null && source.getTargets().size() > 1) { - Player controllingPlayer = game.getPlayer(firstController); - TargetCreaturePermanent targetCreaturePermanent = (TargetCreaturePermanent) source.getTargets().get(1); - // Set a new filter to the second target with the needed restrictions - FilterCreaturePermanent filter = new FilterCreaturePermanent("another creature that player " + controllingPlayer.getName() + " controls"); - filter.add(new ControllerIdPredicate(firstController)); - filter.add(Predicates.not(new PermanentIdPredicate(id))); - targetCreaturePermanent.replaceFilter(filter); - } - } + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (super.canTarget(controllerId, id, source, game)) { - // can only target, if the controller has at least two targetable creatures - UUID controllingPlayerId = game.getControllerId(id); - int possibleTargets = 0; - MageObject sourceObject = game.getObject(source.getId()); - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, controllingPlayerId, game)) { - if (permanent.canBeTargetedBy(sourceObject, controllerId, source, game)) { - possibleTargets++; - } + Permanent firstTarget = game.getPermanent(source.getFirstTarget()); + if (firstTarget == null) { + // playable or first target not yet selected + // use all + if (possibleTargets.size() == 1) { + // workaround to make 1 target invalid + possibleTargets.clear(); } - return possibleTargets > 1; + } else { + // real + // filter by same player + possibleTargets.removeIf(id -> { + Permanent permanent = game.getPermanent(id); + return permanent == null || !permanent.isControlledBy(firstTarget.getControllerId()); + }); } - return false; + possibleTargets.removeIf(id -> firstTarget != null && firstTarget.getId().equals(id)); + + return possibleTargets; } @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - if (super.canChoose(sourceControllerId, source, game)) { - UUID controllingPlayerId = game.getControllerId(source.getSourceId()); - for (UUID playerId : game.getOpponents(controllingPlayerId)) { - int possibleTargets = 0; - MageObject sourceObject = game.getObject(source); - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, playerId, game)) { - if (permanent.canBeTargetedBy(sourceObject, controllingPlayerId, source, game)) { - possibleTargets++; - } - } - if (possibleTargets > 1) { - return true; - } - } - } - return false; - } - - @Override - public BreakingOfTheFellowshipFirstTarget copy() { - return new BreakingOfTheFellowshipFirstTarget(this); + public BreakingOfTheFellowshipSecondTarget copy() { + return new BreakingOfTheFellowshipSecondTarget(this); } } diff --git a/Mage.Sets/src/mage/cards/c/CallOfTheDeathDweller.java b/Mage.Sets/src/mage/cards/c/CallOfTheDeathDweller.java index ed40fb64915..e244de72599 100644 --- a/Mage.Sets/src/mage/cards/c/CallOfTheDeathDweller.java +++ b/Mage.Sets/src/mage/cards/c/CallOfTheDeathDweller.java @@ -141,8 +141,8 @@ class CallOfTheDeathDwellerTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 3, game); } diff --git a/Mage.Sets/src/mage/cards/c/ChandraPyromaster.java b/Mage.Sets/src/mage/cards/c/ChandraPyromaster.java index d219c7239ff..035f31d6a6d 100644 --- a/Mage.Sets/src/mage/cards/c/ChandraPyromaster.java +++ b/Mage.Sets/src/mage/cards/c/ChandraPyromaster.java @@ -1,8 +1,5 @@ package mage.cards.c; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; import mage.ApprovingObject; import mage.MageObject; import mage.abilities.Ability; @@ -17,7 +14,6 @@ import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterInstantOrSorceryCard; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.game.stack.StackObject; import mage.players.Player; import mage.target.Target; import mage.target.TargetCard; @@ -25,6 +21,9 @@ import mage.target.TargetPermanent; import mage.target.common.TargetPlayerOrPlaneswalker; import mage.target.targetpointer.FixedTarget; +import java.util.Set; +import java.util.UUID; + /** * @author jeffwadsworth */ @@ -111,47 +110,23 @@ class ChandraPyromasterTarget extends TargetPermanent { super(target); } - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - Player player = game.getPlayerOrPlaneswalkerController(source.getFirstTarget()); - if (player == null) { - return false; - } - UUID firstTarget = player.getId(); - Permanent permanent = game.getPermanent(id); - if (firstTarget != null && permanent != null && permanent.isControlledBy(firstTarget)) { - return super.canTarget(id, source, game); - } - return false; - } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set availablePossibleTargets = super.possibleTargets(sourceControllerId, source, game); - Set possibleTargets = new HashSet<>(); - MageObject object = game.getObject(source); + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - for (StackObject item : game.getState().getStack()) { - if (item.getId().equals(source.getSourceId())) { - object = item; - } - if (item.getSourceId().equals(source.getSourceId())) { - object = item; - } + Player needPlayer = game.getPlayerOrPlaneswalkerController(source.getFirstTarget()); + if (needPlayer == null) { + // playable or not selected - use any + } else { + // filter by controller + possibleTargets.removeIf(id -> { + Permanent permanent = game.getPermanent(id); + return permanent == null + || permanent.getId().equals(source.getFirstTarget()) + || !permanent.isControlledBy(needPlayer.getId()); + }); } - if (object instanceof StackObject) { - UUID playerId = ((StackObject) object).getStackAbility().getFirstTarget(); - Player player = game.getPlayerOrPlaneswalkerController(playerId); - if (player != null) { - for (UUID targetId : availablePossibleTargets) { - Permanent permanent = game.getPermanent(targetId); - if (permanent != null && permanent.isControlledBy(player.getId())) { - possibleTargets.add(targetId); - } - } - } - } return possibleTargets; } diff --git a/Mage.Sets/src/mage/cards/c/ChaosMutation.java b/Mage.Sets/src/mage/cards/c/ChaosMutation.java index fb4b4cfbc41..bbb0639611e 100644 --- a/Mage.Sets/src/mage/cards/c/ChaosMutation.java +++ b/Mage.Sets/src/mage/cards/c/ChaosMutation.java @@ -124,8 +124,8 @@ class ChaosMutationTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } Permanent creature = game.getPermanent(id); diff --git a/Mage.Sets/src/mage/cards/c/CloudsLimitBreak.java b/Mage.Sets/src/mage/cards/c/CloudsLimitBreak.java index 91b5eea84fd..fa6dcd2fa80 100644 --- a/Mage.Sets/src/mage/cards/c/CloudsLimitBreak.java +++ b/Mage.Sets/src/mage/cards/c/CloudsLimitBreak.java @@ -85,8 +85,8 @@ class CloudsLimitBreakTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } Permanent creature = game.getPermanent(id); diff --git a/Mage.Sets/src/mage/cards/c/CobraTrap.java b/Mage.Sets/src/mage/cards/c/CobraTrap.java index 90ec0240ab8..99cd7e9cd7f 100644 --- a/Mage.Sets/src/mage/cards/c/CobraTrap.java +++ b/Mage.Sets/src/mage/cards/c/CobraTrap.java @@ -78,11 +78,9 @@ class CobraTrapWatcher extends Watcher { if (event.getType() == GameEvent.EventType.DESTROYED_PERMANENT) { Permanent perm = game.getPermanentOrLKIBattlefield(event.getTargetId()); // can regenerate or be indestructible if (perm != null && !perm.isCreature(game)) { - if (!game.getStack().isEmpty()) { - StackObject spell = game.getStack().getStackObject(event.getSourceId()); - if (spell != null && game.getOpponents(perm.getControllerId()).contains(spell.getControllerId())) { - players.add(perm.getControllerId()); - } + StackObject spell = game.getStack().getStackObject(event.getSourceId()); + if (spell != null && game.getOpponents(perm.getControllerId()).contains(spell.getControllerId())) { + players.add(perm.getControllerId()); } } } diff --git a/Mage.Sets/src/mage/cards/c/ConspiracyTheorist.java b/Mage.Sets/src/mage/cards/c/ConspiracyTheorist.java index dbd41c58ddd..d767e99db1d 100644 --- a/Mage.Sets/src/mage/cards/c/ConspiracyTheorist.java +++ b/Mage.Sets/src/mage/cards/c/ConspiracyTheorist.java @@ -119,8 +119,7 @@ class ConspiracyTheoristEffect extends OneShotEffect { if (controller != null) { CardsImpl cards = new CardsImpl(discardedCards); TargetCard target = new TargetCard(Zone.GRAVEYARD, new FilterCard("card to exile")); - boolean validTarget = cards.stream() - .anyMatch(card -> target.canTarget(card, game)); + boolean validTarget = cards.stream().anyMatch(card -> target.canTarget(card, source, game)); if (validTarget && controller.chooseUse(Outcome.Benefit, "Exile a card?", source, game)) { if (controller.choose(Outcome.Benefit, cards, target, source, game)) { Card card = cards.get(target.getFirstTarget(), game); diff --git a/Mage.Sets/src/mage/cards/d/DaringThief.java b/Mage.Sets/src/mage/cards/d/DaringThief.java index 44566227ba6..668d2eaf7ca 100644 --- a/Mage.Sets/src/mage/cards/d/DaringThief.java +++ b/Mage.Sets/src/mage/cards/d/DaringThief.java @@ -9,6 +9,7 @@ import mage.abilities.keyword.InspiredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; +import mage.filter.StaticFilters; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.game.permanent.Permanent; @@ -35,8 +36,12 @@ public final class DaringThief extends CardImpl { this.toughness = new MageInt(3); // Inspired - Whenever Daring Thief becomes untapped, you may exchange control of target nonland permanent you control and target permanent an opponent controls that shares a card type with it. - Ability ability = new InspiredAbility(new ExchangeControlTargetEffect(Duration.EndOfGame, - "you may exchange control of target nonland permanent you control and target permanent an opponent controls that shares a card type with it", false, true), true); + Ability ability = new InspiredAbility(new ExchangeControlTargetEffect( + Duration.EndOfGame, + "you may exchange control of target nonland permanent you control and target permanent an opponent controls that shares a card type with it", + false, + true + ), true); ability.addTarget(new TargetControlledPermanentSharingOpponentPermanentCardType()); ability.addTarget(new DaringThiefSecondTarget()); this.addAbility(ability); @@ -66,9 +71,10 @@ class TargetControlledPermanentSharingOpponentPermanentCardType extends TargetCo } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (super.canTarget(controllerId, id, source, game)) { - Set cardTypes = getOpponentPermanentCardTypes(controllerId, game); + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + Set cardTypes = getOpponentPermanentCardTypes(playerId, game); + + if (super.canTarget(playerId, id, source, game)) { Permanent permanent = game.getPermanent(id); for (CardType type : permanent.getCardType(game)) { if (cardTypes.contains(type)) { @@ -81,23 +87,21 @@ class TargetControlledPermanentSharingOpponentPermanentCardType extends TargetCo @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - // get all cardtypes from opponents permanents Set cardTypes = getOpponentPermanentCardTypes(sourceControllerId, game); + Set possibleTargets = new HashSet<>(); MageObject targetSource = game.getObject(source); if (targetSource != null) { for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId()) && permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - for (CardType type : permanent.getCardType(game)) { - if (cardTypes.contains(type)) { - possibleTargets.add(permanent.getId()); - break; - } + for (CardType type : permanent.getCardType(game)) { + if (cardTypes.contains(type)) { + possibleTargets.add(permanent.getId()); + break; } } } } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override @@ -119,58 +123,54 @@ class TargetControlledPermanentSharingOpponentPermanentCardType extends TargetCo } } - class DaringThiefSecondTarget extends TargetPermanent { - private Permanent firstTarget = null; - public DaringThiefSecondTarget() { - super(); - this.filter = this.filter.copy(); - filter.add(TargetController.OPPONENT.getControllerPredicate()); + super(StaticFilters.FILTER_OPPONENTS_PERMANENT); withTargetName("permanent an opponent controls that shares a card type with it"); } private DaringThiefSecondTarget(final DaringThiefSecondTarget target) { super(target); - this.firstTarget = target.firstTarget; } - @Override public boolean canTarget(UUID id, Ability source, Game game) { - if (super.canTarget(id, source, game)) { - Permanent target1 = game.getPermanent(source.getFirstTarget()); - Permanent opponentPermanent = game.getPermanent(id); - if (target1 != null && opponentPermanent != null) { - return target1.shareTypes(opponentPermanent, game); - } + Permanent ownPermanent = game.getPermanent(source.getFirstTarget()); + Permanent possiblePermanent = game.getPermanent(id); + if (ownPermanent == null || possiblePermanent == null) { + return false; } - return false; + return super.canTarget(id, source, game) && ownPermanent.shareTypes(possiblePermanent, game); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); - if (firstTarget != null) { - MageObject targetSource = game.getObject(source); - if (targetSource != null) { - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId()) && permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - if (permanent.shareTypes(firstTarget, game)) { - possibleTargets.add(permanent.getId()); - } - } + + Permanent ownPermanent = game.getPermanent(source.getFirstTarget()); + for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { + if (ownPermanent == null) { + // playable or first target not yet selected + // use all + possibleTargets.add(permanent.getId()); + } else { + // real + // filter by shared type + if (permanent.shareTypes(ownPermanent, game)) { + possibleTargets.add(permanent.getId()); } } } - return possibleTargets; + possibleTargets.removeIf(id -> ownPermanent != null && ownPermanent.getId().equals(id)); + + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) { - firstTarget = game.getPermanent(source.getFirstTarget()); - return super.chooseTarget(Outcome.Damage, playerId, source, game); + // AI hint with better outcome + return super.chooseTarget(Outcome.GainControl, playerId, source, game); } @Override diff --git a/Mage.Sets/src/mage/cards/d/DeepCavernBat.java b/Mage.Sets/src/mage/cards/d/DeepCavernBat.java index 96b1573da43..67564ae1bd3 100644 --- a/Mage.Sets/src/mage/cards/d/DeepCavernBat.java +++ b/Mage.Sets/src/mage/cards/d/DeepCavernBat.java @@ -91,9 +91,7 @@ class DeepCaverBatEffect extends OneShotEffect { return true; } - TargetCard target = new TargetCardInHand( - 0, 1, StaticFilters.FILTER_CARD_A_NON_LAND - ); + TargetCard target = new TargetCard(0, 1, Zone.HAND, StaticFilters.FILTER_CARD_A_NON_LAND); controller.choose(outcome, opponent.getHand(), target, source, game); Card card = opponent.getHand().get(target.getFirstTarget(), game); if (card == null) { diff --git a/Mage.Sets/src/mage/cards/d/DefenseOfTheHeart.java b/Mage.Sets/src/mage/cards/d/DefenseOfTheHeart.java index 7278452a172..31e5db66543 100644 --- a/Mage.Sets/src/mage/cards/d/DefenseOfTheHeart.java +++ b/Mage.Sets/src/mage/cards/d/DefenseOfTheHeart.java @@ -2,35 +2,32 @@ package mage.cards.d; import mage.abilities.Ability; import mage.abilities.condition.Condition; -import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; import mage.abilities.effects.common.SacrificeSourceEffect; import mage.abilities.effects.common.search.SearchLibraryPutInPlayEffect; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.ComparisonType; import mage.filter.StaticFilters; -import mage.filter.common.FilterOpponentsCreaturePermanent; +import mage.game.Controllable; +import mage.game.Game; import mage.target.common.TargetCardInLibrary; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; /** * @author Plopman */ public final class DefenseOfTheHeart extends CardImpl { - private static final Condition condition = new PermanentsOnTheBattlefieldCondition( - new FilterOpponentsCreaturePermanent("an opponent controls three or more creatures"), - ComparisonType.MORE_THAN, 2 - ); - public DefenseOfTheHeart(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{G}"); // At the beginning of your upkeep, if an opponent controls three or more creatures, sacrifice Defense of the Heart, search your library for up to two creature cards, and put those cards onto the battlefield. Then shuffle your library. - Ability ability = new BeginningOfUpkeepTriggeredAbility(new SacrificeSourceEffect()).withInterveningIf(condition); + Ability ability = new BeginningOfUpkeepTriggeredAbility(new SacrificeSourceEffect()) + .withInterveningIf(DefenseOfTheHeartCondition.instance); ability.addEffect(new SearchLibraryPutInPlayEffect(new TargetCardInLibrary( 0, 2, StaticFilters.FILTER_CARD_CREATURES )).concatBy(",")); @@ -46,3 +43,28 @@ public final class DefenseOfTheHeart extends CardImpl { return new DefenseOfTheHeart(this); } } + +enum DefenseOfTheHeartCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return game + .getBattlefield() + .getActivePermanents( + StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE, + source.getControllerId(), source, game + ) + .stream() + .map(Controllable::getControllerId) + .collect(Collectors.toMap(Function.identity(), x -> 1, Integer::sum)) + .values() + .stream() + .anyMatch(x -> x >= 3); + } + + @Override + public String toString() { + return "an opponent controls three or more creatures"; + } +} diff --git a/Mage.Sets/src/mage/cards/d/DesperateGambit.java b/Mage.Sets/src/mage/cards/d/DesperateGambit.java index a33067ba290..719077d465c 100644 --- a/Mage.Sets/src/mage/cards/d/DesperateGambit.java +++ b/Mage.Sets/src/mage/cards/d/DesperateGambit.java @@ -137,49 +137,12 @@ class TargetControlledSource extends TargetSource { } @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - int count = 0; - for (StackObject stackObject : game.getStack()) { - if (game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) - && Objects.equals(stackObject.getControllerId(), sourceControllerId)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - for (Permanent permanent : game.getBattlefield().getActivePermanents(sourceControllerId, game)) { - if (Objects.equals(permanent.getControllerId(), sourceControllerId)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - for (Player player : game.getPlayers().values()) { - if (Objects.equals(player, game.getPlayer(sourceControllerId))) { - for (Card card : player.getGraveyard().getCards(game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - // 108.4a If anything asks for the controller of a card that doesn't have one (because it's not a permanent or spell), use its owner instead. - for (Card card : game.getExile().getAllCards(game)) { - if (Objects.equals(card.getOwnerId(), sourceControllerId)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - } - } - return false; + public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); for (StackObject stackObject : game.getStack()) { if (game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) @@ -205,7 +168,7 @@ class TargetControlledSource extends TargetSource { } } } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage.Sets/src/mage/cards/d/DistendedMindbender.java b/Mage.Sets/src/mage/cards/d/DistendedMindbender.java index d12bec48bd7..fad22da9f7e 100644 --- a/Mage.Sets/src/mage/cards/d/DistendedMindbender.java +++ b/Mage.Sets/src/mage/cards/d/DistendedMindbender.java @@ -90,10 +90,10 @@ class DistendedMindbenderEffect extends OneShotEffect { TargetCard targetThreeOrLess = new TargetCard(1, Zone.HAND, filterThreeOrLess); TargetCard targetFourOrGreater = new TargetCard(1, Zone.HAND, filterFourOrGreater); Cards toDiscard = new CardsImpl(); - if (controller.chooseTarget(Outcome.Benefit, opponent.getHand(), targetThreeOrLess, source, game)) { + if (controller.choose(Outcome.Benefit, opponent.getHand(), targetThreeOrLess, source, game)) { toDiscard.addAll(targetThreeOrLess.getTargets()); } - if (controller.chooseTarget(Outcome.Benefit, opponent.getHand(), targetFourOrGreater, source, game)) { + if (controller.choose(Outcome.Benefit, opponent.getHand(), targetFourOrGreater, source, game)) { toDiscard.addAll(targetFourOrGreater.getTargets()); } opponent.discard(toDiscard, false, source, game); diff --git a/Mage.Sets/src/mage/cards/d/DrafnasRestoration.java b/Mage.Sets/src/mage/cards/d/DrafnasRestoration.java index 1641294bdf5..47486d9fbe5 100644 --- a/Mage.Sets/src/mage/cards/d/DrafnasRestoration.java +++ b/Mage.Sets/src/mage/cards/d/DrafnasRestoration.java @@ -1,25 +1,26 @@ package mage.cards.d; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; -import mage.cards.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; import mage.constants.CardType; import mage.constants.Outcome; import mage.filter.common.FilterArtifactCard; import mage.game.Game; -import mage.game.events.TargetEvent; -import mage.game.stack.StackObject; import mage.players.Player; import mage.target.TargetPlayer; import mage.target.common.TargetCardInGraveyard; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + /** - * * @author emerald000 */ public final class DrafnasRestoration extends CardImpl { @@ -53,27 +54,33 @@ class DrafnasRestorationTarget extends TargetCardInGraveyard { super(target); } - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - Player targetPlayer = game.getPlayer(source.getFirstTarget()); - return targetPlayer != null && targetPlayer.getGraveyard().contains(id) && super.canTarget(id, source, game); - } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); - MageObject object = game.getObject(source); - if (object instanceof StackObject) { - Player targetPlayer = game.getPlayer(((StackObject) object).getStackAbility().getFirstTarget()); - if (targetPlayer != null) { - for (Card card : targetPlayer.getGraveyard().getCards(filter, sourceControllerId, source, game)) { - if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { - possibleTargets.add(card.getId()); - } - } - } + + Player controller = game.getPlayer(sourceControllerId); + if (controller == null) { + return possibleTargets; } - return possibleTargets; + + Player targetPlayer = game.getPlayer(source.getFirstTarget()); + game.getState().getPlayersInRange(sourceControllerId, game, true).stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .flatMap(player -> player.getGraveyard().getCards(filter, sourceControllerId, source, game).stream()) + .forEach(card -> { + if (targetPlayer == null) { + // playable or not selected - use any + possibleTargets.add(card.getId()); + } else { + // selected, filter by player + if (targetPlayer.getId().equals(card.getControllerOrOwnerId())) { + possibleTargets.add(card.getId()); + } + } + }); + + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage.Sets/src/mage/cards/d/DreamsOfSteelAndOil.java b/Mage.Sets/src/mage/cards/d/DreamsOfSteelAndOil.java index 3c569c5ea9d..b3d7f51fdf7 100644 --- a/Mage.Sets/src/mage/cards/d/DreamsOfSteelAndOil.java +++ b/Mage.Sets/src/mage/cards/d/DreamsOfSteelAndOil.java @@ -72,7 +72,7 @@ class DreamsOfSteelAndOilEffect extends OneShotEffect { filter.add(Predicates.or(CardType.ARTIFACT.getPredicate(), CardType.CREATURE.getPredicate())); TargetCard target = new TargetCard(Zone.HAND, filter); target.withNotTarget(true); - controller.chooseTarget(Outcome.Discard, opponent.getHand(), target, source, game); + controller.choose(Outcome.Discard, opponent.getHand(), target, source, game); Card card = game.getCard(target.getFirstTarget()); if (card != null) { toExile.add(card); @@ -81,7 +81,7 @@ class DreamsOfSteelAndOilEffect extends OneShotEffect { filter.setMessage("artifact or creature card from " + opponent.getName() + "'s graveyard"); target = new TargetCard(Zone.GRAVEYARD, filter); target.withNotTarget(true); - controller.chooseTarget(Outcome.Exile, opponent.getGraveyard(), target, source, game); + controller.choose(Outcome.Exile, opponent.getGraveyard(), target, source, game); card = game.getCard(target.getFirstTarget()); if (card != null) { toExile.add(card); diff --git a/Mage.Sets/src/mage/cards/d/DruidicRitual.java b/Mage.Sets/src/mage/cards/d/DruidicRitual.java index 83df3b1514f..f5986ca4c9d 100644 --- a/Mage.Sets/src/mage/cards/d/DruidicRitual.java +++ b/Mage.Sets/src/mage/cards/d/DruidicRitual.java @@ -105,11 +105,10 @@ class RevivalExperimentTarget extends TargetCardInYourGraveyard { return cardTypeAssigner.getRoleCount(cards, game) >= cards.size(); } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, null, game)); + possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, source, game)); return possibleTargets; } } diff --git a/Mage.Sets/src/mage/cards/e/EarthbendingLesson.java b/Mage.Sets/src/mage/cards/e/EarthbendingLesson.java new file mode 100644 index 00000000000..5a44f3d666c --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EarthbendingLesson.java @@ -0,0 +1,36 @@ +package mage.cards.e; + +import mage.abilities.effects.keyword.EarthbendTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EarthbendingLesson extends CardImpl { + + public EarthbendingLesson(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{G}"); + + this.subtype.add(SubType.LESSON); + + // Earthbend 4. + this.getSpellAbility().addEffect(new EarthbendTargetEffect(4)); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_LAND)); + } + + private EarthbendingLesson(final EarthbendingLesson card) { + super(card); + } + + @Override + public EarthbendingLesson copy() { + return new EarthbendingLesson(this); + } +} diff --git a/Mage.Sets/src/mage/cards/e/EliteSpellbinder.java b/Mage.Sets/src/mage/cards/e/EliteSpellbinder.java index caefc4cb1c2..f7fc09fffcc 100644 --- a/Mage.Sets/src/mage/cards/e/EliteSpellbinder.java +++ b/Mage.Sets/src/mage/cards/e/EliteSpellbinder.java @@ -79,9 +79,7 @@ class EliteSpellbinderEffect extends OneShotEffect { if (controller == null || opponent == null || opponent.getHand().isEmpty()) { return false; } - TargetCard target = new TargetCardInHand( - 0, 1, StaticFilters.FILTER_CARD_A_NON_LAND - ); + TargetCard target = new TargetCard(0, 1, Zone.HAND, StaticFilters.FILTER_CARD_A_NON_LAND); controller.choose(outcome, opponent.getHand(), target, source, game); Card card = opponent.getHand().get(target.getFirstTarget(), game); if (card == null) { diff --git a/Mage.Sets/src/mage/cards/e/EnchantmentAlteration.java b/Mage.Sets/src/mage/cards/e/EnchantmentAlteration.java index 113e9841794..8ba2936186f 100644 --- a/Mage.Sets/src/mage/cards/e/EnchantmentAlteration.java +++ b/Mage.Sets/src/mage/cards/e/EnchantmentAlteration.java @@ -129,7 +129,7 @@ class EnchantmentAlterationEffect extends OneShotEffect { if (oldPermanent != null && !oldPermanent.equals(permanentToBeAttachedTo)) { Target auraTarget = aura.getSpellAbility().getTargets().get(0); - if (!auraTarget.canTarget(permanentToBeAttachedTo.getId(), game)) { + if (!auraTarget.canTarget(permanentToBeAttachedTo.getId(), source, game)) { game.informPlayers(aura.getLogName() + " was not attched to " + permanentToBeAttachedTo.getLogName() + " because it's no legal target for the aura"); } else if (oldPermanent.removeAttachment(aura.getId(), source, game)) { game.informPlayers(aura.getLogName() + " was unattached from " + oldPermanent.getLogName() + " and attached to " + permanentToBeAttachedTo.getLogName()); diff --git a/Mage.Sets/src/mage/cards/e/EverythingComesToDust.java b/Mage.Sets/src/mage/cards/e/EverythingComesToDust.java index 85e501262a3..bd56aeb01bd 100644 --- a/Mage.Sets/src/mage/cards/e/EverythingComesToDust.java +++ b/Mage.Sets/src/mage/cards/e/EverythingComesToDust.java @@ -58,7 +58,7 @@ enum EverythingComesToDustPredicate implements ObjectSourcePlayerPredicate set = CardUtil.getSourceCostsTag(game, input.getSource(), ConvokeAbility.convokingCreaturesKey, new HashSet<>(0)); + HashSet set = CardUtil.getSourceCostsTag(game, input.getSource(), ConvokeAbility.convokingCreaturesKey, new HashSet<>()); for (MageObjectReference mor : set){ Permanent convoked = game.getPermanentOrLKIBattlefield(mor); if (convoked.shareCreatureTypes(game, p)){ diff --git a/Mage.Sets/src/mage/cards/e/ExtractBrain.java b/Mage.Sets/src/mage/cards/e/ExtractBrain.java index 9f33ad20f40..34d0d0ac6e3 100644 --- a/Mage.Sets/src/mage/cards/e/ExtractBrain.java +++ b/Mage.Sets/src/mage/cards/e/ExtractBrain.java @@ -8,9 +8,11 @@ import mage.cards.Cards; import mage.cards.CardsImpl; import mage.constants.CardType; import mage.constants.Outcome; +import mage.constants.Zone; import mage.filter.StaticFilters; import mage.game.Game; import mage.players.Player; +import mage.target.TargetCard; import mage.target.common.TargetCardInHand; import mage.target.common.TargetOpponent; import mage.util.CardUtil; @@ -65,9 +67,7 @@ class ExtractBrainEffect extends OneShotEffect { if (controller == null || opponent == null || opponent.getHand().isEmpty() || xValue < 1) { return false; } - TargetCardInHand target = new TargetCardInHand( - Math.min(opponent.getHand().size(), xValue), StaticFilters.FILTER_CARD - ); + TargetCard target = new TargetCard(Math.min(opponent.getHand().size(), xValue), Integer.MAX_VALUE, Zone.HAND, StaticFilters.FILTER_CARD); opponent.choose(Outcome.Detriment, opponent.getHand(), target, source, game); Cards cards = new CardsImpl(target.getTargets()); controller.lookAtCards(source, null, cards, game); diff --git a/Mage.Sets/src/mage/cards/f/FireNationAttacks.java b/Mage.Sets/src/mage/cards/f/FireNationAttacks.java new file mode 100644 index 00000000000..c4ffb6c0dd1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FireNationAttacks.java @@ -0,0 +1,36 @@ +package mage.cards.f; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.SoldierFirebendingToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FireNationAttacks extends CardImpl { + + public FireNationAttacks(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{4}{R}"); + + // Create two 2/2 red Soldier creature tokens with firebending 1. + this.getSpellAbility().addEffect(new CreateTokenEffect(new SoldierFirebendingToken(), 2)); + + // Flashback {8}{R} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{8}{R}"))); + } + + private FireNationAttacks(final FireNationAttacks card) { + super(card); + } + + @Override + public FireNationAttacks copy() { + return new FireNationAttacks(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/Fireball.java b/Mage.Sets/src/mage/cards/f/Fireball.java index b55b90de1b9..17b77fe176b 100644 --- a/Mage.Sets/src/mage/cards/f/Fireball.java +++ b/Mage.Sets/src/mage/cards/f/Fireball.java @@ -134,7 +134,6 @@ class FireballTargetCreatureOrPlayer extends TargetAnyTarget { continue; } - possibleTargets.removeAll(getTargets()); for (UUID targetId : possibleTargets) { TargetAnyTarget target = this.copy(); target.clearChosen(); diff --git a/Mage.Sets/src/mage/cards/f/FrogkinKidnapper.java b/Mage.Sets/src/mage/cards/f/FrogkinKidnapper.java index 6cd928951b5..1d6dc327fc2 100644 --- a/Mage.Sets/src/mage/cards/f/FrogkinKidnapper.java +++ b/Mage.Sets/src/mage/cards/f/FrogkinKidnapper.java @@ -85,7 +85,7 @@ class FrogkinKidnapperEffect extends OneShotEffect { opponent.revealCards(source, opponent.getHand(), game); TargetCard target = new TargetCard(1, Zone.HAND, StaticFilters.FILTER_CARD_A_NON_LAND); Cards toRansom = new CardsImpl(); - if (controller.chooseTarget(outcome, opponent.getHand(), target, source, game)) { + if (controller.choose(outcome, opponent.getHand(), target, source, game)) { toRansom.addAll(target.getTargets()); String exileName = "Ransomed (owned by " + opponent.getName() + ")"; diff --git a/Mage.Sets/src/mage/cards/g/GateSmasher.java b/Mage.Sets/src/mage/cards/g/GateSmasher.java index 276d48bc734..a5f77b188c3 100644 --- a/Mage.Sets/src/mage/cards/g/GateSmasher.java +++ b/Mage.Sets/src/mage/cards/g/GateSmasher.java @@ -9,13 +9,12 @@ import mage.abilities.keyword.EquipAbility; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.AttachmentType; -import mage.constants.CardType; -import mage.constants.ComparisonType; -import mage.constants.SubType; +import mage.constants.*; import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreatureCard; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.ToughnessPredicate; +import mage.target.TargetCard; import mage.target.TargetPermanent; import java.util.UUID; diff --git a/Mage.Sets/src/mage/cards/g/GauntletsOfChaos.java b/Mage.Sets/src/mage/cards/g/GauntletsOfChaos.java index 049d1ebeb74..9a073fe7d64 100644 --- a/Mage.Sets/src/mage/cards/g/GauntletsOfChaos.java +++ b/Mage.Sets/src/mage/cards/g/GauntletsOfChaos.java @@ -69,9 +69,10 @@ class GauntletsOfChaosFirstTarget extends TargetControlledPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (super.canTarget(controllerId, id, source, game)) { - Set cardTypes = getOpponentPermanentCardTypes(source.getSourceId(), controllerId, game); + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + Set cardTypes = getOpponentPermanentCardTypes(playerId, game); + + if (super.canTarget(playerId, id, source, game)) { Permanent permanent = game.getPermanent(id); for (CardType type : permanent.getCardType(game)) { if (cardTypes.contains(type)) { @@ -84,23 +85,21 @@ class GauntletsOfChaosFirstTarget extends TargetControlledPermanent { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - // get all cardtypes from opponents permanents - Set cardTypes = getOpponentPermanentCardTypes(source.getSourceId(), sourceControllerId, game); + Set cardTypes = getOpponentPermanentCardTypes(sourceControllerId, game); + Set possibleTargets = new HashSet<>(); MageObject targetSource = game.getObject(source); if (targetSource != null) { for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId()) && permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - for (CardType type : permanent.getCardType(game)) { - if (cardTypes.contains(type)) { - possibleTargets.add(permanent.getId()); - break; - } + for (CardType type : permanent.getCardType(game)) { + if (cardTypes.contains(type)) { + possibleTargets.add(permanent.getId()); + break; } } } } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override @@ -108,7 +107,7 @@ class GauntletsOfChaosFirstTarget extends TargetControlledPermanent { return new GauntletsOfChaosFirstTarget(this); } - private EnumSet getOpponentPermanentCardTypes(UUID sourceId, UUID sourceControllerId, Game game) { + private EnumSet getOpponentPermanentCardTypes(UUID sourceControllerId, Game game) { Player controller = game.getPlayer(sourceControllerId); EnumSet cardTypes = EnumSet.noneOf(CardType.class); if (controller != null) { @@ -125,8 +124,6 @@ class GauntletsOfChaosFirstTarget extends TargetControlledPermanent { class GauntletsOfChaosSecondTarget extends TargetPermanent { - private Permanent firstTarget = null; - public GauntletsOfChaosSecondTarget() { super(); this.filter = this.filter.copy(); @@ -136,44 +133,46 @@ class GauntletsOfChaosSecondTarget extends TargetPermanent { private GauntletsOfChaosSecondTarget(final GauntletsOfChaosSecondTarget target) { super(target); - this.firstTarget = target.firstTarget; } @Override public boolean canTarget(UUID id, Ability source, Game game) { - if (super.canTarget(id, source, game)) { - Permanent target1 = game.getPermanent(source.getFirstTarget()); - Permanent opponentPermanent = game.getPermanent(id); - if (target1 != null && opponentPermanent != null) { - return target1.shareTypes(opponentPermanent, game); - } + Permanent ownPermanent = game.getPermanent(source.getFirstTarget()); + Permanent possiblePermanent = game.getPermanent(id); + if (ownPermanent == null || possiblePermanent == null) { + return false; } - return false; + return super.canTarget(id, source, game) && ownPermanent.shareTypes(possiblePermanent, game); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); - if (firstTarget != null) { - MageObject targetSource = game.getObject(source); - if (targetSource != null) { - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId()) && permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - if (permanent.shareTypes(firstTarget, game)) { - possibleTargets.add(permanent.getId()); - } - } + + Permanent ownPermanent = game.getPermanent(source.getFirstTarget()); + for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { + if (ownPermanent == null) { + // playable or first target not yet selected + // use all + possibleTargets.add(permanent.getId()); + } else { + // real + // filter by shared type + if (permanent.shareTypes(ownPermanent, game)) { + possibleTargets.add(permanent.getId()); } } } - return possibleTargets; + possibleTargets.removeIf(id -> ownPermanent != null && ownPermanent.getId().equals(id)); + + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) { - firstTarget = game.getPermanent(source.getFirstTarget()); - return super.chooseTarget(Outcome.Damage, playerId, source, game); + // AI hint with better outcome + return super.chooseTarget(Outcome.GainControl, playerId, source, game); } @Override diff --git a/Mage.Sets/src/mage/cards/g/GenerousPatron.java b/Mage.Sets/src/mage/cards/g/GenerousPatron.java index 92037434730..7e6e7a9937c 100644 --- a/Mage.Sets/src/mage/cards/g/GenerousPatron.java +++ b/Mage.Sets/src/mage/cards/g/GenerousPatron.java @@ -1,16 +1,14 @@ package mage.cards.g; import mage.MageInt; +import mage.abilities.common.PutCounterOnPermanentTriggeredAbility; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.keyword.SupportAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.TargetController; -import mage.filter.FilterPermanent; -import mage.filter.common.FilterCreaturePermanent; -import mage.abilities.common.PutCounterOnCreatureTriggeredAbility; +import mage.filter.StaticFilters; import java.util.UUID; @@ -19,13 +17,6 @@ import java.util.UUID; */ public final class GenerousPatron extends CardImpl { - private static final FilterPermanent filter - = new FilterCreaturePermanent("creature you don't control"); - - static { - filter.add(TargetController.NOT_YOU.getControllerPredicate()); - } - public GenerousPatron(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); @@ -38,7 +29,8 @@ public final class GenerousPatron extends CardImpl { this.addAbility(new SupportAbility(this, 2)); // Whenever you put one or more counters on a creature you don't control, draw a card. - this.addAbility(new PutCounterOnCreatureTriggeredAbility(new DrawCardSourceControllerEffect(1), filter)); + this.addAbility(new PutCounterOnPermanentTriggeredAbility(new DrawCardSourceControllerEffect(1), + null, StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL)); } private GenerousPatron(final GenerousPatron card) { diff --git a/Mage.Sets/src/mage/cards/g/GhastlordOfFugue.java b/Mage.Sets/src/mage/cards/g/GhastlordOfFugue.java index 271c34aaa56..f60ad91dab2 100644 --- a/Mage.Sets/src/mage/cards/g/GhastlordOfFugue.java +++ b/Mage.Sets/src/mage/cards/g/GhastlordOfFugue.java @@ -82,7 +82,7 @@ class GhastlordOfFugueEffect extends OneShotEffect { TargetCard target = new TargetCard(Zone.HAND, new FilterCard()); target.withNotTarget(true); Card chosenCard = null; - if (controller.chooseTarget(Outcome.Benefit, targetPlayer.getHand(), target, source, game)) { + if (controller.choose(Outcome.Benefit, targetPlayer.getHand(), target, source, game)) { chosenCard = game.getCard(target.getFirstTarget()); } if (chosenCard != null) { diff --git a/Mage.Sets/src/mage/cards/g/GiltspireAvenger.java b/Mage.Sets/src/mage/cards/g/GiltspireAvenger.java index fd51af64dd2..9f538b41201 100644 --- a/Mage.Sets/src/mage/cards/g/GiltspireAvenger.java +++ b/Mage.Sets/src/mage/cards/g/GiltspireAvenger.java @@ -2,7 +2,6 @@ package mage.cards.g; import mage.MageInt; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; @@ -12,15 +11,11 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; -import mage.filter.StaticFilters; -import mage.game.Game; -import mage.game.permanent.Permanent; +import mage.constants.TargetController; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.other.DamagedPlayerThisTurnPredicate; import mage.target.TargetPermanent; -import mage.watchers.common.PlayerDamagedBySourceWatcher; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; /** @@ -28,6 +23,12 @@ import java.util.UUID; */ public final class GiltspireAvenger extends CardImpl { + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature that dealt damage to you this turn"); + + static { + filter.add(new DamagedPlayerThisTurnPredicate(TargetController.YOU)); + } + public GiltspireAvenger(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{W}{U}"); this.subtype.add(SubType.HUMAN); @@ -41,7 +42,7 @@ public final class GiltspireAvenger extends CardImpl { // {T}: Destroy target creature that dealt damage to you this turn. Ability ability = new SimpleActivatedAbility(new DestroyTargetEffect(), new TapSourceCost()); - ability.addTarget(new GiltspireAvengerTarget()); + ability.addTarget(new TargetPermanent(filter)); this.addAbility(ability); } @@ -55,64 +56,3 @@ public final class GiltspireAvenger extends CardImpl { return new GiltspireAvenger(this); } } - -class GiltspireAvengerTarget extends TargetPermanent { - - public GiltspireAvengerTarget() { - super(1, 1, StaticFilters.FILTER_PERMANENT_CREATURE, false); - targetName = "creature that dealt damage to you this turn"; - } - - private GiltspireAvengerTarget(final GiltspireAvengerTarget target) { - super(target); - } - - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - PlayerDamagedBySourceWatcher watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, source.getControllerId()); - if (watcher != null && watcher.hasSourceDoneDamage(id, game)) { - return super.canTarget(id, source, game); - } - return false; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set availablePossibleTargets = super.possibleTargets(sourceControllerId, source, game); - Set possibleTargets = new HashSet<>(); - PlayerDamagedBySourceWatcher watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, sourceControllerId); - for (UUID targetId : availablePossibleTargets) { - Permanent permanent = game.getPermanent(targetId); - if (permanent != null && watcher != null && watcher.hasSourceDoneDamage(targetId, game)) { - possibleTargets.add(targetId); - } - } - return possibleTargets; - } - - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int remainingTargets = this.minNumberOfTargets - targets.size(); - if (remainingTargets == 0) { - return true; - } - int count = 0; - MageObject targetSource = game.getObject(source); - PlayerDamagedBySourceWatcher watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, sourceControllerId); - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId()) && permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game) - && watcher != null && watcher.hasSourceDoneDamage(permanent.getId(), game)) { - count++; - if (count >= remainingTargets) { - return true; - } - } - } - return false; - } - - @Override - public GiltspireAvengerTarget copy() { - return new GiltspireAvengerTarget(this); - } -} diff --git a/Mage.Sets/src/mage/cards/g/GlyphOfDelusion.java b/Mage.Sets/src/mage/cards/g/GlyphOfDelusion.java index 4b6b8c25125..06ae97d9e0f 100644 --- a/Mage.Sets/src/mage/cards/g/GlyphOfDelusion.java +++ b/Mage.Sets/src/mage/cards/g/GlyphOfDelusion.java @@ -1,9 +1,7 @@ package mage.cards.g; -import mage.MageObject; import mage.MageObjectReference; import mage.abilities.Ability; -import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.SourceHasCounterCondition; import mage.abilities.decorator.ConditionalContinuousRuleModifyingEffect; @@ -11,11 +9,14 @@ import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DontUntapInControllersUntapStepSourceEffect; import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; import mage.abilities.effects.common.counter.RemoveCounterSourceEffect; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; import mage.counters.CounterType; -import mage.filter.StaticFilters; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; import mage.game.permanent.Permanent; @@ -59,8 +60,6 @@ public final class GlyphOfDelusion extends CardImpl { class GlyphOfDelusionSecondTarget extends TargetPermanent { - private Permanent firstTarget = null; - public GlyphOfDelusionSecondTarget() { super(); withTargetName("target creature that target Wall blocked this turn"); @@ -68,33 +67,39 @@ class GlyphOfDelusionSecondTarget extends TargetPermanent { private GlyphOfDelusionSecondTarget(final GlyphOfDelusionSecondTarget target) { super(target); - this.firstTarget = target.firstTarget; } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); - if (firstTarget != null) { - BlockedAttackerWatcher watcher = game.getState().getWatcher(BlockedAttackerWatcher.class); - if (watcher != null) { - MageObject targetSource = game.getObject(source); - if (targetSource != null) { - for (Permanent creature : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, sourceControllerId, source, game)) { - if (!targets.containsKey(creature.getId()) && creature.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), new MageObjectReference(firstTarget, game))) { - possibleTargets.add(creature.getId()); - } - } - } + + BlockedAttackerWatcher watcher = game.getState().getWatcher(BlockedAttackerWatcher.class); + if (watcher == null) { + return possibleTargets; + } + + Permanent targetWall = game.getPermanent(source.getFirstTarget()); + for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { + if (targetWall == null) { + // playable or first target not yet selected + // use all + possibleTargets.add(permanent.getId()); + } else { + // real + // filter by blocked + if (watcher.creatureHasBlockedAttacker(new MageObjectReference(permanent, game), new MageObjectReference(targetWall, game))) { + possibleTargets.add(permanent.getId()); } } } - return possibleTargets; + possibleTargets.removeIf(id -> targetWall != null && targetWall.getId().equals(id)); + + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) { - firstTarget = game.getPermanent(source.getFirstTarget()); + // AI hint with better outcome return super.chooseTarget(Outcome.Tap, playerId, source, game); } @@ -133,8 +138,7 @@ class GlyphOfDelusionEffect extends OneShotEffect { effect.setTargetPointer(new FixedTarget(targetPermanent.getId(), game)); game.addEffect(effect, source); - BeginningOfUpkeepTriggeredAbility ability2 = new BeginningOfUpkeepTriggeredAbility(new RemoveCounterSourceEffect(CounterType.GLYPH.createInstance()) - ); + BeginningOfUpkeepTriggeredAbility ability2 = new BeginningOfUpkeepTriggeredAbility(new RemoveCounterSourceEffect(CounterType.GLYPH.createInstance())); GainAbilityTargetEffect effect2 = new GainAbilityTargetEffect(ability2, Duration.Custom); effect2.setTargetPointer(new FixedTarget(targetPermanent.getId(), game)); game.addEffect(effect2, source); diff --git a/Mage.Sets/src/mage/cards/g/GoblinArtisans.java b/Mage.Sets/src/mage/cards/g/GoblinArtisans.java index d134b26b94b..1c4a54413ec 100644 --- a/Mage.Sets/src/mage/cards/g/GoblinArtisans.java +++ b/Mage.Sets/src/mage/cards/g/GoblinArtisans.java @@ -81,8 +81,8 @@ class GoblinArtisansTarget extends TargetSpell { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } MageObjectReference sourceRef = new MageObjectReference(source.getSourceObject(game), game); diff --git a/Mage.Sets/src/mage/cards/g/GracefulTakedown.java b/Mage.Sets/src/mage/cards/g/GracefulTakedown.java index 2c946a8ba44..42225c2acb6 100644 --- a/Mage.Sets/src/mage/cards/g/GracefulTakedown.java +++ b/Mage.Sets/src/mage/cards/g/GracefulTakedown.java @@ -61,8 +61,8 @@ class GracefulTakedownTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } Permanent permanent = game.getPermanent(id); diff --git a/Mage.Sets/src/mage/cards/g/GrimeGorger.java b/Mage.Sets/src/mage/cards/g/GrimeGorger.java index 2f55fa76495..01ab1679c2b 100644 --- a/Mage.Sets/src/mage/cards/g/GrimeGorger.java +++ b/Mage.Sets/src/mage/cards/g/GrimeGorger.java @@ -129,7 +129,7 @@ class GrimeGorgerTarget extends TargetCardInGraveyard { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, null, game)); + possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, source, game)); return possibleTargets; } diff --git a/Mage.Sets/src/mage/cards/g/Gurzigost.java b/Mage.Sets/src/mage/cards/g/Gurzigost.java index dfbf9edb8b6..a9faa349cd3 100644 --- a/Mage.Sets/src/mage/cards/g/Gurzigost.java +++ b/Mage.Sets/src/mage/cards/g/Gurzigost.java @@ -92,7 +92,7 @@ class GurzigostCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage.Sets/src/mage/cards/h/HamletGlutton.java b/Mage.Sets/src/mage/cards/h/HamletGlutton.java index 703877d465b..376bef9b089 100644 --- a/Mage.Sets/src/mage/cards/h/HamletGlutton.java +++ b/Mage.Sets/src/mage/cards/h/HamletGlutton.java @@ -63,7 +63,7 @@ enum HamletGluttonAdjuster implements CostAdjuster { @Override public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) - || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { + || (game.inCheckPlayableState() && bargainCost.canPay(ability, ability, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); } } diff --git a/Mage.Sets/src/mage/cards/h/HapatraVizierOfPoisons.java b/Mage.Sets/src/mage/cards/h/HapatraVizierOfPoisons.java index de22bd36181..f9377b114d2 100644 --- a/Mage.Sets/src/mage/cards/h/HapatraVizierOfPoisons.java +++ b/Mage.Sets/src/mage/cards/h/HapatraVizierOfPoisons.java @@ -3,7 +3,7 @@ package mage.cards.h; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; -import mage.abilities.common.PutCounterOnCreatureTriggeredAbility; +import mage.abilities.common.PutCounterOnPermanentTriggeredAbility; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.cards.CardImpl; @@ -12,6 +12,7 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; import mage.counters.CounterType; +import mage.filter.StaticFilters; import mage.game.permanent.token.DeathtouchSnakeToken; import mage.target.common.TargetCreaturePermanent; @@ -37,7 +38,8 @@ public final class HapatraVizierOfPoisons extends CardImpl { this.addAbility(ability); // Whenever you put one or more -1/-1 counters on a creature, create a 1/1 green Snake creature token with deathtouch. - this.addAbility(new PutCounterOnCreatureTriggeredAbility(new CreateTokenEffect(new DeathtouchSnakeToken()), CounterType.M1M1.createInstance())); + this.addAbility(new PutCounterOnPermanentTriggeredAbility(new CreateTokenEffect(new DeathtouchSnakeToken()), + CounterType.M1M1, StaticFilters.FILTER_PERMANENT_CREATURE)); } private HapatraVizierOfPoisons(final HapatraVizierOfPoisons card) { diff --git a/Mage.Sets/src/mage/cards/i/IceOut.java b/Mage.Sets/src/mage/cards/i/IceOut.java index 6d4dcf0eeb3..97fa2d5a392 100644 --- a/Mage.Sets/src/mage/cards/i/IceOut.java +++ b/Mage.Sets/src/mage/cards/i/IceOut.java @@ -54,7 +54,7 @@ enum IceOutAdjuster implements CostAdjuster { @Override public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) - || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { + || (game.inCheckPlayableState() && bargainCost.canPay(ability, ability, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 1); } } diff --git a/Mage.Sets/src/mage/cards/i/IlluminatedFolio.java b/Mage.Sets/src/mage/cards/i/IlluminatedFolio.java index 22bf2737054..1018125175e 100644 --- a/Mage.Sets/src/mage/cards/i/IlluminatedFolio.java +++ b/Mage.Sets/src/mage/cards/i/IlluminatedFolio.java @@ -1,7 +1,5 @@ package mage.cards.i; -import mage.MageItem; -import mage.ObjectColor; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.RevealTargetFromHandCost; @@ -19,9 +17,9 @@ import mage.game.Game; import mage.target.common.TargetCardInHand; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; /** * @author TheElk801 @@ -67,51 +65,8 @@ class IlluminatedFolioTarget extends TargetCardInHand { } @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - return super.canChoose(sourceControllerId, source, game) - && !possibleTargets(sourceControllerId, source, game).isEmpty(); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - if (this.getTargets().size() == 1) { - Card card = game.getCard(this.getTargets().get(0)); - possibleTargets.removeIf( - uuid -> !game - .getCard(uuid) - .getColor(game) - .shares(card.getColor(game)) - ); - return possibleTargets; - } - if (possibleTargets.size() < 2) { - possibleTargets.clear(); - return possibleTargets; - } - Set allTargets = possibleTargets - .stream() - .map(game::getCard) - .collect(Collectors.toSet()); - possibleTargets.clear(); - for (ObjectColor color : ObjectColor.getAllColors()) { - Set inColor = allTargets - .stream() - .filter(card -> card.getColor(game).shares(color)) - .collect(Collectors.toSet()); - if (inColor.size() > 1) { - inColor.stream().map(MageItem::getId).forEach(possibleTargets::add); - } - if (possibleTargets.size() == allTargets.size()) { - break; - } - } - return possibleTargets; - } - - @Override - public boolean canTarget(UUID id, Game game) { - if (!super.canTarget(id, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } List targetList = this.getTargets(); @@ -123,9 +78,17 @@ class IlluminatedFolioTarget extends TargetCardInHand { && targetList .stream() .map(game::getCard) + .filter(Objects::nonNull) .anyMatch(c -> c.getColor(game).shares(card.getColor(game))); } + @Override + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); + possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, source, game)); + return possibleTargets; + } + @Override public IlluminatedFolioTarget copy() { return new IlluminatedFolioTarget(this); diff --git a/Mage.Sets/src/mage/cards/i/ImpelledGiant.java b/Mage.Sets/src/mage/cards/i/ImpelledGiant.java index 5a32d0d837f..1cd50299212 100644 --- a/Mage.Sets/src/mage/cards/i/ImpelledGiant.java +++ b/Mage.Sets/src/mage/cards/i/ImpelledGiant.java @@ -99,7 +99,7 @@ class ImpelledGiantCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return target.canChoose(controllerId, source, game); + return target.canChooseOrAlreadyChosen(controllerId, source, game); } @Override diff --git a/Mage.Sets/src/mage/cards/i/InvasionOfGobakhan.java b/Mage.Sets/src/mage/cards/i/InvasionOfGobakhan.java index a180880981f..d44cf14a51b 100644 --- a/Mage.Sets/src/mage/cards/i/InvasionOfGobakhan.java +++ b/Mage.Sets/src/mage/cards/i/InvasionOfGobakhan.java @@ -79,9 +79,7 @@ class InvasionOfGobakhanEffect extends OneShotEffect { if (controller == null || opponent == null || opponent.getHand().isEmpty()) { return false; } - TargetCard target = new TargetCardInHand( - 0, 1, StaticFilters.FILTER_CARD_A_NON_LAND - ); + TargetCard target = new TargetCard(0, 1, Zone.HAND, StaticFilters.FILTER_CARD_A_NON_LAND); controller.choose(outcome, opponent.getHand(), target, source, game); Card card = opponent.getHand().get(target.getFirstTarget(), game); if (card == null) { diff --git a/Mage.Sets/src/mage/cards/i/IwamoriOfTheOpenFist.java b/Mage.Sets/src/mage/cards/i/IwamoriOfTheOpenFist.java index dd0f1b169ab..47ebfcd443e 100644 --- a/Mage.Sets/src/mage/cards/i/IwamoriOfTheOpenFist.java +++ b/Mage.Sets/src/mage/cards/i/IwamoriOfTheOpenFist.java @@ -1,32 +1,27 @@ - package mage.cards.i; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.TrampleAbility; import mage.cards.*; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Outcome; -import mage.constants.SuperType; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.FilterCard; import mage.game.Game; import mage.players.Player; import mage.target.Target; -import mage.target.common.TargetCardInHand; +import mage.target.TargetCard; + +import java.util.UUID; /** - * * @author LevelX2 */ public final class IwamoriOfTheOpenFist extends CardImpl { public IwamoriOfTheOpenFist(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{G}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}{G}"); this.supertype.add(SuperType.LEGENDARY); this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.MONK); @@ -80,15 +75,11 @@ class IwamoriOfTheOpenFistEffect extends OneShotEffect { Cards cards = new CardsImpl(); for (UUID playerId : game.getOpponents(controller.getId())) { Player opponent = game.getPlayer(playerId); - Target target = new TargetCardInHand(filter); - if (opponent != null && target.canChoose(opponent.getId(), source, game)) { - if (opponent.chooseUse(Outcome.PutCreatureInPlay, "Put a legendary creature card from your hand onto the battlefield?", source, game)) { - if (target.chooseTarget(Outcome.PutCreatureInPlay, opponent.getId(), source, game)) { - Card card = game.getCard(target.getFirstTarget()); - if (card != null) { - cards.add(card); - } - } + Target target = new TargetCard(0, 1, Zone.HAND, filter).withChooseHint("put from hand to battlefield"); + if (target.choose(Outcome.PutCreatureInPlay, opponent.getId(), source, game)) { + Card card = game.getCard(target.getFirstTarget()); + if (card != null) { + cards.add(card); } } } diff --git a/Mage.Sets/src/mage/cards/j/JohannsStopgap.java b/Mage.Sets/src/mage/cards/j/JohannsStopgap.java index fc34f2aa974..ff579d29591 100644 --- a/Mage.Sets/src/mage/cards/j/JohannsStopgap.java +++ b/Mage.Sets/src/mage/cards/j/JohannsStopgap.java @@ -56,7 +56,7 @@ enum JohannsStopgapAdjuster implements CostAdjuster { @Override public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) - || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { + || (game.inCheckPlayableState() && bargainCost.canPay(ability, ability, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); } } diff --git a/Mage.Sets/src/mage/cards/j/JotunGrunt.java b/Mage.Sets/src/mage/cards/j/JotunGrunt.java index 1d271b5065b..0571e036c0e 100644 --- a/Mage.Sets/src/mage/cards/j/JotunGrunt.java +++ b/Mage.Sets/src/mage/cards/j/JotunGrunt.java @@ -78,7 +78,7 @@ class JotunGruntCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage.Sets/src/mage/cards/j/JourneyForTheElixir.java b/Mage.Sets/src/mage/cards/j/JourneyForTheElixir.java index e78ed4ef421..ef35d8307a4 100644 --- a/Mage.Sets/src/mage/cards/j/JourneyForTheElixir.java +++ b/Mage.Sets/src/mage/cards/j/JourneyForTheElixir.java @@ -14,7 +14,6 @@ import mage.filter.predicate.mageobject.NamePredicate; import mage.game.Game; import mage.players.Player; import mage.target.TargetCard; -import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetCardInYourGraveyard; import java.util.Objects; @@ -62,28 +61,31 @@ class JourneyForTheElixirEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - if (player == null) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { return false; } - TargetCardInLibrary targetCardInLibrary = new JourneyForTheElixirLibraryTarget(); - player.searchLibrary(targetCardInLibrary, source, game); - Cards cards = new CardsImpl(targetCardInLibrary.getTargets()); - TargetCard target = new JourneyForTheElixirGraveyardTarget(cards); - player.choose(outcome, target, source, game); - cards.addAll(target.getTargets()); - player.revealCards(source, cards, game); - player.moveCards(cards, Zone.HAND, source, game); - player.shuffleLibrary(source, game); - return true; + + // search your library and graveyard for 2 cards + Cards allCards = new CardsImpl(); + allCards.addAll(controller.getLibrary().getCardList()); + allCards.addAll(controller.getGraveyard()); + TargetCard target = new JourneyForTheElixirTarget(); + if (controller.choose(Outcome.Benefit, allCards, target, source, game)) { + Cards cards = new CardsImpl(target.getTargets()); + controller.revealCards(source, cards, game); + controller.moveCards(cards, Zone.HAND, source, game); + controller.shuffleLibrary(source, game); + return true; + } + return false; } } -class JourneyForTheElixirLibraryTarget extends TargetCardInLibrary { +class JourneyForTheElixirTarget extends TargetCard { private static final String name = "Jiang Yanggu"; - private static final FilterCard filter - = new FilterCard("a basic land card and a card named Jiang Yanggu"); + private static final FilterCard filter = new FilterCard("a basic land card and a card named Jiang Yanggu"); static { filter.add(Predicates.or( @@ -95,17 +97,17 @@ class JourneyForTheElixirLibraryTarget extends TargetCardInLibrary { )); } - JourneyForTheElixirLibraryTarget() { - super(0, 2, filter); + JourneyForTheElixirTarget() { + super(2, 2, Zone.ALL, filter); } - private JourneyForTheElixirLibraryTarget(final JourneyForTheElixirLibraryTarget target) { + private JourneyForTheElixirTarget(final JourneyForTheElixirTarget target) { super(target); } @Override - public JourneyForTheElixirLibraryTarget copy() { - return new JourneyForTheElixirLibraryTarget(this); + public JourneyForTheElixirTarget copy() { + return new JourneyForTheElixirTarget(this); } @Override @@ -117,95 +119,35 @@ class JourneyForTheElixirLibraryTarget extends TargetCardInLibrary { if (card == null) { return false; } - if (this.getTargets().isEmpty()) { - return true; - } + Cards cards = new CardsImpl(this.getTargets()); - if (card.isBasic(game) - && card.isLand(game) - && cards + boolean hasLand = cards .getCards(game) .stream() .filter(Objects::nonNull) .filter(c -> c.isBasic(game)) - .anyMatch(c -> c.isLand(game))) { - return false; - } - if (name.equals(card.getName()) - && cards + .anyMatch(c -> c.isLand(game)); + boolean hasJiang = cards .getCards(game) .stream() .map(MageObject::getName) - .anyMatch(name::equals)) { - return false; + .anyMatch(name::equals); + + if (!hasLand && card.isBasic(game) && card.isLand(game)) { + return true; } - return true; - } -} -class JourneyForTheElixirGraveyardTarget extends TargetCardInYourGraveyard { + if (!hasJiang && name.equals(card.getName())) { + return true; + } - private static final String name = "Jiang Yanggu"; - private static final FilterCard filter - = new FilterCard("a basic land card and a card named Jiang Yanggu"); - - static { - filter.add(Predicates.or( - Predicates.and( - SuperType.BASIC.getPredicate(), - CardType.LAND.getPredicate() - ), - new NamePredicate(name) - )); - } - - private final Cards cards = new CardsImpl(); - - JourneyForTheElixirGraveyardTarget(Cards cards) { - super(0, Integer.MAX_VALUE, filter, true); - this.cards.addAll(cards); - } - - private JourneyForTheElixirGraveyardTarget(final JourneyForTheElixirGraveyardTarget target) { - super(target); - this.cards.addAll(target.cards); - } - - @Override - public JourneyForTheElixirGraveyardTarget copy() { - return new JourneyForTheElixirGraveyardTarget(this); + return false; } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - Cards alreadyTargeted = new CardsImpl(this.getTargets()); - alreadyTargeted.addAll(cards); - boolean hasBasic = alreadyTargeted - .getCards(game) - .stream() - .filter(Objects::nonNull) - .filter(c -> c.isLand(game)) - .anyMatch(c -> c.isBasic(game)); - possibleTargets.removeIf(uuid -> { - Card card = game.getCard(uuid); - return card != null - && hasBasic - && card.isLand(game) - && card.isBasic(game); - }); - boolean hasYanggu = alreadyTargeted - .getCards(game) - .stream() - .filter(Objects::nonNull) - .map(MageObject::getName) - .anyMatch(name::equals); - possibleTargets.removeIf(uuid -> { - Card card = game.getCard(uuid); - return card != null - && hasYanggu - && name.equals(card.getName()); - }); + possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, source, game)); return possibleTargets; } } diff --git a/Mage.Sets/src/mage/cards/k/KairiTheSwirlingSky.java b/Mage.Sets/src/mage/cards/k/KairiTheSwirlingSky.java index 37d1f67a3b1..eff94a35fce 100644 --- a/Mage.Sets/src/mage/cards/k/KairiTheSwirlingSky.java +++ b/Mage.Sets/src/mage/cards/k/KairiTheSwirlingSky.java @@ -88,8 +88,8 @@ class KairiTheSwirlingSkyTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 6, game); } diff --git a/Mage.Sets/src/mage/cards/k/KarnsSylex.java b/Mage.Sets/src/mage/cards/k/KarnsSylex.java index df61aadcc9b..f4e19c66515 100644 --- a/Mage.Sets/src/mage/cards/k/KarnsSylex.java +++ b/Mage.Sets/src/mage/cards/k/KarnsSylex.java @@ -2,21 +2,22 @@ package mage.cards.k; import mage.abilities.Ability; import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.common.CantPayLifeOrSacrificeAbility; import mage.abilities.common.EntersBattlefieldTappedAbility; -import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.common.ExileSourceCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.OneShotEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Outcome; +import mage.constants.SuperType; import mage.filter.common.FilterNonlandPermanent; import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.players.Player; import mage.util.CardUtil; import java.util.UUID; @@ -33,7 +34,7 @@ public class KarnsSylex extends CardImpl { this.addAbility(new EntersBattlefieldTappedAbility()); // Players can’t pay life to cast spells or to activate abilities that aren’t mana abilities. - this.addAbility(new SimpleStaticAbility(new KarnsSylexEffect())); + this.addAbility(new CantPayLifeOrSacrificeAbility(true, null)); // {X}, {T}, Exile Karn’s Sylex: Destroy each nonland permanent with mana value X or less. Activate only as a sorcery. Ability ability = new ActivateAsSorceryActivatedAbility(new KarnsSylexDestroyEffect(), new ManaCostsImpl<>("{X}")); @@ -52,32 +53,6 @@ public class KarnsSylex extends CardImpl { } } -class KarnsSylexEffect extends ContinuousEffectImpl { - - KarnsSylexEffect() { - super(Duration.WhileOnBattlefield, Layer.PlayerEffects, SubLayer.NA, Outcome.Detriment); - staticText = "Players can't pay life to cast spells or to activate abilities that aren't mana abilities"; - } - - private KarnsSylexEffect(final KarnsSylexEffect effect) { - super(effect); - } - - @Override - public KarnsSylexEffect copy() { - return new KarnsSylexEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { - Player player = game.getPlayer(playerId); - player.setPayLifeCostLevel(Player.PayLifeCostLevel.onlyManaAbilities); - } - return true; - } -} - class KarnsSylexDestroyEffect extends OneShotEffect { KarnsSylexDestroyEffect() { diff --git a/Mage.Sets/src/mage/cards/k/KataraTheFearless.java b/Mage.Sets/src/mage/cards/k/KataraTheFearless.java new file mode 100644 index 00000000000..5928803430f --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KataraTheFearless.java @@ -0,0 +1,79 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class KataraTheFearless extends CardImpl { + + public KataraTheFearless(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{W}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.subtype.add(SubType.ALLY); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // If a triggered ability of an Ally you control triggers, that ability triggers an additional time. + this.addAbility(new SimpleStaticAbility(new KataraTheFearlessEffect())); + } + + private KataraTheFearless(final KataraTheFearless card) { + super(card); + } + + @Override + public KataraTheFearless copy() { + return new KataraTheFearless(this); + } +} + +class KataraTheFearlessEffect extends ReplacementEffectImpl { + + KataraTheFearlessEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "if a triggered ability of an Ally you control triggers, that ability triggers an additional time"; + } + + private KataraTheFearlessEffect(final KataraTheFearlessEffect effect) { + super(effect); + } + + @Override + public KataraTheFearlessEffect copy() { + return new KataraTheFearlessEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.NUMBER_OF_TRIGGERS; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId()); + return permanent != null + && permanent.isControlledBy(source.getControllerId()) + && permanent.hasSubtype(SubType.ALLY, game); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + event.setAmount(event.getAmount() + 1); + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/k/KataraWaterbendingMaster.java b/Mage.Sets/src/mage/cards/k/KataraWaterbendingMaster.java new file mode 100644 index 00000000000..a6123b98f0b --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KataraWaterbendingMaster.java @@ -0,0 +1,60 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.condition.common.OpponentsTurnCondition; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CountersControllerCount; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.counter.AddCountersPlayersEffect; +import mage.abilities.effects.common.discard.DiscardControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.TargetController; +import mage.counters.CounterType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class KataraWaterbendingMaster extends CardImpl { + + private static final DynamicValue xValue = new CountersControllerCount(CounterType.EXPERIENCE); + + public KataraWaterbendingMaster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.subtype.add(SubType.ALLY); + this.power = new MageInt(1); + this.toughness = new MageInt(3); + + // Whenever you cast a spell during an opponent's turn, you get an experience counter. + this.addAbility(new SpellCastControllerTriggeredAbility(new AddCountersPlayersEffect( + CounterType.EXPERIENCE.createInstance(), TargetController.YOU), false + ).withTriggerCondition(OpponentsTurnCondition.instance)); + + // Whenever Katara attacks, you may draw a card for each experience counter you have. If you do, discard a card. + Ability ability = new AttacksTriggeredAbility(new DrawCardSourceControllerEffect(xValue) + .setText("draw a card for each experience counter you have")); + ability.addEffect(new DiscardControllerEffect(1).concatBy("If you do,")); + this.addAbility(ability); + } + + private KataraWaterbendingMaster(final KataraWaterbendingMaster card) { + super(card); + } + + @Override + public KataraWaterbendingMaster copy() { + return new KataraWaterbendingMaster(this); + } +} diff --git a/Mage.Sets/src/mage/cards/k/KateStewart.java b/Mage.Sets/src/mage/cards/k/KateStewart.java index 93309859588..3318f2e4053 100644 --- a/Mage.Sets/src/mage/cards/k/KateStewart.java +++ b/Mage.Sets/src/mage/cards/k/KateStewart.java @@ -2,8 +2,8 @@ package mage.cards.k; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.PutCounterOnPermanentTriggeredAbility; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; @@ -18,7 +18,6 @@ import mage.constants.*; import mage.counters.CounterType; import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.events.GameEvent; import mage.game.permanent.token.SoldierToken; import java.util.UUID; @@ -38,7 +37,8 @@ public final class KateStewart extends CardImpl { this.toughness = new MageInt(3); // Whenever you put one or more time counters on a permanent you control, create a 1/1 white Soldier creature token. - this.addAbility(new KateStewartTriggeredAbility()); + this.addAbility(new PutCounterOnPermanentTriggeredAbility(new CreateTokenEffect(new SoldierToken()), + CounterType.TIME, StaticFilters.FILTER_CONTROLLED_PERMANENT)); // Whenever Kate Stewart attacks, you may pay {8}. If you do, attacking creatures get +X/+X until end of turn, where X is the number of time counters among permanents you control. this.addAbility(new AttacksTriggeredAbility(new DoIfCostPaid(new BoostAllEffect( @@ -57,35 +57,6 @@ public final class KateStewart extends CardImpl { } } -class KateStewartTriggeredAbility extends TriggeredAbilityImpl { - - KateStewartTriggeredAbility() { - super(Zone.BATTLEFIELD, new CreateTokenEffect(new SoldierToken())); - setTriggerPhrase("Whenever you put one or more time counters on a permanent you control, "); - } - - private KateStewartTriggeredAbility(final KateStewartTriggeredAbility ability) { - super(ability); - } - - @Override - public KateStewartTriggeredAbility copy() { - return new KateStewartTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.COUNTERS_ADDED; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - return CounterType.TIME.getName().equals(event.getData()) - && this.isControlledBy(event.getPlayerId()) - && this.isControlledBy(game.getControllerId(event.getTargetId())); - } -} - enum KateStewartValue implements DynamicValue { instance; private static final Hint hint = new ValueHint("Time counters among permanents you control", instance); diff --git a/Mage.Sets/src/mage/cards/k/KeeperOfTheBeasts.java b/Mage.Sets/src/mage/cards/k/KeeperOfTheBeasts.java index 8900516e43b..697e9c531c6 100644 --- a/Mage.Sets/src/mage/cards/k/KeeperOfTheBeasts.java +++ b/Mage.Sets/src/mage/cards/k/KeeperOfTheBeasts.java @@ -1,7 +1,6 @@ package mage.cards.k; import mage.MageInt; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; @@ -15,10 +14,8 @@ import mage.filter.FilterOpponent; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.token.BeastToken4; -import mage.players.Player; import mage.target.TargetPlayer; -import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -35,7 +32,7 @@ public final class KeeperOfTheBeasts extends CardImpl { this.power = new MageInt(1); this.toughness = new MageInt(2); - // {G}, {tap}: Choose target opponent who controlled more creatures than you did as you activated this ability. Put a 2/2 green Beast creature token onto the battlefield. + // {G}, {T}: Choose target opponent who controlled more creatures than you did as you activated this ability. Put a 2/2 green Beast creature token onto the battlefield. Ability ability = new SimpleActivatedAbility(new CreateTokenEffect(new BeastToken4()).setText("Choose target opponent who controlled more creatures than you did as you activated this ability. Create a 2/2 green Beast creature token."), new ManaCostsImpl<>("{G}")); ability.addCost(new TapSourceCost()); @@ -65,42 +62,12 @@ class KeeperOfTheBeastsTarget extends TargetPlayer { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set availablePossibleTargets = super.possibleTargets(sourceControllerId, source, game); - Set possibleTargets = new HashSet<>(); - int creaturesController = game.getBattlefield().countAll(StaticFilters.FILTER_PERMANENT_CREATURE, sourceControllerId, game); - - for (UUID targetId : availablePossibleTargets) { - if (game.getBattlefield().countAll(StaticFilters.FILTER_PERMANENT_CREATURE, targetId, game) > creaturesController) { - possibleTargets.add(targetId); - } - } + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); + int myCount = game.getBattlefield().countAll(StaticFilters.FILTER_PERMANENT_CREATURE, sourceControllerId, game); + possibleTargets.removeIf(playerId -> game.getBattlefield().countAll(StaticFilters.FILTER_PERMANENT_CREATURE, playerId, game) < myCount); return possibleTargets; } - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int count = 0; - MageObject targetSource = game.getObject(source); - Player controller = game.getPlayer(sourceControllerId); - if (controller != null && targetSource != null) { - for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - Player player = game.getPlayer(playerId); - if (player != null - && game.getBattlefield().countAll(StaticFilters.FILTER_PERMANENT_CREATURE, sourceControllerId, game) - < game.getBattlefield().countAll(StaticFilters.FILTER_PERMANENT_CREATURE, playerId, game) - && !player.hasLeft() - && filter.match(player, sourceControllerId, source, game) - && player.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - } - return false; - } - @Override public KeeperOfTheBeastsTarget copy() { return new KeeperOfTheBeastsTarget(this); diff --git a/Mage.Sets/src/mage/cards/k/KeeperOfTheDead.java b/Mage.Sets/src/mage/cards/k/KeeperOfTheDead.java index 85b5284fff2..45f0f0f72c7 100644 --- a/Mage.Sets/src/mage/cards/k/KeeperOfTheDead.java +++ b/Mage.Sets/src/mage/cards/k/KeeperOfTheDead.java @@ -1,10 +1,7 @@ package mage.cards.k; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; import mage.MageInt; -import mage.MageObject; +import mage.ObjectColor; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; @@ -15,21 +12,23 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; -import mage.constants.Zone; import mage.filter.FilterPlayer; import mage.filter.StaticFilters; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.ColorPredicate; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.game.stack.StackObject; import mage.players.Player; import mage.target.TargetPermanent; import mage.target.TargetPlayer; +import java.util.Set; +import java.util.UUID; + /** - * * @author spjspj */ public final class KeeperOfTheDead extends CardImpl { @@ -95,48 +94,37 @@ class KeeperOfDeadPredicate implements ObjectSourcePlayerPredicate { class KeeperOfTheDeadCreatureTarget extends TargetPermanent { + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("nonblack creature that player controls"); + + static { + filter.add(Predicates.not(new ColorPredicate(ObjectColor.BLACK))); + } + public KeeperOfTheDeadCreatureTarget() { - super(1, 1, new FilterCreaturePermanent("nonblack creature that player controls"), false); + super(1, 1, filter); } private KeeperOfTheDeadCreatureTarget(final KeeperOfTheDeadCreatureTarget target) { super(target); } - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - UUID firstTarget = source.getFirstTarget(); - Permanent permanent = game.getPermanent(id); - if (firstTarget != null && permanent != null && permanent.isControlledBy(firstTarget)) { - return super.canTarget(id, source, game); - } - return false; - } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set availablePossibleTargets = super.possibleTargets(sourceControllerId, source, game); - Set possibleTargets = new HashSet<>(); - MageObject object = game.getObject(source); + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - for (StackObject item : game.getState().getStack()) { - if (item.getId().equals(source.getSourceId())) { - object = item; - } - if (item.getSourceId().equals(source.getSourceId())) { - object = item; - } + Player needPlayer = game.getPlayerOrPlaneswalkerController(source.getFirstTarget()); + if (needPlayer == null) { + // playable or not selected - use any + } else { + // filter by controller + possibleTargets.removeIf(id -> { + Permanent permanent = game.getPermanent(id); + return permanent == null + || permanent.getId().equals(source.getFirstTarget()) + || !permanent.isControlledBy(needPlayer.getId()); + }); } - if (object instanceof StackObject) { - UUID playerId = ((StackObject) object).getStackAbility().getFirstTarget(); - for (UUID targetId : availablePossibleTargets) { - Permanent permanent = game.getPermanent(targetId); - if (permanent != null && StaticFilters.FILTER_PERMANENT_CREATURE_NON_BLACK.match(permanent, game) && permanent.isControlledBy(playerId)) { - possibleTargets.add(targetId); - } - } - } return possibleTargets; } diff --git a/Mage.Sets/src/mage/cards/k/KeeperOfTheLight.java b/Mage.Sets/src/mage/cards/k/KeeperOfTheLight.java index 6e24e7d16b2..b4a54b45b4b 100644 --- a/Mage.Sets/src/mage/cards/k/KeeperOfTheLight.java +++ b/Mage.Sets/src/mage/cards/k/KeeperOfTheLight.java @@ -1,8 +1,6 @@ - package mage.cards.k; import mage.MageInt; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; @@ -12,13 +10,11 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; import mage.filter.FilterOpponent; import mage.game.Game; import mage.players.Player; import mage.target.TargetPlayer; -import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -67,43 +63,19 @@ class KeeperOfTheLightTarget extends TargetPlayer { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set availablePossibleTargets = super.possibleTargets(sourceControllerId, source, game); - Set possibleTargets = new HashSet<>(); - int lifeController = game.getPlayer(sourceControllerId).getLife(); + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - for (UUID targetId : availablePossibleTargets) { - Player opponent = game.getPlayer(targetId); - if (opponent != null) { - int lifeOpponent = opponent.getLife(); - if (lifeOpponent > lifeController) { - possibleTargets.add(targetId); - } - } - } - return possibleTargets; - } - - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int count = 0; - MageObject targetSource = game.getObject(source); Player controller = game.getPlayer(sourceControllerId); - if (controller != null && targetSource != null) { - for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - Player player = game.getPlayer(playerId); - if (player != null - && controller.getLife() < player.getLife() - && !player.hasLeft() - && filter.match(player, sourceControllerId, source, game) - && player.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } + if (controller == null) { + return possibleTargets; } - return false; + + possibleTargets.removeIf(playerId -> { + Player player = game.getPlayer(playerId); + return player == null || player.getLife() >= controller.getLife(); + }); + + return possibleTargets; } @Override diff --git a/Mage.Sets/src/mage/cards/k/KeldonBattlewagon.java b/Mage.Sets/src/mage/cards/k/KeldonBattlewagon.java index 1543346e478..6283587baa9 100644 --- a/Mage.Sets/src/mage/cards/k/KeldonBattlewagon.java +++ b/Mage.Sets/src/mage/cards/k/KeldonBattlewagon.java @@ -105,7 +105,7 @@ class KeldonBattlewagonCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return target.canChoose(controllerId, source, game); + return target.canChooseOrAlreadyChosen(controllerId, source, game); } @Override diff --git a/Mage.Sets/src/mage/cards/k/KondasBanner.java b/Mage.Sets/src/mage/cards/k/KondasBanner.java index e706ee087a0..72005df704a 100644 --- a/Mage.Sets/src/mage/cards/k/KondasBanner.java +++ b/Mage.Sets/src/mage/cards/k/KondasBanner.java @@ -7,15 +7,14 @@ import mage.abilities.effects.common.continuous.BoostAllEffect; import mage.abilities.keyword.EquipAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.SubType; -import mage.constants.SuperType; +import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.StaticFilters; +import mage.filter.common.FilterCreatureCard; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; import mage.game.permanent.Permanent; +import mage.target.TargetCard; import mage.target.TargetPermanent; import java.util.UUID; diff --git a/Mage.Sets/src/mage/cards/k/KotoseTheSilentSpider.java b/Mage.Sets/src/mage/cards/k/KotoseTheSilentSpider.java index 4e6aa44c4b6..d478459ff8c 100644 --- a/Mage.Sets/src/mage/cards/k/KotoseTheSilentSpider.java +++ b/Mage.Sets/src/mage/cards/k/KotoseTheSilentSpider.java @@ -15,6 +15,7 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.game.stack.Spell; import mage.players.Player; +import mage.target.TargetCard; import mage.target.common.TargetCardInGraveyard; import mage.target.common.TargetCardInHand; import mage.target.common.TargetCardInLibrary; @@ -107,9 +108,9 @@ class KotoseTheSilentSpiderEffect extends OneShotEffect { cards.addAll(targetCardInGraveyard.getTargets()); filter.setMessage("cards named " + card.getName() + " from " + opponent.getName() + "'s hand"); - TargetCardInHand targetCardInHand = new TargetCardInHand(0, Integer.MAX_VALUE, filter); - controller.choose(outcome, opponent.getHand(), targetCardInHand, source, game); - cards.addAll(targetCardInHand.getTargets()); + TargetCard targetCard = new TargetCard(0, Integer.MAX_VALUE, Zone.HAND, filter); + controller.choose(outcome, opponent.getHand(), targetCard, source, game); + cards.addAll(targetCard.getTargets()); filter.setMessage("cards named " + card.getName() + " from " + opponent.getName() + "'s library"); TargetCardInLibrary target = new TargetCardInLibrary(0, Integer.MAX_VALUE, filter); diff --git a/Mage.Sets/src/mage/cards/k/KravensLastHunt.java b/Mage.Sets/src/mage/cards/k/KravensLastHunt.java new file mode 100644 index 00000000000..50efa741010 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KravensLastHunt.java @@ -0,0 +1,144 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.SagaAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SagaChapter; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInYourGraveyard; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class KravensLastHunt extends CardImpl { + + public KravensLastHunt(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{G}"); + + this.subtype.add(SubType.SAGA); + + // (As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.) + SagaAbility sagaAbility = new SagaAbility(this); + + // I -- Mill five cards. When you do, this Saga deals damage equal to the greatest power among creature cards in your graveyard to target creature. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_I, + new MillCardsControllerEffect(4), new KravensLastHuntEffect() + ); + + // II -- Target creature you control gets +2/+2 until end of turn. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_II, + new BoostTargetEffect(2, 2), + new TargetControlledCreaturePermanent() + ); + + // III -- Return target creature card from your graveyard to your hand. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_III, new ReturnFromGraveyardToHandTargetEffect(), + new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD) + ); + this.addAbility(sagaAbility.addHint(KravensLastHuntValue.getHint())); + } + + private KravensLastHunt(final KravensLastHunt card) { + super(card); + } + + @Override + public KravensLastHunt copy() { + return new KravensLastHunt(this); + } +} + +enum KravensLastHuntValue implements DynamicValue { + instance; + private static final Hint hint = new ValueHint("Greatest power among creature cards in your graveyard", instance); + + public static Hint getHint() { + return hint; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + Player player = game.getPlayer(sourceAbility.getControllerId()); + if (player == null) { + return 0; + } + return player + .getGraveyard() + .getCards(StaticFilters.FILTER_CARD_CREATURE, game) + .stream() + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .max() + .orElse(0); + } + + @Override + public KravensLastHuntValue copy() { + return this; + } + + @Override + public String getMessage() { + return ""; + } + + @Override + public String toString() { + return "1"; + } +} + +class KravensLastHuntEffect extends OneShotEffect { + + + KravensLastHuntEffect() { + super(Outcome.Benefit); + staticText = "When you do, {this} deals damage equal to the greatest power among creature cards in your graveyard to target creature"; + } + + private KravensLastHuntEffect(final KravensLastHuntEffect effect) { + super(effect); + } + + @Override + public KravensLastHuntEffect copy() { + return new KravensLastHuntEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility( + new DamageTargetEffect(KravensLastHuntValue.instance) + .setText("{this} deals damage equal to the greatest power " + + "among creature cards in your graveyard to target creature"), false + ); + ability.addTarget(new TargetCreaturePermanent()); + game.fireReflexiveTriggeredAbility(ability, source); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/k/KrosDefenseContractor.java b/Mage.Sets/src/mage/cards/k/KrosDefenseContractor.java index 400c90449e9..7ca5c749599 100644 --- a/Mage.Sets/src/mage/cards/k/KrosDefenseContractor.java +++ b/Mage.Sets/src/mage/cards/k/KrosDefenseContractor.java @@ -3,7 +3,7 @@ package mage.cards.k; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; -import mage.abilities.common.PutCounterOnCreatureTriggeredAbility; +import mage.abilities.common.PutCounterOnPermanentTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.combat.GoadTargetEffect; import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; @@ -42,7 +42,8 @@ public final class KrosDefenseContractor extends CardImpl { this.addAbility(ability); // Whenever you put one or more counters on a creature you don't control, tap that creature and goad it. It gains trample until your next turn. - this.addAbility(new PutCounterOnCreatureTriggeredAbility(new KrosDefenseContractorEffect(), null, StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL, true)); + this.addAbility(new PutCounterOnPermanentTriggeredAbility(new KrosDefenseContractorEffect(), + null, StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL, true, false)); } private KrosDefenseContractor(final KrosDefenseContractor card) { diff --git a/Mage.Sets/src/mage/cards/l/LagrellaTheMagpie.java b/Mage.Sets/src/mage/cards/l/LagrellaTheMagpie.java index 12fae5b95ff..50cec499a1a 100644 --- a/Mage.Sets/src/mage/cards/l/LagrellaTheMagpie.java +++ b/Mage.Sets/src/mage/cards/l/LagrellaTheMagpie.java @@ -130,8 +130,8 @@ class LagrellaTheMagpieTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } Permanent creature = game.getPermanent(id); diff --git a/Mage.Sets/src/mage/cards/l/LethalScheme.java b/Mage.Sets/src/mage/cards/l/LethalScheme.java index 03c07511b81..15f1e57ce00 100644 --- a/Mage.Sets/src/mage/cards/l/LethalScheme.java +++ b/Mage.Sets/src/mage/cards/l/LethalScheme.java @@ -69,7 +69,7 @@ class LethalSchemeEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { HashSet convokingCreatures = CardUtil.getSourceCostsTag(game, source, - ConvokeAbility.convokingCreaturesKey, new HashSet<>(0)); + ConvokeAbility.convokingCreaturesKey, new HashSet<>()); Set> playerPermanentsPairs = convokingCreatures .stream() diff --git a/Mage.Sets/src/mage/cards/l/LivelyDirge.java b/Mage.Sets/src/mage/cards/l/LivelyDirge.java index 39579ba8ca9..a9d8b5ddca2 100644 --- a/Mage.Sets/src/mage/cards/l/LivelyDirge.java +++ b/Mage.Sets/src/mage/cards/l/LivelyDirge.java @@ -103,8 +103,8 @@ class LivelyDirgeTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 4, game); } diff --git a/Mage.Sets/src/mage/cards/l/Lobotomy.java b/Mage.Sets/src/mage/cards/l/Lobotomy.java index 73cb29ae2c2..040971b51f9 100644 --- a/Mage.Sets/src/mage/cards/l/Lobotomy.java +++ b/Mage.Sets/src/mage/cards/l/Lobotomy.java @@ -74,7 +74,7 @@ class LobotomyEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExile TargetCard target = new TargetCard(Zone.HAND, filter); target.withNotTarget(true); Card chosenCard = null; - if (controller.chooseTarget(Outcome.Benefit, targetPlayer.getHand(), target, source, game)) { + if (controller.choose(Outcome.Benefit, targetPlayer.getHand(), target, source, game)) { chosenCard = game.getCard(target.getFirstTarget()); } diff --git a/Mage.Sets/src/mage/cards/l/LongRest.java b/Mage.Sets/src/mage/cards/l/LongRest.java index 04e84e63d25..68532a86f3b 100644 --- a/Mage.Sets/src/mage/cards/l/LongRest.java +++ b/Mage.Sets/src/mage/cards/l/LongRest.java @@ -21,7 +21,6 @@ import java.util.Set; import java.util.UUID; /** - * * @author weirddan455 */ public final class LongRest extends CardImpl { @@ -67,19 +66,22 @@ class LongRestTarget extends TargetCardInYourGraveyard { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); - Set manaValues = new HashSet<>(); + + Set usedManaValues = new HashSet<>(); for (UUID targetId : this.getTargets()) { Card card = game.getCard(targetId); if (card != null) { - manaValues.add(card.getManaValue()); + usedManaValues.add(card.getManaValue()); } } + for (UUID possibleTargetId : super.possibleTargets(sourceControllerId, source, game)) { Card card = game.getCard(possibleTargetId); - if (card != null && !manaValues.contains(card.getManaValue())) { + if (card != null && !usedManaValues.contains(card.getManaValue())) { possibleTargets.add(possibleTargetId); } } + return possibleTargets; } } diff --git a/Mage.Sets/src/mage/cards/m/MarchFromTheTomb.java b/Mage.Sets/src/mage/cards/m/MarchFromTheTomb.java index 47f751c7aae..73e256f652f 100644 --- a/Mage.Sets/src/mage/cards/m/MarchFromTheTomb.java +++ b/Mage.Sets/src/mage/cards/m/MarchFromTheTomb.java @@ -56,8 +56,8 @@ class MarchFromTheTombTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 8, game); } diff --git a/Mage.Sets/src/mage/cards/m/ModifyMemory.java b/Mage.Sets/src/mage/cards/m/ModifyMemory.java index b41118ba499..9c4d25397a9 100644 --- a/Mage.Sets/src/mage/cards/m/ModifyMemory.java +++ b/Mage.Sets/src/mage/cards/m/ModifyMemory.java @@ -86,8 +86,8 @@ class ModifyMemoryTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } Permanent creature = game.getPermanent(id); diff --git a/Mage.Sets/src/mage/cards/m/MoorlandRescuer.java b/Mage.Sets/src/mage/cards/m/MoorlandRescuer.java index a619369c7e8..45bcd1a28fd 100644 --- a/Mage.Sets/src/mage/cards/m/MoorlandRescuer.java +++ b/Mage.Sets/src/mage/cards/m/MoorlandRescuer.java @@ -112,8 +112,8 @@ class MoorlandRescuerTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, m -> m.getPower().getValue(), xValue, game); } diff --git a/Mage.Sets/src/mage/cards/m/Mutiny.java b/Mage.Sets/src/mage/cards/m/Mutiny.java index 40caae27b00..50ecaac9d64 100644 --- a/Mage.Sets/src/mage/cards/m/Mutiny.java +++ b/Mage.Sets/src/mage/cards/m/Mutiny.java @@ -1,6 +1,5 @@ package mage.cards.m; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.cards.CardImpl; @@ -9,18 +8,16 @@ import mage.constants.CardType; import mage.constants.Outcome; import mage.filter.StaticFilters; import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.Predicates; -import mage.filter.predicate.permanent.ControllerIdPredicate; -import mage.filter.predicate.permanent.PermanentIdPredicate; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.players.Player; import mage.target.TargetPermanent; -import mage.target.common.TargetCreaturePermanent; +import java.util.Set; import java.util.UUID; /** + * TODO: combine with BreakingOfTheFellowship + * * @author LevelX2 */ public final class Mutiny extends CardImpl { @@ -30,7 +27,8 @@ public final class Mutiny extends CardImpl { // Target creature an opponent controls deals damage equal to its power to another target creature that player controls. this.getSpellAbility().addEffect(new MutinyEffect()); - this.getSpellAbility().addTarget(new MutinyFirstTarget(StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE)); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE)); + this.getSpellAbility().addTarget(new MutinySecondTarget()); this.getSpellAbility().addTarget(new TargetPermanent(new FilterCreaturePermanent("another target creature that player controls"))); } @@ -72,74 +70,45 @@ class MutinyEffect extends OneShotEffect { } return true; } - } -class MutinyFirstTarget extends TargetPermanent { +class MutinySecondTarget extends TargetPermanent { - public MutinyFirstTarget(FilterCreaturePermanent filter) { - super(1, 1, filter, false); + public MutinySecondTarget() { + super(StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE); } - private MutinyFirstTarget(final MutinyFirstTarget target) { + private MutinySecondTarget(final MutinySecondTarget target) { super(target); } @Override - public void addTarget(UUID id, Ability source, Game game, boolean skipEvent) { - super.addTarget(id, source, game, skipEvent); - // Update the second target - UUID firstController = game.getControllerId(id); - if (firstController != null && source.getTargets().size() > 1) { - Player controllingPlayer = game.getPlayer(firstController); - TargetCreaturePermanent targetCreaturePermanent = (TargetCreaturePermanent) source.getTargets().get(1); - // Set a new filter to the second target with the needed restrictions - FilterCreaturePermanent filter = new FilterCreaturePermanent("another creature that player " + controllingPlayer.getName() + " controls"); - filter.add(new ControllerIdPredicate(firstController)); - filter.add(Predicates.not(new PermanentIdPredicate(id))); - targetCreaturePermanent.replaceFilter(filter); - } - } + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (super.canTarget(controllerId, id, source, game)) { - // can only target, if the controller has at least two targetable creatures - UUID controllingPlayerId = game.getControllerId(id); - int possibleTargets = 0; - MageObject sourceObject = game.getObject(source.getId()); - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, controllingPlayerId, game)) { - if (permanent.canBeTargetedBy(sourceObject, controllerId, source, game)) { - possibleTargets++; - } + Permanent firstTarget = game.getPermanent(source.getFirstTarget()); + if (firstTarget == null) { + // playable or first target not yet selected + // use all + if (possibleTargets.size() == 1) { + // workaround to make 1 target invalid + possibleTargets.clear(); } - return possibleTargets > 1; + } else { + // real + // filter by same player + possibleTargets.removeIf(id -> { + Permanent permanent = game.getPermanent(id); + return permanent == null || !permanent.isControlledBy(firstTarget.getControllerId()); + }); } - return false; + possibleTargets.removeIf(id -> firstTarget != null && firstTarget.getId().equals(id)); + + return possibleTargets; } @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - if (super.canChoose(sourceControllerId, source, game)) { - UUID controllingPlayerId = game.getControllerId(source.getSourceId()); - for (UUID playerId : game.getOpponents(controllingPlayerId)) { - int possibleTargets = 0; - MageObject sourceObject = game.getObject(source); - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, playerId, game)) { - if (permanent.canBeTargetedBy(sourceObject, controllingPlayerId, source, game)) { - possibleTargets++; - } - } - if (possibleTargets > 1) { - return true; - } - } - } - return false; - } - - @Override - public MutinyFirstTarget copy() { - return new MutinyFirstTarget(this); + public MutinySecondTarget copy() { + return new MutinySecondTarget(this); } } diff --git a/Mage.Sets/src/mage/cards/n/NahiriForgedInFury.java b/Mage.Sets/src/mage/cards/n/NahiriForgedInFury.java index 291781b2532..4f578491bc0 100644 --- a/Mage.Sets/src/mage/cards/n/NahiriForgedInFury.java +++ b/Mage.Sets/src/mage/cards/n/NahiriForgedInFury.java @@ -11,6 +11,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.permanent.EquippedPredicate; import mage.game.Game; import mage.players.Player; @@ -24,6 +25,10 @@ public final class NahiriForgedInFury extends CardImpl { private static final FilterControlledCreaturePermanent equippedFilter = new FilterControlledCreaturePermanent( "an equipped creature you control"); + static { + equippedFilter.add(EquippedPredicate.instance); + } + public NahiriForgedInFury(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{R}{W}"); diff --git a/Mage.Sets/src/mage/cards/n/NestOfScarabs.java b/Mage.Sets/src/mage/cards/n/NestOfScarabs.java index d4545e85df7..80971943f4f 100644 --- a/Mage.Sets/src/mage/cards/n/NestOfScarabs.java +++ b/Mage.Sets/src/mage/cards/n/NestOfScarabs.java @@ -1,12 +1,13 @@ package mage.cards.n; -import mage.abilities.common.PutCounterOnCreatureTriggeredAbility; +import mage.abilities.common.PutCounterOnPermanentTriggeredAbility; import mage.abilities.dynamicvalue.common.EffectKeyValue; import mage.abilities.effects.common.CreateTokenEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.counters.CounterType; +import mage.filter.StaticFilters; import mage.game.permanent.token.NestOfScarabsBlackInsectToken; import java.util.UUID; @@ -20,10 +21,10 @@ public final class NestOfScarabs extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{B}"); // Whenever you put one or more -1/-1 counters on a creature, create that many 1/1 black Insect creature tokens. - this.addAbility(new PutCounterOnCreatureTriggeredAbility( + this.addAbility(new PutCounterOnPermanentTriggeredAbility( new CreateTokenEffect(new NestOfScarabsBlackInsectToken(), new EffectKeyValue("countersAdded", "that many")), - CounterType.M1M1.createInstance())); + CounterType.M1M1, StaticFilters.FILTER_PERMANENT_CREATURE)); } private NestOfScarabs(final NestOfScarabs card) { diff --git a/Mage.Sets/src/mage/cards/n/Nethergoyf.java b/Mage.Sets/src/mage/cards/n/Nethergoyf.java index 858e2718695..955bd409171 100644 --- a/Mage.Sets/src/mage/cards/n/Nethergoyf.java +++ b/Mage.Sets/src/mage/cards/n/Nethergoyf.java @@ -22,10 +22,7 @@ import mage.target.common.TargetCardInYourGraveyard; import mage.util.CardUtil; import java.awt.*; -import java.util.Collection; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; /** @@ -113,7 +110,10 @@ class NethergoyfTarget extends TargetCardInYourGraveyard { return false; } // Check that exiling all the possible cards would have >= 4 different card types - return metCondition(this.possibleTargets(sourceControllerId, source, game), game); + Set idsToCheck = new HashSet<>(); + idsToCheck.addAll(this.getTargets()); + idsToCheck.addAll(this.possibleTargets(sourceControllerId, source, game)); + return metCondition(idsToCheck, game); } private static Set typesAmongSelection(Collection cardsIds, Game game) { diff --git a/Mage.Sets/src/mage/cards/n/NethroiApexOfDeath.java b/Mage.Sets/src/mage/cards/n/NethroiApexOfDeath.java index 0f1746723d8..f55584b1dc5 100644 --- a/Mage.Sets/src/mage/cards/n/NethroiApexOfDeath.java +++ b/Mage.Sets/src/mage/cards/n/NethroiApexOfDeath.java @@ -83,8 +83,8 @@ class NethroiApexOfDeathTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, m -> m.getPower().getValue(), 10, game); } diff --git a/Mage.Sets/src/mage/cards/n/NicolBolasDragonGod.java b/Mage.Sets/src/mage/cards/n/NicolBolasDragonGod.java index b14942908a4..321e6980297 100644 --- a/Mage.Sets/src/mage/cards/n/NicolBolasDragonGod.java +++ b/Mage.Sets/src/mage/cards/n/NicolBolasDragonGod.java @@ -150,7 +150,7 @@ class NicolBolasDragonGodPlusOneEffect extends OneShotEffect { TargetPermanent targetPermanent = new TargetControlledPermanent(); targetPermanent.withNotTarget(true); targetPermanent.setTargetController(opponentId); - if (!targetPermanent.possibleTargets(opponentId, game).isEmpty()) { + if (!targetPermanent.possibleTargets(opponentId, source, game).isEmpty()) { possibleTargetTypes.add(targetPermanent); } diff --git a/Mage.Sets/src/mage/cards/n/NivmagusElemental.java b/Mage.Sets/src/mage/cards/n/NivmagusElemental.java index a602a4adec4..f94d741e0d0 100644 --- a/Mage.Sets/src/mage/cards/n/NivmagusElemental.java +++ b/Mage.Sets/src/mage/cards/n/NivmagusElemental.java @@ -91,7 +91,7 @@ class NivmagusElementalCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage.Sets/src/mage/cards/n/NoContest.java b/Mage.Sets/src/mage/cards/n/NoContest.java index 3c0b2732f17..6d25a4de9db 100644 --- a/Mage.Sets/src/mage/cards/n/NoContest.java +++ b/Mage.Sets/src/mage/cards/n/NoContest.java @@ -1,5 +1,6 @@ package mage.cards.n; +import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.effects.common.FightTargetsEffect; import mage.cards.Card; @@ -15,7 +16,9 @@ import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.target.TargetPermanent; import mage.target.common.TargetControlledCreaturePermanent; +import mage.watchers.common.BlockedAttackerWatcher; +import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -54,50 +57,27 @@ class TargetCreatureWithLessPowerPermanent extends TargetPermanent { super(target); } - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int maxPower = Integer.MIN_VALUE; // get the most powerful controlled creature that can be targeted - Card sourceCard = game.getCard(source.getSourceId()); - if (sourceCard == null) { - return false; - } - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES, sourceControllerId, game)) { - if (permanent.getPower().getValue() > maxPower && permanent.canBeTargetedBy(sourceCard, sourceControllerId, source, game)) { - maxPower = permanent.getPower().getValue(); - } - } - // now check, if another creature has less power and can be targeted - FilterCreaturePermanent checkFilter = new FilterCreaturePermanent(); - checkFilter.add(new PowerPredicate(ComparisonType.FEWER_THAN, maxPower)); - for (Permanent permanent : game.getBattlefield().getActivePermanents(checkFilter, sourceControllerId, source, game)) { - if (permanent.canBeTargetedBy(sourceCard, sourceControllerId, source, game)) { - return true; - } - } - return false; - } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Spell spell = game.getStack().getSpell(source.getSourceId()); - if (spell != null) { - Permanent firstTarget = getPermanentFromFirstTarget(spell.getSpellAbility(), game); - if (firstTarget != null) { - int power = firstTarget.getPower().getValue(); - // overwrite the filter with the power predicate - filter = new FilterCreaturePermanent("creature with power less than " + power); - filter.add(new PowerPredicate(ComparisonType.FEWER_THAN, power)); + Set possibleTargets = new HashSet<>(); + + Permanent firstPermanent = game.getPermanent(source.getFirstTarget()); + for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { + if (firstPermanent == null) { + // playable or first target not yet selected + // use all + possibleTargets.add(permanent.getId()); + } else { + // real + // filter by power + if (firstPermanent.getPower().getValue() > permanent.getPower().getValue()) { + possibleTargets.add(permanent.getId()); + } } } - return super.possibleTargets(sourceControllerId, source, game); - } + possibleTargets.removeIf(id -> firstPermanent != null && firstPermanent.getId().equals(id)); - private Permanent getPermanentFromFirstTarget(Ability source, Game game) { - Permanent firstTarget = null; - if (source.getTargets().size() == 2) { - firstTarget = game.getPermanent(source.getTargets().get(0).getFirstTarget()); - } - return firstTarget; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage.Sets/src/mage/cards/o/OKagachiVengefulKami.java b/Mage.Sets/src/mage/cards/o/OKagachiVengefulKami.java index 8fb3f30b2c9..ebfe51e41a6 100644 --- a/Mage.Sets/src/mage/cards/o/OKagachiVengefulKami.java +++ b/Mage.Sets/src/mage/cards/o/OKagachiVengefulKami.java @@ -52,7 +52,8 @@ public final class OKagachiVengefulKami extends CardImpl { ability.addTarget(new TargetPermanent(filter)); ability.setTargetAdjuster(new ThatPlayerControlsTargetAdjuster()); ability.withInterveningIf(KagachiVengefulKamiCondition.instance); - this.addAbility(ability); + + this.addAbility(ability, new OKagachiVengefulKamiWatcher()); } private OKagachiVengefulKami(final OKagachiVengefulKami card) { diff --git a/Mage.Sets/src/mage/cards/o/ONaginata.java b/Mage.Sets/src/mage/cards/o/ONaginata.java index 104488f9627..16d3ea98f67 100644 --- a/Mage.Sets/src/mage/cards/o/ONaginata.java +++ b/Mage.Sets/src/mage/cards/o/ONaginata.java @@ -9,12 +9,12 @@ import mage.abilities.keyword.EquipAbility; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.AttachmentType; -import mage.constants.CardType; -import mage.constants.ComparisonType; -import mage.constants.SubType; +import mage.constants.*; import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.common.FilterCreatureCard; +import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.PowerPredicate; +import mage.target.TargetCard; import mage.target.TargetPermanent; import java.util.UUID; @@ -24,7 +24,7 @@ import java.util.UUID; */ public final class ONaginata extends CardImpl { - private static final FilterControlledCreaturePermanent filter = new FilterControlledCreaturePermanent("creature with power 3 or greater"); + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature with power 3 or greater"); static { filter.add(new PowerPredicate(ComparisonType.MORE_THAN, 2)); diff --git a/Mage.Sets/src/mage/cards/o/ObeliskSpider.java b/Mage.Sets/src/mage/cards/o/ObeliskSpider.java index c9982c40480..06fc55739f7 100644 --- a/Mage.Sets/src/mage/cards/o/ObeliskSpider.java +++ b/Mage.Sets/src/mage/cards/o/ObeliskSpider.java @@ -1,11 +1,9 @@ package mage.cards.o; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.DealsDamageToACreatureTriggeredAbility; -import mage.abilities.common.PutCounterOnCreatureTriggeredAbility; -import mage.abilities.effects.Effect; +import mage.abilities.common.PutCounterOnPermanentTriggeredAbility; import mage.abilities.effects.common.GainLifeEffect; import mage.abilities.effects.common.LoseLifeOpponentsEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; @@ -15,6 +13,9 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.counters.CounterType; +import mage.filter.StaticFilters; + +import java.util.UUID; /** * @@ -36,10 +37,9 @@ public final class ObeliskSpider extends CardImpl { this.addAbility(new DealsDamageToACreatureTriggeredAbility(new AddCountersTargetEffect(CounterType.M1M1.createInstance(1)), true, false, true)); // Whenever you put one or more -1/-1 counters on a creature, each opponent loses 1 life and you gain 1 life. - Ability ability = new PutCounterOnCreatureTriggeredAbility(new LoseLifeOpponentsEffect(1), CounterType.M1M1.createInstance()); - Effect effect = new GainLifeEffect(1); - effect.setText("and you gain 1 life"); - ability.addEffect(effect); + Ability ability = new PutCounterOnPermanentTriggeredAbility(new LoseLifeOpponentsEffect(1), + CounterType.M1M1, StaticFilters.FILTER_PERMANENT_CREATURE); + ability.addEffect(new GainLifeEffect(1).concatBy("and")); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/o/OildeepGearhulk.java b/Mage.Sets/src/mage/cards/o/OildeepGearhulk.java index ad63fcbc14e..72e646c0ab5 100644 --- a/Mage.Sets/src/mage/cards/o/OildeepGearhulk.java +++ b/Mage.Sets/src/mage/cards/o/OildeepGearhulk.java @@ -13,6 +13,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; +import mage.constants.Zone; import mage.filter.FilterCard; import mage.game.Game; import mage.players.Player; @@ -81,7 +82,7 @@ class OildeepGearhulkEffect extends OneShotEffect { } controller.lookAtCards(targetPlayer.getName() + " Hand", targetPlayer.getHand(), game); - TargetCard chosenCard = new TargetCardInHand(0, 1, new FilterCard("card to discard")); + TargetCard chosenCard = new TargetCard(0, 1, Zone.HAND, new FilterCard("card to discard")); if (!controller.choose(Outcome.Discard, targetPlayer.getHand(), chosenCard, source, game)) { return false; } diff --git a/Mage.Sets/src/mage/cards/o/OmarthisGhostfireInitiate.java b/Mage.Sets/src/mage/cards/o/OmarthisGhostfireInitiate.java index 7dff16ac6f9..f9bb0201478 100644 --- a/Mage.Sets/src/mage/cards/o/OmarthisGhostfireInitiate.java +++ b/Mage.Sets/src/mage/cards/o/OmarthisGhostfireInitiate.java @@ -3,7 +3,7 @@ package mage.cards.o; import mage.MageInt; import mage.abilities.common.DiesSourceTriggeredAbility; import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.common.PutCounterOnCreatureTriggeredAbility; +import mage.abilities.common.PutCounterOnPermanentTriggeredAbility; import mage.abilities.dynamicvalue.common.CountersSourceCount; import mage.abilities.effects.common.EntersBattlefieldWithXCountersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; @@ -49,10 +49,9 @@ public final class OmarthisGhostfireInitiate extends CardImpl { // Whenever you put one or more +1/+1 counters on another colorless creature, you may put a +1/+1 counter on Omarthis. this.addAbility( - new PutCounterOnCreatureTriggeredAbility( + new PutCounterOnPermanentTriggeredAbility( new AddCountersSourceEffect(CounterType.P1P1.createInstance()), - CounterType.P1P1.createInstance(), filter, - false, true + CounterType.P1P1, filter, false, true ) ); diff --git a/Mage.Sets/src/mage/cards/o/OrcusPrinceOfUndeath.java b/Mage.Sets/src/mage/cards/o/OrcusPrinceOfUndeath.java index 9f1c1c8c302..c41b9ec34a1 100644 --- a/Mage.Sets/src/mage/cards/o/OrcusPrinceOfUndeath.java +++ b/Mage.Sets/src/mage/cards/o/OrcusPrinceOfUndeath.java @@ -119,8 +119,8 @@ class OrcusPrinceOfUndeathTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, xValue, game); } diff --git a/Mage.Sets/src/mage/cards/p/PairODiceLost.java b/Mage.Sets/src/mage/cards/p/PairODiceLost.java index b6ff94a6107..dd049052382 100644 --- a/Mage.Sets/src/mage/cards/p/PairODiceLost.java +++ b/Mage.Sets/src/mage/cards/p/PairODiceLost.java @@ -101,8 +101,8 @@ class PairODiceLostTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, xValue, game); } diff --git a/Mage.Sets/src/mage/cards/p/PatchUp.java b/Mage.Sets/src/mage/cards/p/PatchUp.java index 16237f5b2e6..6717fd3b0eb 100644 --- a/Mage.Sets/src/mage/cards/p/PatchUp.java +++ b/Mage.Sets/src/mage/cards/p/PatchUp.java @@ -58,8 +58,8 @@ class PatchUpTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 3, game); } diff --git a/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java b/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java index 2170f980cb3..7407044a59c 100644 --- a/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java +++ b/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java @@ -105,7 +105,7 @@ class PhenomenonInvestigatorsReturnCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage.Sets/src/mage/cards/p/PhoenixWardenOfFire.java b/Mage.Sets/src/mage/cards/p/PhoenixWardenOfFire.java index f95a9a46b15..9a9ff1da417 100644 --- a/Mage.Sets/src/mage/cards/p/PhoenixWardenOfFire.java +++ b/Mage.Sets/src/mage/cards/p/PhoenixWardenOfFire.java @@ -94,8 +94,8 @@ class PhoenixWardenOfFireTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 6, game ); diff --git a/Mage.Sets/src/mage/cards/p/PrimordialMist.java b/Mage.Sets/src/mage/cards/p/PrimordialMist.java index 8960f2f2ef2..50718656957 100644 --- a/Mage.Sets/src/mage/cards/p/PrimordialMist.java +++ b/Mage.Sets/src/mage/cards/p/PrimordialMist.java @@ -78,7 +78,7 @@ class PrimordialMistCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return target.canChoose(controllerId, source, game); + return target.canChooseOrAlreadyChosen(controllerId, source, game); } @Override diff --git a/Mage.Sets/src/mage/cards/p/ProteanHulk.java b/Mage.Sets/src/mage/cards/p/ProteanHulk.java index afde70d42f8..5a0a7d68168 100644 --- a/Mage.Sets/src/mage/cards/p/ProteanHulk.java +++ b/Mage.Sets/src/mage/cards/p/ProteanHulk.java @@ -64,8 +64,8 @@ class ProteanHulkTarget extends TargetCardInLibrary { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 6, game); } diff --git a/Mage.Sets/src/mage/cards/p/ProtectorOfTheWastes.java b/Mage.Sets/src/mage/cards/p/ProtectorOfTheWastes.java index 4ab7cb7b658..90554e75de2 100644 --- a/Mage.Sets/src/mage/cards/p/ProtectorOfTheWastes.java +++ b/Mage.Sets/src/mage/cards/p/ProtectorOfTheWastes.java @@ -1,36 +1,36 @@ package mage.cards.p; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; import mage.MageInt; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.common.ExileTargetEffect; -import mage.abilities.keyword.MonstrosityAbility; -import mage.constants.SubType; import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.MonstrosityAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.SubType; import mage.constants.Zone; import mage.filter.common.FilterArtifactOrEnchantmentPermanent; +import mage.game.Controllable; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + /** - * * @author Jmlundeen */ public final class ProtectorOfTheWastes extends CardImpl { public ProtectorOfTheWastes(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{W}{W}"); - + this.subtype.add(SubType.DRAGON); this.power = new MageInt(5); this.toughness = new MageInt(5); @@ -76,36 +76,19 @@ class ProtectorOfTheWastesTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID id, Ability source, Game game) { - if (!super.canTarget(id, source, game)) { - return false; - } - Permanent permanent = game.getPermanent(id); - if (permanent == null) { - return false; - } - return this.getTargets().stream() + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); + + Set usedControllers = this.getTargets().stream() .map(game::getPermanent) .filter(Objects::nonNull) - .noneMatch(perm -> !perm.getId().equals(permanent.getId()) - && perm.isControlledBy(permanent.getControllerId())); - } + .map(Controllable::getControllerId) + .collect(Collectors.toSet()); + possibleTargets.removeIf(id -> { + Permanent permanent = game.getPermanent(id); + return permanent == null || usedControllers.contains(permanent.getControllerId()); + }); - @Override - public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set possibleTargets = new HashSet<>(); - MageObject targetSource = game.getObject(source); - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - boolean validTarget = this.getTargets().stream() - .map(game::getPermanent) - .filter(Objects::nonNull) - .noneMatch(perm -> !perm.getId().equals(permanent.getId()) && perm.isControlledBy(permanent.getControllerId())); - if (validTarget) { - if (notTarget || permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - possibleTargets.add(permanent.getId()); - } - } - } return possibleTargets; } } diff --git a/Mage.Sets/src/mage/cards/p/PucasMischief.java b/Mage.Sets/src/mage/cards/p/PucasMischief.java index 9b19fbab9b1..7e6c133608b 100644 --- a/Mage.Sets/src/mage/cards/p/PucasMischief.java +++ b/Mage.Sets/src/mage/cards/p/PucasMischief.java @@ -1,21 +1,18 @@ package mage.cards.p; -import mage.MageObject; import mage.abilities.Ability; -import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.common.continuous.ExchangeControlTargetEffect; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; import mage.constants.Outcome; -import mage.constants.TargetController; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; -import mage.target.common.TargetControlledPermanent; import java.util.HashSet; import java.util.Set; @@ -33,7 +30,7 @@ public final class PucasMischief extends CardImpl { // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. Ability ability = new BeginningOfUpkeepTriggeredAbility(new ExchangeControlTargetEffect(Duration.EndOfGame, rule, false, true), true); - ability.addTarget(new TargetControlledPermanentWithCMCGreaterOrLessThanOpponentPermanent()); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_NON_LAND)); ability.addTarget(new PucasMischiefSecondTarget()); this.addAbility(ability); @@ -49,89 +46,53 @@ public final class PucasMischief extends CardImpl { } } -class TargetControlledPermanentWithCMCGreaterOrLessThanOpponentPermanent extends TargetControlledPermanent { - - public TargetControlledPermanentWithCMCGreaterOrLessThanOpponentPermanent() { - super(); - this.filter = this.filter.copy(); - filter.add(Predicates.not(CardType.LAND.getPredicate())); - withTargetName("nonland permanent you control"); - } - - private TargetControlledPermanentWithCMCGreaterOrLessThanOpponentPermanent(final TargetControlledPermanentWithCMCGreaterOrLessThanOpponentPermanent target) { - super(target); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set possibleTargets = new HashSet<>(); - MageObject targetSource = game.getObject(source); - if (targetSource != null) { - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId()) && permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - possibleTargets.add(permanent.getId()); - } - } - } - return possibleTargets; - } - - @Override - public TargetControlledPermanentWithCMCGreaterOrLessThanOpponentPermanent copy() { - return new TargetControlledPermanentWithCMCGreaterOrLessThanOpponentPermanent(this); - } -} - class PucasMischiefSecondTarget extends TargetPermanent { - private Permanent firstTarget = null; - public PucasMischiefSecondTarget() { - super(); - this.filter = this.filter.copy(); - filter.add(TargetController.OPPONENT.getControllerPredicate()); - filter.add(Predicates.not(CardType.LAND.getPredicate())); + super(StaticFilters.FILTER_OPPONENTS_PERMANENT_NON_LAND); withTargetName("permanent an opponent controls with an equal or lesser mana value"); } private PucasMischiefSecondTarget(final PucasMischiefSecondTarget target) { super(target); - this.firstTarget = target.firstTarget; } @Override public boolean canTarget(UUID id, Ability source, Game game) { - if (super.canTarget(id, source, game)) { - Permanent target1 = game.getPermanent(source.getFirstTarget()); - Permanent opponentPermanent = game.getPermanent(id); - if (target1 != null && opponentPermanent != null) { - return target1.getManaValue() >= opponentPermanent.getManaValue(); - } + Permanent ownPermanent = game.getPermanent(source.getFirstTarget()); + Permanent possiblePermanent = game.getPermanent(id); + if (ownPermanent == null || possiblePermanent == null) { + return false; } - return false; + return super.canTarget(id, source, game) && ownPermanent.getManaValue() >= possiblePermanent.getManaValue(); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); - if (firstTarget != null) { - MageObject targetSource = game.getObject(source); - if (targetSource != null) { - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId()) && permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - if (firstTarget.getManaValue() >= permanent.getManaValue()) { - possibleTargets.add(permanent.getId()); - } - } + + Permanent ownPermanent = game.getPermanent(source.getFirstTarget()); + for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { + if (ownPermanent == null) { + // playable or first target not yet selected + // use all + possibleTargets.add(permanent.getId()); + } else { + // real + // filter by cmc + if (ownPermanent.getManaValue() >= permanent.getManaValue()) { + possibleTargets.add(permanent.getId()); } } } - return possibleTargets; + possibleTargets.removeIf(id -> ownPermanent != null && ownPermanent.getId().equals(id)); + + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) { - firstTarget = game.getPermanent(source.getFirstTarget()); + // AI hint with better outcome return super.chooseTarget(Outcome.GainControl, playerId, source, game); } diff --git a/Mage.Sets/src/mage/cards/q/QueenKaylaBinKroog.java b/Mage.Sets/src/mage/cards/q/QueenKaylaBinKroog.java index ae49d30f05b..812ba34c56e 100644 --- a/Mage.Sets/src/mage/cards/q/QueenKaylaBinKroog.java +++ b/Mage.Sets/src/mage/cards/q/QueenKaylaBinKroog.java @@ -109,26 +109,11 @@ class QueenKaylaBinKroogTarget extends TargetCard { return new QueenKaylaBinKroogTarget(this); } - @Override - public boolean canTarget(UUID playerId, UUID id, Ability ability, Game game) { - if (!super.canTarget(playerId, id, ability, game)) { - return false; - } - Card card = game.getCard(id); - return card != null && 1 <= card.getManaValue() && card.getManaValue() <= 3 && this - .getTargets() - .stream() - .map(game::getCard) - .filter(Objects::nonNull) - .mapToInt(MageObject::getManaValue) - .noneMatch(x -> card.getManaValue() == x); - } - - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - Set manaValues = this + + Set usedManaValues = this .getTargets() .stream() .map(game::getCard) @@ -137,8 +122,9 @@ class QueenKaylaBinKroogTarget extends TargetCard { .collect(Collectors.toSet()); possibleTargets.removeIf(uuid -> { Card card = game.getCard(uuid); - return card != null && manaValues.contains(card.getManaValue()); + return card == null || usedManaValues.contains(card.getManaValue()); }); + return possibleTargets; } } diff --git a/Mage.Sets/src/mage/cards/r/RainOfGore.java b/Mage.Sets/src/mage/cards/r/RainOfGore.java index dc769b3a515..32c46e62b10 100644 --- a/Mage.Sets/src/mage/cards/r/RainOfGore.java +++ b/Mage.Sets/src/mage/cards/r/RainOfGore.java @@ -1,7 +1,5 @@ - package mage.cards.r; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.ReplacementEffectImpl; @@ -10,20 +8,20 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; import mage.constants.Outcome; -import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.stack.StackObject; import mage.players.Player; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class RainOfGore extends CardImpl { public RainOfGore(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{B}{R}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}{R}"); // If a spell or ability would cause its controller to gain life, that player loses that much life instead. @@ -71,13 +69,10 @@ class RainOfGoreEffect extends ReplacementEffectImpl { public boolean checksEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.GAIN_LIFE; } - + @Override public boolean applies(GameEvent event, Ability source, Game game) { - if (!game.getStack().isEmpty()) { - StackObject stackObject = game.getStack().getFirst(); - return stackObject.isControlledBy(event.getPlayerId()); - } - return false; + StackObject stackObject = game.getStack().getFirstOrNull(); + return stackObject != null && stackObject.isControlledBy(event.getPlayerId()); } } diff --git a/Mage.Sets/src/mage/cards/r/RaiseTheDraugr.java b/Mage.Sets/src/mage/cards/r/RaiseTheDraugr.java index f53cdc145ba..5ca5445d4bd 100644 --- a/Mage.Sets/src/mage/cards/r/RaiseTheDraugr.java +++ b/Mage.Sets/src/mage/cards/r/RaiseTheDraugr.java @@ -58,8 +58,8 @@ class RaiseTheDraugrTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } if (getTargets().isEmpty()) { diff --git a/Mage.Sets/src/mage/cards/r/RampagingYaoGuai.java b/Mage.Sets/src/mage/cards/r/RampagingYaoGuai.java index 6a1fbc46c72..ab15c13a4ae 100644 --- a/Mage.Sets/src/mage/cards/r/RampagingYaoGuai.java +++ b/Mage.Sets/src/mage/cards/r/RampagingYaoGuai.java @@ -82,8 +82,8 @@ class RampagingYaoGuaiTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, GetXValue.instance.calculate(game, source, null), game); } diff --git a/Mage.Sets/src/mage/cards/r/RasputinDreamweaver.java b/Mage.Sets/src/mage/cards/r/RasputinDreamweaver.java index d499b452b17..34bfd6ab556 100644 --- a/Mage.Sets/src/mage/cards/r/RasputinDreamweaver.java +++ b/Mage.Sets/src/mage/cards/r/RasputinDreamweaver.java @@ -102,7 +102,7 @@ class RasputinDreamweaverWatcher extends Watcher { filter.add(TappedPredicate.UNTAPPED); } - private final Set startedUntapped = new HashSet<>(0); + private final Set startedUntapped = new HashSet<>(); RasputinDreamweaverWatcher() { super(WatcherScope.GAME); diff --git a/Mage.Sets/src/mage/cards/r/RavagerOfTheFells.java b/Mage.Sets/src/mage/cards/r/RavagerOfTheFells.java index f5e141d7811..ed4bd054210 100644 --- a/Mage.Sets/src/mage/cards/r/RavagerOfTheFells.java +++ b/Mage.Sets/src/mage/cards/r/RavagerOfTheFells.java @@ -1,7 +1,6 @@ package mage.cards.r; import mage.MageInt; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.TransformIntoSourceTriggeredAbility; import mage.abilities.common.WerewolfBackTriggeredAbility; @@ -15,13 +14,11 @@ import mage.constants.SubType; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.game.stack.StackObject; import mage.players.Player; import mage.target.TargetPermanent; import mage.target.common.TargetOpponentOrPlaneswalker; import mage.target.targetpointer.EachTargetPointer; -import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -100,54 +97,30 @@ class RavagerOfTheFellsEffect extends OneShotEffect { class RavagerOfTheFellsTarget extends TargetPermanent { RavagerOfTheFellsTarget() { - super(0, 1, StaticFilters.FILTER_PERMANENT_CREATURE, false); + super(0, 1, StaticFilters.FILTER_PERMANENT_CREATURE); } private RavagerOfTheFellsTarget(final RavagerOfTheFellsTarget target) { super(target); } - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - Player player = game.getPlayerOrPlaneswalkerController(source.getFirstTarget()); - if (player == null) { - return false; - } - UUID firstTarget = player.getId(); - Permanent permanent = game.getPermanent(id); - if (firstTarget != null && permanent != null && permanent.isControlledBy(firstTarget)) { - return super.canTarget(id, source, game); - } - return false; - } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set availablePossibleTargets = super.possibleTargets(sourceControllerId, source, game); - Set possibleTargets = new HashSet<>(); - MageObject object = game.getObject(source); + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - for (StackObject item : game.getState().getStack()) { - if (item.getId().equals(source.getSourceId())) { - object = item; - } - if (item.getSourceId().equals(source.getSourceId())) { - object = item; - } + Player needPlayer = game.getPlayerOrPlaneswalkerController(source.getFirstTarget()); + if (needPlayer == null) { + // playable or not selected - use any + } else { + // filter by controller + possibleTargets.removeIf(id -> { + Permanent permanent = game.getPermanent(id); + return permanent == null + || permanent.getId().equals(source.getFirstTarget()) + || !permanent.isControlledBy(needPlayer.getId()); + }); } - if (object instanceof StackObject) { - UUID playerId = ((StackObject) object).getStackAbility().getFirstTarget(); - Player player = game.getPlayerOrPlaneswalkerController(playerId); - if (player != null) { - for (UUID targetId : availablePossibleTargets) { - Permanent permanent = game.getPermanent(targetId); - if (permanent != null && permanent.isControlledBy(player.getId())) { - possibleTargets.add(targetId); - } - } - } - } return possibleTargets; } diff --git a/Mage.Sets/src/mage/cards/r/ReapIntellect.java b/Mage.Sets/src/mage/cards/r/ReapIntellect.java index 1e9f975dbfc..028cca76fd4 100644 --- a/Mage.Sets/src/mage/cards/r/ReapIntellect.java +++ b/Mage.Sets/src/mage/cards/r/ReapIntellect.java @@ -85,7 +85,7 @@ class ReapIntellectEffect extends OneShotEffect { int xCost = Math.min(CardUtil.getSourceCostsTag(game, source, "X", 0), targetPlayer.getHand().size()); TargetCard target = new TargetCard(0, xCost, Zone.HAND, filterNonLands); target.withNotTarget(true); - controller.chooseTarget(Outcome.Benefit, targetPlayer.getHand(), target, source, game); + controller.choose(Outcome.Benefit, targetPlayer.getHand(), target, source, game); for (UUID cardId : target.getTargets()) { Card chosenCard = game.getCard(cardId); if (chosenCard != null) { diff --git a/Mage.Sets/src/mage/cards/r/Reciprocate.java b/Mage.Sets/src/mage/cards/r/Reciprocate.java index 500e2f03c4c..ddb05c1c5ad 100644 --- a/Mage.Sets/src/mage/cards/r/Reciprocate.java +++ b/Mage.Sets/src/mage/cards/r/Reciprocate.java @@ -1,20 +1,15 @@ package mage.cards.r; -import mage.MageObject; -import mage.abilities.Ability; import mage.abilities.effects.common.ExileTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.filter.StaticFilters; -import mage.game.Game; -import mage.game.permanent.Permanent; +import mage.constants.TargetController; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.other.DamagedPlayerThisTurnPredicate; import mage.target.TargetPermanent; -import mage.watchers.common.PlayerDamagedBySourceWatcher; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; /** @@ -22,12 +17,18 @@ import java.util.UUID; */ public final class Reciprocate extends CardImpl { + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature that dealt damage to you this turn"); + + static { + filter.add(new DamagedPlayerThisTurnPredicate(TargetController.YOU)); + } + public Reciprocate(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{W}"); // Exile target creature that dealt damage to you this turn. this.getSpellAbility().addEffect(new ExileTargetEffect()); - this.getSpellAbility().addTarget(new ReciprocateTarget()); + this.getSpellAbility().addTarget(new TargetPermanent(filter)); } private Reciprocate(final Reciprocate card) { @@ -40,66 +41,3 @@ public final class Reciprocate extends CardImpl { } } - -class ReciprocateTarget extends TargetPermanent { - - public ReciprocateTarget() { - super(1, 1, StaticFilters.FILTER_PERMANENT_CREATURE, false); - targetName = "creature that dealt damage to you this turn"; - } - - private ReciprocateTarget(final ReciprocateTarget target) { - super(target); - } - - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - PlayerDamagedBySourceWatcher watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, source.getControllerId()); - if (watcher != null && watcher.hasSourceDoneDamage(id, game)) { - return super.canTarget(id, source, game); - } - return false; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set availablePossibleTargets = super.possibleTargets(sourceControllerId, source, game); - Set possibleTargets = new HashSet<>(); - PlayerDamagedBySourceWatcher watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, sourceControllerId); - for (UUID targetId : availablePossibleTargets) { - Permanent permanent = game.getPermanent(targetId); - if (permanent != null && watcher != null && watcher.hasSourceDoneDamage(targetId, game)) { - possibleTargets.add(targetId); - } - } - return possibleTargets; - } - - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int remainingTargets = this.minNumberOfTargets - targets.size(); - if (remainingTargets == 0) { - return true; - } - int count = 0; - MageObject targetSource = game.getObject(source); - if (targetSource != null) { - PlayerDamagedBySourceWatcher watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, sourceControllerId); - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId()) && permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game) - && watcher != null && watcher.hasSourceDoneDamage(permanent.getId(), game)) { - count++; - if (count >= remainingTargets) { - return true; - } - } - } - } - return false; - } - - @Override - public ReciprocateTarget copy() { - return new ReciprocateTarget(this); - } -} diff --git a/Mage.Sets/src/mage/cards/r/ReckonerShakedown.java b/Mage.Sets/src/mage/cards/r/ReckonerShakedown.java index 0d616b47e77..72f208a3d11 100644 --- a/Mage.Sets/src/mage/cards/r/ReckonerShakedown.java +++ b/Mage.Sets/src/mage/cards/r/ReckonerShakedown.java @@ -7,6 +7,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; +import mage.constants.Zone; import mage.counters.CounterType; import mage.filter.StaticFilters; import mage.game.Game; @@ -68,7 +69,7 @@ class ReckonerShakedownEffect extends OneShotEffect { return false; } player.revealCards(source, player.getHand(), game); - TargetCard target = new TargetCardInHand(0, 1, StaticFilters.FILTER_CARD_A_NON_LAND); + TargetCard target = new TargetCard(0, 1, Zone.HAND, StaticFilters.FILTER_CARD_A_NON_LAND); controller.choose(Outcome.Discard, player.getHand(), target, source, game); Card card = game.getCard(target.getFirstTarget()); if (card != null) { diff --git a/Mage.Sets/src/mage/cards/r/RedirectLightning.java b/Mage.Sets/src/mage/cards/r/RedirectLightning.java new file mode 100644 index 00000000000..60822f32d1b --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RedirectLightning.java @@ -0,0 +1,49 @@ +package mage.cards.r; + +import mage.abilities.costs.OrCost; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.ChooseNewTargetsTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.FilterStackObject; +import mage.filter.predicate.other.NumberOfTargetsPredicate; +import mage.target.TargetStackObject; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RedirectLightning extends CardImpl { + + private static final FilterStackObject filter = new FilterStackObject("spell or ability with a single target"); + + static { + filter.add(new NumberOfTargetsPredicate(1)); + } + + public RedirectLightning(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}"); + + this.subtype.add(SubType.LESSON); + + // As an additional cost to cast this spell, pay 5 life or pay {2}. + this.getSpellAbility().addCost(new OrCost("pay 5 life or pay {2}", new PayLifeCost(5), new GenericManaCost(2))); + + // Change the target of target spell or ability with a single target. + this.getSpellAbility().addEffect(new ChooseNewTargetsTargetEffect(true, true)); + this.getSpellAbility().addTarget(new TargetStackObject(filter)); + } + + private RedirectLightning(final RedirectLightning card) { + super(card); + } + + @Override + public RedirectLightning copy() { + return new RedirectLightning(this); + } +} diff --git a/Mage.Sets/src/mage/cards/r/Retether.java b/Mage.Sets/src/mage/cards/r/Retether.java index 97db1ae084e..23315d003bd 100644 --- a/Mage.Sets/src/mage/cards/r/Retether.java +++ b/Mage.Sets/src/mage/cards/r/Retether.java @@ -87,7 +87,7 @@ class RetetherEffect extends OneShotEffect { for (Ability ability : aura.getAbilities()) { if (ability instanceof SpellAbility) { for (Target abilityTarget : ability.getTargets()) { - if (abilityTarget.possibleTargets(controller.getId(), game).contains(permanent.getId())) { + if (abilityTarget.possibleTargets(controller.getId(), source, game).contains(permanent.getId())) { target = abilityTarget.copy(); break auraLegalitySearch; } diff --git a/Mage.Sets/src/mage/cards/r/ReturnFromExtinction.java b/Mage.Sets/src/mage/cards/r/ReturnFromExtinction.java index 7c77ece8f41..2e00840a16d 100644 --- a/Mage.Sets/src/mage/cards/r/ReturnFromExtinction.java +++ b/Mage.Sets/src/mage/cards/r/ReturnFromExtinction.java @@ -58,8 +58,8 @@ class ReturnFromExtinctionTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } if (getTargets().isEmpty()) { diff --git a/Mage.Sets/src/mage/cards/r/ReunionOfTheHouse.java b/Mage.Sets/src/mage/cards/r/ReunionOfTheHouse.java index 50f9f129c53..8309d525b8d 100644 --- a/Mage.Sets/src/mage/cards/r/ReunionOfTheHouse.java +++ b/Mage.Sets/src/mage/cards/r/ReunionOfTheHouse.java @@ -61,8 +61,8 @@ class ReunionOfTheHouseTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, m -> m.getPower().getValue(), 10, game); } diff --git a/Mage.Sets/src/mage/cards/r/RevealingEye.java b/Mage.Sets/src/mage/cards/r/RevealingEye.java index cbd392e737c..284c7eee69b 100644 --- a/Mage.Sets/src/mage/cards/r/RevealingEye.java +++ b/Mage.Sets/src/mage/cards/r/RevealingEye.java @@ -11,6 +11,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; +import mage.constants.Zone; import mage.filter.StaticFilters; import mage.game.Game; import mage.players.Player; @@ -82,7 +83,7 @@ class RevealingEyeEffect extends OneShotEffect { if (opponent.getHand().count(StaticFilters.FILTER_CARD_NON_LAND, game) < 1) { return true; } - TargetCard target = new TargetCardInHand(0, 1, StaticFilters.FILTER_CARD_NON_LAND); + TargetCard target = new TargetCard(0, 1, Zone.HAND, StaticFilters.FILTER_CARD_NON_LAND); controller.choose(outcome, opponent.getHand(), target, source, game); Card card = game.getCard(target.getFirstTarget()); if (card == null) { diff --git a/Mage.Sets/src/mage/cards/r/RevivalExperiment.java b/Mage.Sets/src/mage/cards/r/RevivalExperiment.java index bf79a994bd5..caa7c28ec3f 100644 --- a/Mage.Sets/src/mage/cards/r/RevivalExperiment.java +++ b/Mage.Sets/src/mage/cards/r/RevivalExperiment.java @@ -120,7 +120,7 @@ class RevivalExperimentTarget extends TargetCardInYourGraveyard { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, null, game)); + possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, source, game)); return possibleTargets; } } diff --git a/Mage.Sets/src/mage/cards/r/RiftElemental.java b/Mage.Sets/src/mage/cards/r/RiftElemental.java index cb9a06e5890..dab8891b0cb 100644 --- a/Mage.Sets/src/mage/cards/r/RiftElemental.java +++ b/Mage.Sets/src/mage/cards/r/RiftElemental.java @@ -99,7 +99,7 @@ class RiftElementalCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { Target target = new TargetPermanentOrSuspendedCard(filter, true); - return target.canChoose(controllerId, source, game); + return target.canChooseOrAlreadyChosen(controllerId, source, game); } @Override diff --git a/Mage.Sets/src/mage/cards/r/RikkuResourcefulGuardian.java b/Mage.Sets/src/mage/cards/r/RikkuResourcefulGuardian.java index 360e59dfc59..b9636978bde 100644 --- a/Mage.Sets/src/mage/cards/r/RikkuResourcefulGuardian.java +++ b/Mage.Sets/src/mage/cards/r/RikkuResourcefulGuardian.java @@ -2,22 +2,22 @@ package mage.cards.r; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.common.PutCounterOnPermanentTriggeredAbility; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.common.combat.CantBeBlockedByAllTargetEffect; import mage.abilities.effects.common.counter.MoveCounterTargetsEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.game.events.GameEvent; import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetOpponentsCreaturePermanent; -import java.util.Optional; import java.util.UUID; /** @@ -35,7 +35,12 @@ public final class RikkuResourcefulGuardian extends CardImpl { this.toughness = new MageInt(3); // Whenever you put one or more counters on a creature, until end of turn, that creature can't be blocked by creatures your opponents control. - this.addAbility(new RikkuResourcefulGuardianTriggeredAbility()); + this.addAbility(new PutCounterOnPermanentTriggeredAbility( + new CantBeBlockedByAllTargetEffect( + StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE, Duration.EndOfTurn + ).setText("until end of turn, that creature can't be blocked by creatures your opponents control"), + null, StaticFilters.FILTER_PERMANENT_CREATURE, true, false + )); // Steal -- {1}, {T}: Move a counter from target creature an opponent controls onto target creature you control. Activate only as a sorcery. Ability ability = new ActivateAsSorceryActivatedAbility(new MoveCounterTargetsEffect(), new GenericManaCost(1)); @@ -54,38 +59,3 @@ public final class RikkuResourcefulGuardian extends CardImpl { return new RikkuResourcefulGuardian(this); } } - -class RikkuResourcefulGuardianTriggeredAbility extends TriggeredAbilityImpl { - - RikkuResourcefulGuardianTriggeredAbility() { - super(Zone.BATTLEFIELD, new CantBeBlockedByAllTargetEffect( - StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE, Duration.EndOfTurn - ).setText("until end of turn, that creature can't be blocked by creatures your opponents control")); - this.setTriggerPhrase("Whenever you put one or more counters on a creature, "); - } - - private RikkuResourcefulGuardianTriggeredAbility(final RikkuResourcefulGuardianTriggeredAbility ability) { - super(ability); - } - - @Override - public RikkuResourcefulGuardianTriggeredAbility copy() { - return new RikkuResourcefulGuardianTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.COUNTERS_ADDED; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - return isControlledBy(event.getPlayerId()) - && Optional - .ofNullable(event) - .map(GameEvent::getTargetId) - .map(game::getPermanent) - .map(permanent -> permanent.isCreature(game)) - .orElse(false); - } -} diff --git a/Mage.Sets/src/mage/cards/r/RivalsDuel.java b/Mage.Sets/src/mage/cards/r/RivalsDuel.java index 8e4851faec7..1f31733aa21 100644 --- a/Mage.Sets/src/mage/cards/r/RivalsDuel.java +++ b/Mage.Sets/src/mage/cards/r/RivalsDuel.java @@ -90,8 +90,8 @@ class RivalsDuelTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } Permanent creature = game.getPermanent(id); diff --git a/Mage.Sets/src/mage/cards/r/RoguesGallery.java b/Mage.Sets/src/mage/cards/r/RoguesGallery.java index e7769a5831e..2dad3fa0abd 100644 --- a/Mage.Sets/src/mage/cards/r/RoguesGallery.java +++ b/Mage.Sets/src/mage/cards/r/RoguesGallery.java @@ -84,7 +84,7 @@ class RoguesGalleryTarget extends TargetCardInYourGraveyard { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, null, game)); + possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, source, game)); return possibleTargets; } } diff --git a/Mage.Sets/src/mage/cards/r/RoleReversal.java b/Mage.Sets/src/mage/cards/r/RoleReversal.java index 61a31de577e..12d30093ed1 100644 --- a/Mage.Sets/src/mage/cards/r/RoleReversal.java +++ b/Mage.Sets/src/mage/cards/r/RoleReversal.java @@ -53,8 +53,8 @@ class TargetPermanentsThatShareCardType extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (super.canTarget(playerId, id, source, game)) { if (!getTargets().isEmpty()) { Permanent targetOne = game.getPermanent(getTargets().get(0)); Permanent targetTwo = game.getPermanent(id); diff --git a/Mage.Sets/src/mage/cards/r/RunAwayTogether.java b/Mage.Sets/src/mage/cards/r/RunAwayTogether.java index d5107d33568..62ab7a5d2f7 100644 --- a/Mage.Sets/src/mage/cards/r/RunAwayTogether.java +++ b/Mage.Sets/src/mage/cards/r/RunAwayTogether.java @@ -59,8 +59,8 @@ class RunAwayTogetherTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } Permanent creature = game.getPermanent(id); diff --git a/Mage.Sets/src/mage/cards/s/ScoutForSurvivors.java b/Mage.Sets/src/mage/cards/s/ScoutForSurvivors.java index 44072938fec..ffc40661fda 100644 --- a/Mage.Sets/src/mage/cards/s/ScoutForSurvivors.java +++ b/Mage.Sets/src/mage/cards/s/ScoutForSurvivors.java @@ -96,8 +96,8 @@ class ScoutForSurvivorsTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 3, game ); diff --git a/Mage.Sets/src/mage/cards/s/ScreamsFromWithin.java b/Mage.Sets/src/mage/cards/s/ScreamsFromWithin.java index 6037655954e..a2276bf5f14 100644 --- a/Mage.Sets/src/mage/cards/s/ScreamsFromWithin.java +++ b/Mage.Sets/src/mage/cards/s/ScreamsFromWithin.java @@ -77,7 +77,7 @@ class ScreamsFromWithinEffect extends OneShotEffect { if (aura != null && game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD && player != null && player.getGraveyard().contains(source.getSourceId())) { for (Permanent creaturePermanent : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, source.getControllerId(), source, game)) { for (Target target : aura.getSpellAbility().getTargets()) { - if (target.canTarget(creaturePermanent.getId(), game)) { + if (target.canTarget(creaturePermanent.getId(), source, game)) { return player.moveCards(aura, Zone.BATTLEFIELD, source, game); } } diff --git a/Mage.Sets/src/mage/cards/s/SearingBlaze.java b/Mage.Sets/src/mage/cards/s/SearingBlaze.java index 9c76774aa3e..113afd02074 100644 --- a/Mage.Sets/src/mage/cards/s/SearingBlaze.java +++ b/Mage.Sets/src/mage/cards/s/SearingBlaze.java @@ -1,6 +1,5 @@ package mage.cards.s; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.cards.CardImpl; @@ -11,19 +10,16 @@ import mage.constants.Outcome; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.game.stack.StackObject; import mage.players.Player; import mage.target.TargetPermanent; import mage.target.common.TargetPlayerOrPlaneswalker; import mage.watchers.common.LandfallWatcher; -import java.util.HashSet; import java.util.Set; import java.util.UUID; /** - * @author BetaSteward_at_googlemail.com - * @author North + * @author BetaSteward_at_googlemail.com, North */ public final class SearingBlaze extends CardImpl { @@ -111,21 +107,21 @@ class SearingBlazeTarget extends TargetPermanent { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set availablePossibleTargets = super.possibleTargets(sourceControllerId, source, game); - Set possibleTargets = new HashSet<>(); - MageObject object = game.getObject(source); - if (object instanceof StackObject) { - UUID playerId = ((StackObject) object).getStackAbility().getFirstTarget(); - Player player = game.getPlayerOrPlaneswalkerController(playerId); - if (player != null) { - for (UUID targetId : availablePossibleTargets) { - Permanent permanent = game.getPermanent(targetId); - if (permanent != null && permanent.isControlledBy(player.getId())) { - possibleTargets.add(targetId); - } - } - } + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); + + Player needPlayer = game.getPlayerOrPlaneswalkerController(source.getFirstTarget()); + if (needPlayer == null) { + // playable or not selected - use any + } else { + // filter by controller + possibleTargets.removeIf(id -> { + Permanent permanent = game.getPermanent(id); + return permanent == null + || permanent.getId().equals(source.getFirstTarget()) + || !permanent.isControlledBy(needPlayer.getId()); + }); } + return possibleTargets; } diff --git a/Mage.Sets/src/mage/cards/s/SeverancePriest.java b/Mage.Sets/src/mage/cards/s/SeverancePriest.java index ff17d0efc7b..6f040691e1d 100644 --- a/Mage.Sets/src/mage/cards/s/SeverancePriest.java +++ b/Mage.Sets/src/mage/cards/s/SeverancePriest.java @@ -13,6 +13,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; +import mage.constants.Zone; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.token.SpiritXXToken; @@ -86,7 +87,7 @@ class SeverancePriestEffect extends OneShotEffect { return false; } opponent.revealCards(source, opponent.getHand(), game); - TargetCard target = new TargetCardInHand(0, 1, StaticFilters.FILTER_CARD_NON_LAND); + TargetCard target = new TargetCard(0, 1, Zone.HAND, StaticFilters.FILTER_CARD_NON_LAND); controller.choose(Outcome.Discard, opponent.getHand(), target, source, game); Card card = game.getCard(target.getFirstTarget()); return card != null && controller.moveCardsToExile( diff --git a/Mage.Sets/src/mage/cards/s/ShiftingLoyalties.java b/Mage.Sets/src/mage/cards/s/ShiftingLoyalties.java index 40772d276f3..e7983619e12 100644 --- a/Mage.Sets/src/mage/cards/s/ShiftingLoyalties.java +++ b/Mage.Sets/src/mage/cards/s/ShiftingLoyalties.java @@ -53,8 +53,8 @@ class TargetPermanentsThatShareCardType extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (super.canTarget(playerId, id, source, game)) { if (!getTargets().isEmpty()) { Permanent targetOne = game.getPermanent(getTargets().get(0)); Permanent targetTwo = game.getPermanent(id); diff --git a/Mage.Sets/src/mage/cards/s/ShimianSpecter.java b/Mage.Sets/src/mage/cards/s/ShimianSpecter.java index 408f6f34638..d79a441e3d7 100644 --- a/Mage.Sets/src/mage/cards/s/ShimianSpecter.java +++ b/Mage.Sets/src/mage/cards/s/ShimianSpecter.java @@ -88,9 +88,7 @@ class ShimianSpecterEffect extends SearchTargetGraveyardHandLibraryForCardNameAn // You choose a nonland card from it TargetCard target = new TargetCard(Zone.HAND, new FilterNonlandCard()); - target.withNotTarget(true); - if (target.canChoose(controller.getId(), source, game) - && controller.chooseTarget(Outcome.Benefit, targetPlayer.getHand(), target, source, game)) { + if (controller.choose(Outcome.Benefit, targetPlayer.getHand(), target, source, game)) { return applySearchAndExile(game, source, CardUtil.getCardNameForSameNameSearch(game.getCard(target.getFirstTarget())), getTargetPointer().getFirst(game, source)); } } diff --git a/Mage.Sets/src/mage/cards/s/Shuriken.java b/Mage.Sets/src/mage/cards/s/Shuriken.java index 9e922183eff..5ee5b8a7a91 100644 --- a/Mage.Sets/src/mage/cards/s/Shuriken.java +++ b/Mage.Sets/src/mage/cards/s/Shuriken.java @@ -84,7 +84,7 @@ class ShurikenEffect extends OneShotEffect { return true; } game.addEffect(new GainControlTargetEffect( - Duration.Custom, true, attached.getControllerId() + Duration.Custom, true, targetedPermanent.getControllerId() ).setTargetPointer(new FixedTarget(equipment, game)), source); return true; } diff --git a/Mage.Sets/src/mage/cards/s/SirenStormtamer.java b/Mage.Sets/src/mage/cards/s/SirenStormtamer.java index 4800139e41d..55612b969cc 100644 --- a/Mage.Sets/src/mage/cards/s/SirenStormtamer.java +++ b/Mage.Sets/src/mage/cards/s/SirenStormtamer.java @@ -1,9 +1,6 @@ package mage.cards.s; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; @@ -15,19 +12,18 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; -import mage.filter.Filter; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.game.stack.Spell; -import mage.game.stack.StackAbility; import mage.game.stack.StackObject; import mage.target.Target; -import mage.target.TargetObject; +import mage.target.TargetStackObject; import mage.target.Targets; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + /** - * * @author spjspj */ public final class SirenStormtamer extends CardImpl { @@ -46,7 +42,7 @@ public final class SirenStormtamer extends CardImpl { // {U}, Sacrifice Siren Stormtamer: Counter target spell or ability that targets you or a creature you control. Ability ability = new SimpleActivatedAbility(new CounterTargetEffect(), new ManaCostsImpl<>("{U}")); - ability.addTarget(new SirenStormtamerTargetObject()); + ability.addTarget(new SirenStormtamerTarget()); ability.addCost(new SacrificeSourceCost()); this.addAbility(ability); @@ -62,97 +58,45 @@ public final class SirenStormtamer extends CardImpl { } } -class SirenStormtamerTargetObject extends TargetObject { +class SirenStormtamerTarget extends TargetStackObject { - public SirenStormtamerTargetObject() { - this.minNumberOfTargets = 1; - this.maxNumberOfTargets = 1; - this.zone = Zone.STACK; - this.targetName = "spell or ability that targets you or a creature you control"; + public SirenStormtamerTarget() { + super(); + withTargetName("spell or ability that targets you or a creature you control"); } - private SirenStormtamerTargetObject(final SirenStormtamerTargetObject target) { + private SirenStormtamerTarget(final SirenStormtamerTarget target) { super(target); } @Override - public boolean canTarget(UUID id, Ability source, Game game) { - StackObject stackObject = game.getStack().getStackObject(id); - return (stackObject instanceof Spell) || (stackObject instanceof StackAbility); - } + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - return canChoose(sourceControllerId, game); - } - - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { + Set targetsMe = new HashSet<>(); for (StackObject stackObject : game.getStack()) { - if ((stackObject instanceof Spell) || (stackObject instanceof StackAbility)) { - Targets objectTargets = stackObject.getStackAbility().getTargets(); - if (!objectTargets.isEmpty()) { - for (Target target : objectTargets) { - for (UUID targetId : target.getTargets()) { - Permanent targetedPermanent = game.getPermanentOrLKIBattlefield(targetId); - if (targetedPermanent != null - && targetedPermanent.isControlledBy(sourceControllerId) - && targetedPermanent.isCreature(game)) { - return true; - } - - if (sourceControllerId.equals(targetId)) { - return true; - } - - } + Targets objectTargets = stackObject.getStackAbility().getTargets(); + for (Target target : objectTargets) { + for (UUID targetId : target.getTargets()) { + Permanent targetedPermanent = game.getPermanentOrLKIBattlefield(targetId); + if (targetedPermanent != null + && targetedPermanent.isControlledBy(sourceControllerId) + && targetedPermanent.isCreature(game)) { + targetsMe.add(stackObject.getId()); + } + if (sourceControllerId.equals(targetId)) { + targetsMe.add(stackObject.getId()); } } } } - return false; - } + possibleTargets.removeIf(id -> !targetsMe.contains(id)); - @Override - public Set possibleTargets(UUID sourceControllerId, - Ability source, Game game) { - return possibleTargets(sourceControllerId, game); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - Set possibleTargets = new HashSet<>(); - for (StackObject stackObject : game.getStack()) { - if ((stackObject instanceof Spell) || (stackObject instanceof StackAbility)) { - Targets objectTargets = stackObject.getStackAbility().getTargets(); - if (!objectTargets.isEmpty()) { - for (Target target : objectTargets) { - for (UUID targetId : target.getTargets()) { - Permanent targetedPermanent = game.getPermanentOrLKIBattlefield(targetId); - if (targetedPermanent != null - && targetedPermanent.isControlledBy(sourceControllerId) - && targetedPermanent.isCreature(game)) { - possibleTargets.add(stackObject.getId()); - } - - if (sourceControllerId.equals(targetId)) { - possibleTargets.add(stackObject.getId()); - } - } - } - } - } - } return possibleTargets; } @Override - public SirenStormtamerTargetObject copy() { - return new SirenStormtamerTargetObject(this); - } - - @Override - public Filter getFilter() { - throw new UnsupportedOperationException("Not supported yet."); + public SirenStormtamerTarget copy() { + return new SirenStormtamerTarget(this); } } diff --git a/Mage.Sets/src/mage/cards/s/SokkaBoldBoomeranger.java b/Mage.Sets/src/mage/cards/s/SokkaBoldBoomeranger.java new file mode 100644 index 00000000000..272744db546 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SokkaBoldBoomeranger.java @@ -0,0 +1,60 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.common.discard.DiscardAndDrawThatManyEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.counters.CounterType; +import mage.filter.FilterSpell; +import mage.filter.predicate.Predicates; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SokkaBoldBoomeranger extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("an artifact or Lesson spell"); + + static { + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + SubType.LESSON.getPredicate() + )); + } + + public SokkaBoldBoomeranger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.subtype.add(SubType.ALLY); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // When Sokka enters, discard up to two cards, then draw that many cards. + this.addAbility(new EntersBattlefieldTriggeredAbility(new DiscardAndDrawThatManyEffect(2))); + + // Whenever you cast an artifact or Lesson spell, put a +1/+1 counter on Sokka. + this.addAbility(new SpellCastControllerTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), filter, false + )); + } + + private SokkaBoldBoomeranger(final SokkaBoldBoomeranger card) { + super(card); + } + + @Override + public SokkaBoldBoomeranger copy() { + return new SokkaBoldBoomeranger(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SokkasHaiku.java b/Mage.Sets/src/mage/cards/s/SokkasHaiku.java new file mode 100644 index 00000000000..ea0b7faf6ea --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SokkasHaiku.java @@ -0,0 +1,50 @@ +package mage.cards.s; + +import mage.abilities.effects.common.CounterTargetEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.effects.common.UntapTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.target.TargetSpell; +import mage.target.common.TargetLandPermanent; +import mage.target.targetpointer.SecondTargetPointer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SokkasHaiku extends CardImpl { + + public SokkasHaiku(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{U}{U}"); + + this.subtype.add(SubType.LESSON); + + // Counter target spell. + this.getSpellAbility().addEffect(new CounterTargetEffect()); + this.getSpellAbility().addTarget(new TargetSpell()); + + // Draw a card, then mill three cards. + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1).concatBy("
")); + this.getSpellAbility().addEffect(new MillCardsControllerEffect(3).concatBy(", then")); + + // Untap target land. + this.getSpellAbility().addEffect(new UntapTargetEffect("untap target land") + .concatBy("
") + .setTargetPointer(new SecondTargetPointer())); + this.getSpellAbility().addTarget(new TargetLandPermanent()); + } + + private SokkasHaiku(final SokkasHaiku card) { + super(card); + } + + @Override + public SokkasHaiku copy() { + return new SokkasHaiku(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SoulOfShandalar.java b/Mage.Sets/src/mage/cards/s/SoulOfShandalar.java index c26c14ca77b..f18b7cf1613 100644 --- a/Mage.Sets/src/mage/cards/s/SoulOfShandalar.java +++ b/Mage.Sets/src/mage/cards/s/SoulOfShandalar.java @@ -1,11 +1,7 @@ package mage.cards.s; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; import mage.MageInt; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.ExileSourceFromGraveCost; @@ -15,17 +11,19 @@ import mage.abilities.keyword.FirstStrikeAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.Zone; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.game.stack.StackObject; import mage.players.Player; import mage.target.TargetPermanent; import mage.target.common.TargetPlayerOrPlaneswalker; +import java.util.Set; +import java.util.UUID; + /** * @author noxx */ @@ -96,54 +94,30 @@ class SoulOfShandalarEffect extends OneShotEffect { class SoulOfShandalarTarget extends TargetPermanent { public SoulOfShandalarTarget() { - super(0, 1, new FilterCreaturePermanent("creature that the targeted player or planeswalker's controller controls"), false); + super(0, 1, new FilterCreaturePermanent("creature that the targeted player or planeswalker's controller controls")); } private SoulOfShandalarTarget(final SoulOfShandalarTarget target) { super(target); } - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - Player player = game.getPlayerOrPlaneswalkerController(source.getFirstTarget()); - if (player == null) { - return false; - } - UUID firstTarget = player.getId(); - Permanent permanent = game.getPermanent(id); - if (firstTarget != null && permanent != null && permanent.isControlledBy(firstTarget)) { - return super.canTarget(id, source, game); - } - return false; - } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set availablePossibleTargets = super.possibleTargets(sourceControllerId, source, game); - Set possibleTargets = new HashSet<>(); - MageObject object = game.getObject(source); + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - for (StackObject item : game.getState().getStack()) { - if (item.getId().equals(source.getSourceId())) { - object = item; - } - if (item.getSourceId().equals(source.getSourceId())) { - object = item; - } + Player needPlayer = game.getPlayerOrPlaneswalkerController(source.getFirstTarget()); + if (needPlayer == null) { + // playable or not selected - use any + } else { + // filter by controller + possibleTargets.removeIf(id -> { + Permanent permanent = game.getPermanent(id); + return permanent == null + || permanent.getId().equals(source.getFirstTarget()) + || !permanent.isControlledBy(needPlayer.getId()); + }); } - if (object instanceof StackObject) { - UUID playerId = ((StackObject) object).getStackAbility().getFirstTarget(); - Player player = game.getPlayerOrPlaneswalkerController(playerId); - if (player != null) { - for (UUID targetId : availablePossibleTargets) { - Permanent permanent = game.getPermanent(targetId); - if (permanent != null && permanent.isControlledBy(player.getId())) { - possibleTargets.add(targetId); - } - } - } - } return possibleTargets; } diff --git a/Mage.Sets/src/mage/cards/s/SoulSculptor.java b/Mage.Sets/src/mage/cards/s/SoulSculptor.java index d0ab23a7962..3e4a46d323d 100644 --- a/Mage.Sets/src/mage/cards/s/SoulSculptor.java +++ b/Mage.Sets/src/mage/cards/s/SoulSculptor.java @@ -120,13 +120,8 @@ enum SoulSculptorCondition implements Condition { @Override public boolean apply(Game game, Ability source) { - if (!game.getStack().isEmpty()) { - StackObject stackObject = game.getStack().getFirst(); - if (stackObject != null) { - return !stackObject.getCardType(game).contains(CardType.CREATURE); - } - } - return true; + StackObject stackObject = game.getStack().getFirstOrNull(); + return stackObject != null && !stackObject.getCardType(game).contains(CardType.CREATURE); } @Override diff --git a/Mage.Sets/src/mage/cards/s/SoulSearch.java b/Mage.Sets/src/mage/cards/s/SoulSearch.java index 75da014f391..8d53266e750 100644 --- a/Mage.Sets/src/mage/cards/s/SoulSearch.java +++ b/Mage.Sets/src/mage/cards/s/SoulSearch.java @@ -69,7 +69,7 @@ class SoulSearchEffect extends OneShotEffect { if (opponent.getHand().count(StaticFilters.FILTER_CARD_NON_LAND, game) < 1) { return true; } - TargetCard target = new TargetCardInHand(StaticFilters.FILTER_CARD_NON_LAND); + TargetCard target = new TargetCard(1, Zone.HAND, StaticFilters.FILTER_CARD_NON_LAND); controller.choose(Outcome.Discard, opponent.getHand(), target, source, game); Card card = game.getCard(target.getFirstTarget()); if (card == null) { diff --git a/Mage.Sets/src/mage/cards/s/SoulfireGrandMaster.java b/Mage.Sets/src/mage/cards/s/SoulfireGrandMaster.java index b6e6f71b085..2f8db38d4a6 100644 --- a/Mage.Sets/src/mage/cards/s/SoulfireGrandMaster.java +++ b/Mage.Sets/src/mage/cards/s/SoulfireGrandMaster.java @@ -21,6 +21,7 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; import mage.game.stack.Spell; +import mage.game.stack.StackObject; import mage.players.Player; import java.util.UUID; @@ -94,18 +95,22 @@ class SoulfireGrandMasterCastFromHandReplacementEffect extends ReplacementEffect @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Spell spell = (Spell) game.getStack().getFirst(); - if (!spell.isCopy() && !spell.isCountered()) { - Card sourceCard = game.getCard(spellId); - if (sourceCard != null && Zone.STACK.equals(game.getState().getZone(spellId))) { - Player player = game.getPlayer(sourceCard.getOwnerId()); - if (player != null) { - player.moveCards(sourceCard, Zone.HAND, source, game); - discard(); - return true; + StackObject stackObject = game.getStack().getFirstOrNull(); + if (stackObject instanceof Spell) { + Spell spell = (Spell) stackObject; + if (!spell.isCopy() && !spell.isCountered()) { + Card sourceCard = game.getCard(spellId); + if (sourceCard != null && Zone.STACK.equals(game.getState().getZone(spellId))) { + Player player = game.getPlayer(sourceCard.getOwnerId()); + if (player != null) { + player.moveCards(sourceCard, Zone.HAND, source, game); + discard(); + return true; + } } } } + return false; } @@ -134,8 +139,8 @@ class SoulfireGrandMasterCastFromHandReplacementEffect extends ReplacementEffect if (zEvent.getFromZone() == Zone.STACK && zEvent.getToZone() == Zone.GRAVEYARD && event.getTargetId().equals(spellId)) { - if (game.getStack().getFirst() instanceof Spell) { - Card cardOfSpell = ((Spell) game.getStack().getFirst()).getCard(); + if (game.getStack().getFirstOrNull() instanceof Spell) { + Card cardOfSpell = ((Spell) game.getStack().getFirstOrNull()).getCard(); return cardOfSpell.getMainCard().getId().equals(spellId); } } diff --git a/Mage.Sets/src/mage/cards/s/SouthernAirTemple.java b/Mage.Sets/src/mage/cards/s/SouthernAirTemple.java new file mode 100644 index 00000000000..ce184fcb615 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SouthernAirTemple.java @@ -0,0 +1,61 @@ +package mage.cards.s; + +import mage.abilities.common.EntersBattlefieldAllTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.counter.AddCountersAllEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SouthernAirTemple extends CardImpl { + + private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(new FilterControlledPermanent(SubType.SHRINE)); + private static final Hint hint = new ValueHint("Shrines you control", xValue); + private static final FilterPermanent filter = new FilterControlledPermanent(SubType.SHRINE, "another Shrine you control"); + + static { + filter.add(AnotherPredicate.instance); + } + + public SouthernAirTemple(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SHRINE); + + // When Southern Air Temple enters, put X +1/+1 counters on each creature you control, where X is the number of Shrines you control. + this.addAbility(new EntersBattlefieldTriggeredAbility(new AddCountersAllEffect( + CounterType.P1P1.createInstance(), xValue, StaticFilters.FILTER_CONTROLLED_CREATURE + ).setText("put X +1/+1 counters on each creature you control, where X is the number of Shrines you control")).addHint(hint)); + + // Whenever another Shrine you control enters, put a +1/+1 counter on each creature you control. + this.addAbility(new EntersBattlefieldAllTriggeredAbility(new AddCountersAllEffect( + CounterType.P1P1.createInstance(), StaticFilters.FILTER_CONTROLLED_CREATURE + ), filter)); + } + + private SouthernAirTemple(final SouthernAirTemple card) { + super(card); + } + + @Override + public SouthernAirTemple copy() { + return new SouthernAirTemple(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/Spawnbroker.java b/Mage.Sets/src/mage/cards/s/Spawnbroker.java index 476dc520868..bad7f039c65 100644 --- a/Mage.Sets/src/mage/cards/s/Spawnbroker.java +++ b/Mage.Sets/src/mage/cards/s/Spawnbroker.java @@ -1,18 +1,19 @@ - package mage.cards.s; import mage.MageInt; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.effects.common.continuous.ExchangeControlTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; -import mage.target.common.TargetControlledPermanent; import java.util.HashSet; import java.util.Set; @@ -33,8 +34,8 @@ public final class Spawnbroker extends CardImpl { this.toughness = new MageInt(1); // When Spawnbroker enters the battlefield, you may exchange control of target creature you control and target creature with power less than or equal to that creature's power an opponent controls. - Ability ability = new EntersBattlefieldTriggeredAbility(new ExchangeControlTargetEffect(Duration.Custom, rule, false, true), true); - ability.addTarget(new TargetControlledCreatureWithPowerGreaterOrLessThanOpponentPermanent()); + Ability ability = new EntersBattlefieldTriggeredAbility(new ExchangeControlTargetEffect(Duration.EndOfGame, rule, false, true), true); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_CREATURE)); ability.addTarget(new SpawnbrokerSecondTarget()); this.addAbility(ability); @@ -50,89 +51,53 @@ public final class Spawnbroker extends CardImpl { } } -class TargetControlledCreatureWithPowerGreaterOrLessThanOpponentPermanent extends TargetControlledPermanent { - - public TargetControlledCreatureWithPowerGreaterOrLessThanOpponentPermanent() { - super(); - this.filter = this.filter.copy(); - filter.add(CardType.CREATURE.getPredicate()); - withTargetName("creature you control"); - } - - private TargetControlledCreatureWithPowerGreaterOrLessThanOpponentPermanent(final TargetControlledCreatureWithPowerGreaterOrLessThanOpponentPermanent target) { - super(target); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set possibleTargets = new HashSet<>(); - MageObject targetSource = game.getObject(source); - if (targetSource != null) { - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId()) && permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - possibleTargets.add(permanent.getId()); - } - } - } - return possibleTargets; - } - - @Override - public TargetControlledCreatureWithPowerGreaterOrLessThanOpponentPermanent copy() { - return new TargetControlledCreatureWithPowerGreaterOrLessThanOpponentPermanent(this); - } -} - class SpawnbrokerSecondTarget extends TargetPermanent { - private Permanent firstTarget = null; - public SpawnbrokerSecondTarget() { - super(); - this.filter = this.filter.copy(); - filter.add(TargetController.OPPONENT.getControllerPredicate()); - filter.add(CardType.CREATURE.getPredicate()); + super(StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE); withTargetName("creature with power less than or equal to that creature's power an opponent controls"); } private SpawnbrokerSecondTarget(final SpawnbrokerSecondTarget target) { super(target); - this.firstTarget = target.firstTarget; } @Override public boolean canTarget(UUID id, Ability source, Game game) { - if (super.canTarget(id, source, game)) { - Permanent target1 = game.getPermanent(source.getFirstTarget()); - Permanent opponentPermanent = game.getPermanent(id); - if (target1 != null && opponentPermanent != null) { - return target1.getPower().getValue() >= opponentPermanent.getPower().getValue(); - } + Permanent ownPermanent = game.getPermanent(source.getFirstTarget()); + Permanent possiblePermanent = game.getPermanent(id); + if (ownPermanent == null || possiblePermanent == null) { + return false; } - return false; + return super.canTarget(id, source, game) && ownPermanent.getPower().getValue() >= possiblePermanent.getPower().getValue(); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); - if (firstTarget != null) { - MageObject targetSource = game.getObject(source); - if (targetSource != null) { - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId()) && permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - if (firstTarget.getPower().getValue() >= permanent.getPower().getValue()) { - possibleTargets.add(permanent.getId()); - } - } + + Permanent ownPermanent = game.getPermanent(source.getFirstTarget()); + for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { + if (ownPermanent == null) { + // playable or first target not yet selected + // use all + possibleTargets.add(permanent.getId()); + } else { + // real + // filter by power + if (ownPermanent.getPower().getValue() >= permanent.getPower().getValue()) { + possibleTargets.add(permanent.getId()); } } } - return possibleTargets; + possibleTargets.removeIf(id -> ownPermanent != null && ownPermanent.getId().equals(id)); + + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) { - firstTarget = game.getPermanent(source.getFirstTarget()); + // AI hint with better outcome return super.chooseTarget(Outcome.GainControl, playerId, source, game); } diff --git a/Mage.Sets/src/mage/cards/s/SpearOfHeliod.java b/Mage.Sets/src/mage/cards/s/SpearOfHeliod.java index 85de10c7bbd..015a0bc0950 100644 --- a/Mage.Sets/src/mage/cards/s/SpearOfHeliod.java +++ b/Mage.Sets/src/mage/cards/s/SpearOfHeliod.java @@ -18,7 +18,6 @@ import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.other.DamagedPlayerThisTurnPredicate; import mage.target.Target; import mage.target.TargetPermanent; -import mage.target.common.TargetCreaturePermanent; import java.util.UUID; @@ -27,8 +26,7 @@ import java.util.UUID; */ public final class SpearOfHeliod extends CardImpl { - private static final FilterCreaturePermanent filter - = new FilterCreaturePermanent("creature that dealt damage to you this turn"); + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature that dealt damage to you this turn"); static { filter.add(new DamagedPlayerThisTurnPredicate(TargetController.YOU)); @@ -38,15 +36,13 @@ public final class SpearOfHeliod extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT, CardType.ARTIFACT}, "{1}{W}{W}"); this.supertype.add(SuperType.LEGENDARY); - // Creatures you control get +1/+1. this.addAbility(new SimpleStaticAbility(new BoostControlledEffect(1, 1, Duration.WhileOnBattlefield))); // {1}{W}{W}, {T}: Destroy target creature that dealt damage to you this turn. Ability ability = new SimpleActivatedAbility(new DestroyTargetEffect(), new ManaCostsImpl<>("{1}{W}{W}")); ability.addCost(new TapSourceCost()); - Target target = new TargetPermanent(filter); - ability.addTarget(target); + ability.addTarget(new TargetPermanent(filter)); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/s/SpectersShriek.java b/Mage.Sets/src/mage/cards/s/SpectersShriek.java index 0374dacc8f7..e50892777cc 100644 --- a/Mage.Sets/src/mage/cards/s/SpectersShriek.java +++ b/Mage.Sets/src/mage/cards/s/SpectersShriek.java @@ -77,8 +77,7 @@ class SpectersShriekEffect extends OneShotEffect { return false; } TargetCard target = new TargetCard(0, 1, Zone.HAND, new FilterNonlandCard()); - target.withNotTarget(true); - if (!controller.chooseTarget(Outcome.Benefit, player.getHand(), target, source, game)) { + if (!controller.choose(Outcome.Benefit, player.getHand(), target, source, game)) { return false; } Card card = game.getCard(target.getFirstTarget()); diff --git a/Mage.Sets/src/mage/cards/s/SphinxOfTheChimes.java b/Mage.Sets/src/mage/cards/s/SphinxOfTheChimes.java index 7d5070a531b..f36c37b1baa 100644 --- a/Mage.Sets/src/mage/cards/s/SphinxOfTheChimes.java +++ b/Mage.Sets/src/mage/cards/s/SphinxOfTheChimes.java @@ -9,7 +9,6 @@ import mage.abilities.keyword.FlyingAbility; import mage.cards.*; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.common.FilterNonlandCard; import mage.filter.predicate.mageobject.NamePredicate; @@ -82,27 +81,22 @@ class TargetTwoNonLandCardsWithSameNameInHand extends TargetCardInHand { } @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - Set newPossibleTargets = new HashSet<>(); + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); Player player = game.getPlayer(sourceControllerId); if (player == null) { - return newPossibleTargets; - } - for (Card card : player.getHand().getCards(filter, game)) { - possibleTargets.add(card.getId()); + return possibleTargets; } - Cards cardsToCheck = new CardsImpl(); - cardsToCheck.addAll(possibleTargets); + Cards cardsToCheck = new CardsImpl(player.getHand().getCards(filter, game)); if (targets.size() == 1) { - // first target is laready chosen, now only targets with the same name are selectable + // first target is already chosen, now only targets with the same name are selectable for (Map.Entry entry : targets.entrySet()) { Card chosenCard = cardsToCheck.get(entry.getKey(), game); if (chosenCard != null) { for (UUID cardToCheck : cardsToCheck) { if (!cardToCheck.equals(chosenCard.getId()) && chosenCard.getName().equals(game.getCard(cardToCheck).getName())) { - newPossibleTargets.add(cardToCheck); + possibleTargets.add(cardToCheck); } } } @@ -116,56 +110,13 @@ class TargetTwoNonLandCardsWithSameNameInHand extends TargetCardInHand { nameFilter.add(new NamePredicate(nameToSearch)); if (cardsToCheck.count(nameFilter, game) > 1) { - newPossibleTargets.add(cardToCheck); + possibleTargets.add(cardToCheck); } } } } - return newPossibleTargets; - } - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - Cards cardsToCheck = new CardsImpl(); - Player player = game.getPlayer(sourceControllerId); - if (player == null) { - return false; - } - for (Card card : player.getHand().getCards(filter, game)) { - cardsToCheck.add(card.getId()); - } - int possibleCards = 0; - for (Card card : cardsToCheck.getCards(game)) { - String nameToSearch = CardUtil.getCardNameForSameNameSearch(card); - FilterCard nameFilter = new FilterCard(); - nameFilter.add(new NamePredicate(nameToSearch)); - - if (cardsToCheck.count(nameFilter, game) > 1) { - ++possibleCards; - } - } - return possibleCards > 0; - } - - @Override - public boolean canTarget(UUID id, Game game) { - if (super.canTarget(id, game)) { - Card card = game.getCard(id); - if (card != null) { - if (targets.size() == 1) { - Card card2 = game.getCard(targets.entrySet().iterator().next().getKey()); - return CardUtil.haveSameNames(card2, card); - } else { - String nameToSearch = CardUtil.getCardNameForSameNameSearch(card); - FilterCard nameFilter = new FilterCard(); - nameFilter.add(new NamePredicate(nameToSearch)); - - Player player = game.getPlayer(card.getOwnerId()); - return player != null && player.getHand().getCards(nameFilter, game).size() > 1; - } - } - } - return false; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage.Sets/src/mage/cards/s/StickTogether.java b/Mage.Sets/src/mage/cards/s/StickTogether.java index 50f8d9b917c..58064f05a35 100644 --- a/Mage.Sets/src/mage/cards/s/StickTogether.java +++ b/Mage.Sets/src/mage/cards/s/StickTogether.java @@ -139,14 +139,7 @@ class StickTogetherTarget extends TargetPermanent { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, null, game)); + possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, source, game)); return possibleTargets; } - - static int checkTargetCount(Ability source, Game game) { - List permanents = game - .getBattlefield() - .getActivePermanents(filterParty, source.getControllerId(), source, game); - return subTypeAssigner.getRoleCount(new CardsImpl(permanents), game); - } } diff --git a/Mage.Sets/src/mage/cards/s/StoneGiant.java b/Mage.Sets/src/mage/cards/s/StoneGiant.java index 5a781fa56be..a0f1062dfc0 100644 --- a/Mage.Sets/src/mage/cards/s/StoneGiant.java +++ b/Mage.Sets/src/mage/cards/s/StoneGiant.java @@ -68,13 +68,13 @@ class StoneGiantTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { Permanent sourceCreature = game.getPermanent(source.getSourceId()); Permanent targetCreature = game.getPermanent(id); if (targetCreature != null && sourceCreature != null && targetCreature.getToughness().getValue() < sourceCreature.getPower().getValue()) { - return super.canTarget(controllerId, id, source, game); + return super.canTarget(playerId, id, source, game); } return false; } diff --git a/Mage.Sets/src/mage/cards/s/SurgeConductor.java b/Mage.Sets/src/mage/cards/s/SurgeConductor.java index be0d385f7ea..276fee76c2f 100644 --- a/Mage.Sets/src/mage/cards/s/SurgeConductor.java +++ b/Mage.Sets/src/mage/cards/s/SurgeConductor.java @@ -24,7 +24,7 @@ public final class SurgeConductor extends CardImpl { static { filter.add(AnotherPredicate.instance); - filter.add(TokenPredicate.TRUE); + filter.add(TokenPredicate.FALSE); } public SurgeConductor(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage.Sets/src/mage/cards/s/SynthesisPod.java b/Mage.Sets/src/mage/cards/s/SynthesisPod.java index b668b9fe5bc..d1d90bedd00 100644 --- a/Mage.Sets/src/mage/cards/s/SynthesisPod.java +++ b/Mage.Sets/src/mage/cards/s/SynthesisPod.java @@ -93,7 +93,7 @@ class SynthesisPodCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage.Sets/src/mage/cards/t/Technomancer.java b/Mage.Sets/src/mage/cards/t/Technomancer.java index 830c673ba3d..1db84f1f7b8 100644 --- a/Mage.Sets/src/mage/cards/t/Technomancer.java +++ b/Mage.Sets/src/mage/cards/t/Technomancer.java @@ -107,8 +107,8 @@ class TechnomancerTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 6, game); } diff --git a/Mage.Sets/src/mage/cards/t/TemptingWurm.java b/Mage.Sets/src/mage/cards/t/TemptingWurm.java index 1d808530d7e..aeafeff497c 100644 --- a/Mage.Sets/src/mage/cards/t/TemptingWurm.java +++ b/Mage.Sets/src/mage/cards/t/TemptingWurm.java @@ -1,34 +1,30 @@ package mage.cards.t; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.effects.OneShotEffect; -import mage.cards.Card; -import mage.cards.CardImpl; -import mage.cards.CardSetInfo; -import mage.cards.Cards; -import mage.cards.CardsImpl; +import mage.cards.*; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.players.Player; import mage.target.Target; -import mage.target.common.TargetCardInHand; +import mage.target.TargetCard; + +import java.util.UUID; /** - * * @author Eirkei */ public final class TemptingWurm extends CardImpl { public TemptingWurm(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{1}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}"); this.subtype.add(SubType.WURM); this.power = new MageInt(5); this.toughness = new MageInt(5); @@ -59,43 +55,36 @@ class TemptingWurmEffect extends OneShotEffect { CardType.LAND.getPredicate() )); } - + TemptingWurmEffect() { super(Outcome.Detriment); this.staticText = "each opponent may put any number of artifact, creature, enchantment, and/or land cards from their hand onto the battlefield."; } - + @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - + if (controller != null) { Cards cards = new CardsImpl(); - + for (UUID playerId : game.getOpponents(controller.getId())) { Player opponent = game.getPlayer(playerId); - - if (opponent != null){ - Target target = new TargetCardInHand(0, Integer.MAX_VALUE, filter); - - if (target.canChoose(opponent.getId(), source, game)) { - if (opponent.chooseUse(Outcome.PutCardInPlay , "Put any artifact, creature, enchantment, and/or land cards cards from your hand onto the battlefield?", source, game)) { - if (target.chooseTarget(Outcome.PutCardInPlay, opponent.getId(), source, game)) { - for (UUID cardId: target.getTargets()){ - Card card = game.getCard(cardId); - - if (card != null) { - cards.add(card); - } - } + if (opponent != null) { + Target target = new TargetCard(0, Integer.MAX_VALUE, Zone.HAND, filter).withChooseHint("put from hand to battlefield"); + if (target.chooseTarget(Outcome.PutCardInPlay, opponent.getId(), source, game)) { + for (UUID cardId : target.getTargets()) { + Card card = game.getCard(cardId); + if (card != null) { + cards.add(card); } } } } } - + controller.moveCards(cards.getCards(game), Zone.BATTLEFIELD, source, game, false, false, true, null); - + return true; } @@ -105,7 +94,7 @@ class TemptingWurmEffect extends OneShotEffect { private TemptingWurmEffect(final TemptingWurmEffect effect) { super(effect); } - + @Override public TemptingWurmEffect copy() { return new TemptingWurmEffect(this); diff --git a/Mage.Sets/src/mage/cards/t/Terrasymbiosis.java b/Mage.Sets/src/mage/cards/t/Terrasymbiosis.java index 447a2ac2b93..bc3a48921ea 100644 --- a/Mage.Sets/src/mage/cards/t/Terrasymbiosis.java +++ b/Mage.Sets/src/mage/cards/t/Terrasymbiosis.java @@ -1,6 +1,6 @@ package mage.cards.t; -import mage.abilities.common.PutCounterOnCreatureTriggeredAbility; +import mage.abilities.common.PutCounterOnPermanentTriggeredAbility; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.EffectKeyValue; import mage.abilities.effects.common.DrawCardSourceControllerEffect; @@ -23,9 +23,9 @@ public final class Terrasymbiosis extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); // Whenever you put one or more +1/+1 counters on a creature you control, you may draw that many cards. Do this only once each turn. - this.addAbility(new PutCounterOnCreatureTriggeredAbility( + this.addAbility(new PutCounterOnPermanentTriggeredAbility( new DrawCardSourceControllerEffect(xValue), - CounterType.P1P1.createInstance(), StaticFilters.FILTER_CONTROLLED_CREATURE + CounterType.P1P1, StaticFilters.FILTER_CONTROLLED_CREATURE ).setDoOnlyOnceEachTurn(true)); } diff --git a/Mage.Sets/src/mage/cards/t/TheCapitolineTriad.java b/Mage.Sets/src/mage/cards/t/TheCapitolineTriad.java index 1b00eec46b6..107603aedee 100644 --- a/Mage.Sets/src/mage/cards/t/TheCapitolineTriad.java +++ b/Mage.Sets/src/mage/cards/t/TheCapitolineTriad.java @@ -1,9 +1,7 @@ package mage.cards.t; import java.awt.*; -import java.util.Collection; -import java.util.Objects; -import java.util.UUID; +import java.util.*; import mage.MageInt; import mage.MageObject; @@ -170,7 +168,10 @@ class TheCapitolineTriadTarget extends TargetCardInYourGraveyard { return false; } // Check that exiling all the possible cards would have >= 4 different card types - return metCondition(this.possibleTargets(sourceControllerId, source, game), game); + Set idsToCheck = new HashSet<>(); + idsToCheck.addAll(this.getTargets()); + idsToCheck.addAll(this.possibleTargets(sourceControllerId, source, game)); + return metCondition(idsToCheck, game); } private static int manaValueOfSelection(Collection cardsIds, Game game) { diff --git a/Mage.Sets/src/mage/cards/t/TheTricksterGodsHeist.java b/Mage.Sets/src/mage/cards/t/TheTricksterGodsHeist.java index a4eb35e9231..e6cfac11e2b 100644 --- a/Mage.Sets/src/mage/cards/t/TheTricksterGodsHeist.java +++ b/Mage.Sets/src/mage/cards/t/TheTricksterGodsHeist.java @@ -95,8 +95,8 @@ class TheTricksterGodsHeistTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } if (getTargets().isEmpty()) { @@ -118,7 +118,7 @@ class TheTricksterGodsHeistTarget extends TargetPermanent { return false; } for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { + if (!isNotTarget() && !permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { continue; } for (CardType cardType : permanent.getCardType(game)) { diff --git a/Mage.Sets/src/mage/cards/t/TheTwelfthDoctor.java b/Mage.Sets/src/mage/cards/t/TheTwelfthDoctor.java index cea349b3b7e..b1359692b74 100644 --- a/Mage.Sets/src/mage/cards/t/TheTwelfthDoctor.java +++ b/Mage.Sets/src/mage/cards/t/TheTwelfthDoctor.java @@ -99,12 +99,10 @@ enum FirstSpellCastFromNotHandEachTurnCondition implements Condition { @Override public boolean apply(Game game, Ability source) { - if (game.getStack().isEmpty()) { - return false; - } TheTwelfthDoctorWatcher watcher = game.getState().getWatcher(TheTwelfthDoctorWatcher.class); - StackObject so = game.getStack().getFirst(); - return watcher != null + StackObject so = game.getStack().getFirstOrNull(); + return so != null + && watcher != null && TheTwelfthDoctorWatcher.checkSpell(so, game); } } diff --git a/Mage.Sets/src/mage/cards/t/TheWarInHeaven.java b/Mage.Sets/src/mage/cards/t/TheWarInHeaven.java index b8e1ebd88f9..6f628a029f1 100644 --- a/Mage.Sets/src/mage/cards/t/TheWarInHeaven.java +++ b/Mage.Sets/src/mage/cards/t/TheWarInHeaven.java @@ -128,8 +128,8 @@ class TheWarInHeavenTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 8, game); } diff --git a/Mage.Sets/src/mage/cards/t/ThranPortal.java b/Mage.Sets/src/mage/cards/t/ThranPortal.java index 01dd82ddf01..fda95f376fd 100644 --- a/Mage.Sets/src/mage/cards/t/ThranPortal.java +++ b/Mage.Sets/src/mage/cards/t/ThranPortal.java @@ -7,10 +7,8 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.YouControlPermanentCondition; import mage.abilities.costs.common.PayLifeCost; import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ChooseBasicLandTypeEffect; -import mage.abilities.effects.common.continuous.AddChosenSubtypeEffect; -import mage.abilities.effects.common.cost.CostModificationEffectImpl; -import mage.abilities.effects.common.enterAttribute.EnterAttributeAddChosenSubtypeEffect; import mage.abilities.mana.*; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -21,6 +19,7 @@ import mage.game.Game; import mage.game.permanent.Permanent; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -47,16 +46,15 @@ public class ThranPortal extends CardImpl { this.addAbility(new EntersBattlefieldTappedUnlessAbility(condition).addHint(condition.getHint())); // As Thran Portal enters the battlefield, choose a basic land type. - // Thran Portal is the chosen type in addition to its other types. AsEntersBattlefieldAbility chooseLandTypeAbility = new AsEntersBattlefieldAbility(new ChooseBasicLandTypeEffect(Outcome.AddAbility)); - chooseLandTypeAbility.addEffect(new EnterAttributeAddChosenSubtypeEffect()); // While it enters + chooseLandTypeAbility.addEffect(new ThranPortalAddSubtypeEnteringEffect()); this.addAbility(chooseLandTypeAbility); - this.addAbility(new SimpleStaticAbility(new AddChosenSubtypeEffect())); // While on the battlefield + + // Thran Portal is the chosen type in addition to its other types. + this.addAbility(new SimpleStaticAbility(new ThranPortalManaAbilityContinuousEffect())); // Mana abilities of Thran Portal cost an additional 1 life to activate. - // This also adds the mana ability Ability ability = new SimpleStaticAbility(new ThranPortalAdditionalCostEffect()); - ability.addEffect(new ThranPortalManaAbilityContinousEffect()); this.addAbility(ability); } @@ -70,7 +68,34 @@ public class ThranPortal extends CardImpl { } } -class ThranPortalManaAbilityContinousEffect extends ContinuousEffectImpl { +class ThranPortalAddSubtypeEnteringEffect extends OneShotEffect { + + public ThranPortalAddSubtypeEnteringEffect() { + super(Outcome.Benefit); + } + + protected ThranPortalAddSubtypeEnteringEffect(final ThranPortalAddSubtypeEnteringEffect effect) { + super(effect); + } + + @Override + public ThranPortalAddSubtypeEnteringEffect copy() { + return new ThranPortalAddSubtypeEnteringEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent thranPortal = game.getPermanentEntering(source.getSourceId()); + SubType choice = SubType.byDescription((String) game.getState().getValue(source.getSourceId().toString() + ChooseBasicLandTypeEffect.VALUE_KEY)); + if (thranPortal != null && choice != null) { + thranPortal.addSubType(choice); + return true; + } + return false; + } +} + +class ThranPortalManaAbilityContinuousEffect extends ContinuousEffectImpl { private static final Map abilityMap = new HashMap() {{ put(SubType.PLAINS, new WhiteManaAbility()); @@ -80,18 +105,18 @@ class ThranPortalManaAbilityContinousEffect extends ContinuousEffectImpl { put(SubType.FOREST, new GreenManaAbility()); }}; - public ThranPortalManaAbilityContinousEffect() { + public ThranPortalManaAbilityContinuousEffect() { super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Neutral); - staticText = "mana abilities of {this} cost an additional 1 life to activate"; + staticText = "{this} is the chosen type in addition to its other types."; } - private ThranPortalManaAbilityContinousEffect(final ThranPortalManaAbilityContinousEffect effect) { + private ThranPortalManaAbilityContinuousEffect(final ThranPortalManaAbilityContinuousEffect effect) { super(effect); } @Override - public ThranPortalManaAbilityContinousEffect copy() { - return new ThranPortalManaAbilityContinousEffect(this); + public ThranPortalManaAbilityContinuousEffect copy() { + return new ThranPortalManaAbilityContinuousEffect(this); } @Override @@ -143,10 +168,11 @@ class ThranPortalManaAbilityContinousEffect extends ContinuousEffectImpl { } } -class ThranPortalAdditionalCostEffect extends CostModificationEffectImpl { +class ThranPortalAdditionalCostEffect extends ContinuousEffectImpl { ThranPortalAdditionalCostEffect() { - super(Duration.WhileOnBattlefield, Outcome.Benefit, CostModificationType.INCREASE_COST); + super(Duration.WhileOnBattlefield, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit); + staticText = "mana abilities of {this} cost an additional 1 life to activate"; } private ThranPortalAdditionalCostEffect(final ThranPortalAdditionalCostEffect effect) { @@ -159,17 +185,22 @@ class ThranPortalAdditionalCostEffect extends CostModificationEffectImpl { } @Override - public boolean apply(Game game, Ability source, Ability abilityToModify) { - abilityToModify.addCost(new PayLifeCost(1)); - return true; - } - - @Override - public boolean applies(Ability abilityToModify, Ability source, Game game) { - if (!abilityToModify.getSourceId().equals(source.getSourceId())) { + public boolean apply(Game game, Ability source) { + Permanent thranPortal = game.getPermanent(source.getSourceId()); + if (thranPortal == null) { return false; } - - return abilityToModify instanceof ManaAbility; + List abilities = thranPortal.getAbilities(game); + if (abilities.isEmpty()) { + return false; + } + boolean result = false; + for (Ability ability : abilities) { + if (ability.isManaAbility()) { + ability.addCost(new PayLifeCost(1)); + result = true; + } + } + return result; } } diff --git a/Mage.Sets/src/mage/cards/t/TocasiaDigSiteMentor.java b/Mage.Sets/src/mage/cards/t/TocasiaDigSiteMentor.java index 6c2c76bd899..51e50cd3a1e 100644 --- a/Mage.Sets/src/mage/cards/t/TocasiaDigSiteMentor.java +++ b/Mage.Sets/src/mage/cards/t/TocasiaDigSiteMentor.java @@ -93,8 +93,8 @@ class TocasiaDigSiteMentorTarget extends TargetCardInYourGraveyard { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - return super.canTarget(controllerId, id, source, game) + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) && CardUtil.checkCanTargetTotalValueLimit( this.getTargets(), id, MageObject::getManaValue, 10, game); } diff --git a/Mage.Sets/src/mage/cards/t/TromellSeymoursButler.java b/Mage.Sets/src/mage/cards/t/TromellSeymoursButler.java index 3194755eb18..ce1ea4d0d02 100644 --- a/Mage.Sets/src/mage/cards/t/TromellSeymoursButler.java +++ b/Mage.Sets/src/mage/cards/t/TromellSeymoursButler.java @@ -22,6 +22,7 @@ import mage.constants.SuperType; import mage.counters.CounterType; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.permanent.EnteredThisTurnPredicate; import mage.filter.predicate.permanent.TokenPredicate; import mage.game.Game; @@ -33,7 +34,7 @@ import java.util.UUID; */ public final class TromellSeymoursButler extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("nontoken creature"); + private static final FilterPermanent filter = new FilterCreaturePermanent("nontoken creature"); static { filter.add(TokenPredicate.FALSE); diff --git a/Mage.Sets/src/mage/cards/v/VATS.java b/Mage.Sets/src/mage/cards/v/VATS.java index ebf4b169f15..057affe65ab 100644 --- a/Mage.Sets/src/mage/cards/v/VATS.java +++ b/Mage.Sets/src/mage/cards/v/VATS.java @@ -62,8 +62,8 @@ class VATSTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } if (this.getTargets().isEmpty()) { diff --git a/Mage.Sets/src/mage/cards/v/ValiantRescuer.java b/Mage.Sets/src/mage/cards/v/ValiantRescuer.java index 6c1c956bf92..31a9b48f47c 100644 --- a/Mage.Sets/src/mage/cards/v/ValiantRescuer.java +++ b/Mage.Sets/src/mage/cards/v/ValiantRescuer.java @@ -74,12 +74,11 @@ class ValiantRescuerTriggeredAbility extends TriggeredAbilityImpl { ValiantRescuerWatcher watcher = game.getState().getWatcher(ValiantRescuerWatcher.class); if (watcher == null || !watcher.checkSpell(event.getPlayerId(), event.getSourceId()) - || game.getState().getStack().isEmpty() || !event.getPlayerId().equals(this.getControllerId()) || event.getSourceId().equals(this.getSourceId())) { return false; } - StackObject item = game.getState().getStack().getFirst(); + StackObject item = game.getState().getStack().getFirstOrNull(); return item instanceof StackAbility && item.getStackAbility() instanceof CyclingAbility; } @@ -106,11 +105,10 @@ class ValiantRescuerWatcher extends Watcher { @Override public void watch(GameEvent event, Game game) { - if (event.getType() != GameEvent.EventType.ACTIVATED_ABILITY - || game.getState().getStack().isEmpty()) { + if (event.getType() != GameEvent.EventType.ACTIVATED_ABILITY) { return; } - StackObject item = game.getState().getStack().getFirst(); + StackObject item = game.getState().getStack().getFirstOrNull(); if (item instanceof StackAbility && item.getStackAbility() instanceof CyclingAbility) { playerMap.computeIfAbsent(event.getPlayerId(), u -> new HashMap<>()); diff --git a/Mage.Sets/src/mage/cards/v/ValkiGodOfLies.java b/Mage.Sets/src/mage/cards/v/ValkiGodOfLies.java index 667cebb3c60..fb5f2ea73d9 100644 --- a/Mage.Sets/src/mage/cards/v/ValkiGodOfLies.java +++ b/Mage.Sets/src/mage/cards/v/ValkiGodOfLies.java @@ -123,8 +123,7 @@ class ValkiGodOfLiesRevealExileEffect extends OneShotEffect { TargetCard targetToExile = new TargetCard(Zone.HAND, StaticFilters.FILTER_CARD_CREATURE); targetToExile.withChooseHint("card to exile"); targetToExile.withNotTarget(true); - if (opponent.getHand().count(StaticFilters.FILTER_CARD_CREATURE, game) > 0 && - controller.choose(Outcome.Exile, opponent.getHand(), targetToExile, source, game)) { + if (controller.choose(Outcome.Exile, opponent.getHand(), targetToExile, source, game)) { Card targetedCardToExile = game.getCard(targetToExile.getFirstTarget()); if (targetedCardToExile != null && game.getState().getZone(source.getSourceId()) == Zone.BATTLEFIELD) { diff --git a/Mage.Sets/src/mage/cards/w/WeightOfConscience.java b/Mage.Sets/src/mage/cards/w/WeightOfConscience.java index 063c4dec9a6..222e01d0130 100644 --- a/Mage.Sets/src/mage/cards/w/WeightOfConscience.java +++ b/Mage.Sets/src/mage/cards/w/WeightOfConscience.java @@ -1,6 +1,5 @@ package mage.cards.w; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleStaticAbility; @@ -11,11 +10,13 @@ import mage.abilities.effects.common.combat.CantAttackAttachedEffect; import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.AttachmentType; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterControlledPermanent; -import mage.filter.predicate.Predicate; import mage.filter.predicate.mageobject.SharesCreatureTypePredicate; import mage.filter.predicate.permanent.TappedPredicate; import mage.game.Game; @@ -25,7 +26,10 @@ import mage.target.TargetPermanent; import mage.target.common.TargetControlledPermanent; import mage.target.common.TargetCreaturePermanent; -import java.util.*; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; /** * @author emerald000 @@ -66,7 +70,6 @@ class WeightOfConscienceTarget extends TargetControlledPermanent { static { filterUntapped.add(TappedPredicate.UNTAPPED); - filterUntapped.add(WeightOfConsciencePredicate.instance); } WeightOfConscienceTarget() { @@ -79,13 +82,15 @@ class WeightOfConscienceTarget extends TargetControlledPermanent { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + Set possibleTargets = new HashSet<>(); + Player player = game.getPlayer(sourceControllerId); - Set possibleTargets = new HashSet<>(0); if (player == null) { return possibleTargets; } - // Choosing first target + if (this.getTargets().isEmpty()) { + // choosing first target - use any permanent with shared types List permanentList = game.getBattlefield().getActivePermanents(filterUntapped, sourceControllerId, source, game); if (permanentList.size() < 2) { return possibleTargets; @@ -101,8 +106,8 @@ class WeightOfConscienceTarget extends TargetControlledPermanent { possibleTargets.add(permanent.getId()); } } - } // Choosing second target - else { + } else { + // choosing second target - must have shared type with first target Permanent firstTargetCreature = game.getPermanent(this.getFirstTarget()); if (firstTargetCreature == null) { return possibleTargets; @@ -115,50 +120,8 @@ class WeightOfConscienceTarget extends TargetControlledPermanent { } } } - return possibleTargets; - } - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - for (Permanent permanent1 : game.getBattlefield().getActivePermanents(filterUntapped, sourceControllerId, source, game)) { - for (Permanent permanent2 : game.getBattlefield().getActivePermanents(filterUntapped, sourceControllerId, source, game)) { - if (!Objects.equals(permanent1, permanent2) && permanent1.shareCreatureTypes(game, permanent2)) { - return true; - } - } - } - return false; - } - - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - if (!super.canTarget(id, game)) { - return false; - } - Permanent targetPermanent = game.getPermanent(id); - if (targetPermanent == null) { - return false; - } - if (this.getTargets().isEmpty()) { - List permanentList = game.getBattlefield().getActivePermanents(filterUntapped, source.getControllerId(), source, game); - if (permanentList.size() < 2) { - return false; - } - for (Permanent permanent : permanentList) { - if (permanent.isAllCreatureTypes(game)) { - return true; - } - FilterPermanent filter = filterUntapped.copy(); - filter.add(new SharesCreatureTypePredicate(permanent)); - if (game.getBattlefield().count(filter, source.getControllerId(), source, game) > 1) { - return true; - } - } - } else { - Permanent firstTarget = game.getPermanent(this.getTargets().get(0)); - return firstTarget != null && firstTarget.shareCreatureTypes(game, targetPermanent); - } - return false; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override @@ -166,17 +129,3 @@ class WeightOfConscienceTarget extends TargetControlledPermanent { return new WeightOfConscienceTarget(this); } } - -enum WeightOfConsciencePredicate implements Predicate { - instance; - - @Override - public boolean apply(MageObject input, Game game) { - return input.isAllCreatureTypes(game) - || input - .getSubtype(game) - .stream() - .map(SubType::getSubTypeSet) - .anyMatch(SubTypeSet.CreatureType::equals); - } -} diff --git a/Mage.Sets/src/mage/cards/w/WhimsOfTheFates.java b/Mage.Sets/src/mage/cards/w/WhimsOfTheFates.java index e246f1e5933..52d8f8d66df 100644 --- a/Mage.Sets/src/mage/cards/w/WhimsOfTheFates.java +++ b/Mage.Sets/src/mage/cards/w/WhimsOfTheFates.java @@ -175,20 +175,17 @@ class TargetSecondPilePermanent extends TargetPermanent { this.firstPile = firstPile; } - @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game, boolean flag) { - if (firstPile.contains(id)) { - return false; - } - return super.canTarget(controllerId, id, source, game, flag); + public TargetSecondPilePermanent(final TargetSecondPilePermanent target) { + super(target); + this.firstPile = new HashSet<>(target.firstPile); } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { if (firstPile.contains(id)) { return false; } - return super.canTarget(controllerId, id, source, game); + return super.canTarget(playerId, id, source, game); } @Override @@ -199,4 +196,8 @@ class TargetSecondPilePermanent extends TargetPermanent { return super.canTarget(id, source, game); } + @Override + public TargetSecondPilePermanent copy() { + return new TargetSecondPilePermanent(this); + } } diff --git a/Mage.Sets/src/mage/cards/w/WildMagicSorcerer.java b/Mage.Sets/src/mage/cards/w/WildMagicSorcerer.java index f91d13843b2..dc97b1e7937 100644 --- a/Mage.Sets/src/mage/cards/w/WildMagicSorcerer.java +++ b/Mage.Sets/src/mage/cards/w/WildMagicSorcerer.java @@ -2,7 +2,10 @@ package mage.cards.w; import mage.MageInt; import mage.MageObjectReference; +import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.keyword.CascadeAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -11,14 +14,12 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.game.stack.Spell; import mage.game.stack.StackObject; +import mage.players.Player; import mage.watchers.Watcher; + import java.util.HashMap; import java.util.Map; import java.util.UUID; -import mage.abilities.Ability; -import mage.abilities.condition.Condition; -import mage.abilities.effects.ContinuousEffectImpl; -import mage.players.Player; /** * @author TheElk801 @@ -35,7 +36,7 @@ public final class WildMagicSorcerer extends CardImpl { // The first spell you cast from exile each turn has cascade. this.addAbility(new SimpleStaticAbility( - new WildMagicSorcererGainCascadeFirstSpellCastFromExileEffect()), + new WildMagicSorcererGainCascadeFirstSpellCastFromExileEffect()), new WildMagicSorcererWatcher()); } @@ -96,12 +97,10 @@ enum FirstSpellCastFromExileEachTurnCondition implements Condition { @Override public boolean apply(Game game, Ability source) { - if (game.getStack().isEmpty()) { - return false; - } WildMagicSorcererWatcher watcher = game.getState().getWatcher(WildMagicSorcererWatcher.class); - StackObject so = game.getStack().getFirst(); - return watcher != null + StackObject so = game.getStack().getFirstOrNull(); + return so != null + && watcher != null && WildMagicSorcererWatcher.checkSpell(so, game); } } diff --git a/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java b/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java index 6f5f5a44238..ee9b54f81e4 100644 --- a/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java +++ b/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java @@ -1,34 +1,22 @@ package mage.cards.y; -import java.util.Optional; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.Cost; -import mage.abilities.costs.common.SacrificeTargetCost; import mage.abilities.assignment.common.SubTypeAssignment; +import mage.abilities.common.CantPayLifeOrSacrificeAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; import mage.cards.*; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.target.common.TargetCardInLibrary; import java.util.UUID; -import mage.MageObject; -import mage.abilities.costs.common.PayLifeCost; -import mage.abilities.costs.common.PayVariableLifeCost; -import mage.abilities.costs.common.SacrificeAllCost; -import mage.abilities.costs.common.SacrificeAttachedCost; -import mage.abilities.costs.common.SacrificeAttachmentCost; -import mage.abilities.costs.common.SacrificeSourceCost; -import mage.abilities.costs.common.SacrificeXTargetCost; -import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; -import mage.filter.Filter; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; /** * @author TheElk801 @@ -52,8 +40,7 @@ public final class YasharnImplacableEarth extends CardImpl { )); // Players can't pay life or sacrifice nonland permanents to cast spells or activate abilities. - Ability ability = new SimpleStaticAbility(new YasharnImplacableEarthEffect()); - this.addAbility(ability); + this.addAbility(new CantPayLifeOrSacrificeAbility(StaticFilters.FILTER_PERMANENTS_NON_LAND)); } private YasharnImplacableEarth(final YasharnImplacableEarth card) { @@ -111,122 +98,3 @@ class YasharnImplacableEarthTarget extends TargetCardInLibrary { return subTypeAssigner.getRoleCount(cards, game) >= cards.size(); } } - -class YasharnImplacableEarthEffect extends ContinuousRuleModifyingEffectImpl { - - YasharnImplacableEarthEffect() { - super(Duration.WhileOnBattlefield, Outcome.Neutral); - staticText = "Players can't pay life or sacrifice nonland permanents to cast spells or activate abilities"; - } - - private YasharnImplacableEarthEffect(final YasharnImplacableEarthEffect effect) { - super(effect); - } - - @Override - public YasharnImplacableEarthEffect copy() { - return new YasharnImplacableEarthEffect(this); - } - - @Override - public String getInfoMessage(Ability source, GameEvent event, Game game) { - MageObject mageObject = game.getObject(source); - if (mageObject != null) { - return "Players can't pay life or sacrifice nonland permanents to cast spells or activate abilities. (" + mageObject.getIdName() + ")."; - } - return null; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ACTIVATE_ABILITY - || event.getType() == GameEvent.EventType.CAST_SPELL; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId()); - if (event.getType() == GameEvent.EventType.ACTIVATE_ABILITY && permanent == null) { - return false; - } - - boolean canTargetLand = true; - Optional ability = game.getAbility(event.getTargetId(), event.getSourceId()); - if (!ability.isPresent()) { - return false; - } - - for (Cost cost : ability.get().getCosts()) { - if (cost instanceof PayLifeCost - || cost instanceof PayVariableLifeCost) { - return true; // can't pay with life - } - if (cost instanceof SacrificeSourceCost - && !permanent.isLand(game)) { - return true; - } - if (cost instanceof SacrificeTargetCost) { - SacrificeTargetCost sacrificeCost = (SacrificeTargetCost) cost; - Filter filter = sacrificeCost.getTargets().get(0).getFilter(); - for (Object predicate : filter.getPredicates()) { - if (predicate instanceof CardType.CardTypePredicate) { - if (!predicate.toString().equals("CardType(Land)")) { - canTargetLand = false; - } - } - } - return !canTargetLand; // must be nonland target - } - if (cost instanceof SacrificeAllCost) { - SacrificeAllCost sacrificeAllCost = (SacrificeAllCost) cost; - Filter filter = sacrificeAllCost.getTargets().get(0).getFilter(); - for (Object predicate : filter.getPredicates()) { - if (predicate instanceof CardType.CardTypePredicate) { - if (!predicate.toString().equals("CardType(Land)")) { - canTargetLand = false; - } - } - } - return !canTargetLand; // must be nonland target - } - if (cost instanceof SacrificeAttachedCost) { - SacrificeAttachedCost sacrificeAllCost = (SacrificeAttachedCost) cost; - Filter filter = sacrificeAllCost.getTargets().get(0).getFilter(); - for (Object predicate : filter.getPredicates()) { - if (predicate instanceof CardType.CardTypePredicate) { - if (!predicate.toString().equals("CardType(Land)")) { - canTargetLand = false; - } - } - } - return !canTargetLand; // must be nonland target - } - if (cost instanceof SacrificeAttachmentCost) { - SacrificeAttachmentCost sacrificeAllCost = (SacrificeAttachmentCost) cost; - Filter filter = sacrificeAllCost.getTargets().get(0).getFilter(); - for (Object predicate : filter.getPredicates()) { - if (predicate instanceof CardType.CardTypePredicate) { - if (!predicate.toString().equals("CardType(Land)")) { - canTargetLand = false; - } - } - } - return !canTargetLand; // must be nonland target - } - - if (cost instanceof SacrificeXTargetCost) { - SacrificeXTargetCost sacrificeCost = (SacrificeXTargetCost) cost; - Filter filter = sacrificeCost.getFilter(); - for (Object predicate : filter.getPredicates()) { - if (predicate instanceof CardType.CardTypePredicate) { - if (!predicate.toString().equals("CardType(Land)")) { - canTargetLand = false; - } - } - } - return !canTargetLand; // must be nonland target - } - } - return false; - } -} diff --git a/Mage.Sets/src/mage/cards/y/YorionSkyNomad.java b/Mage.Sets/src/mage/cards/y/YorionSkyNomad.java index c65013cc80d..f31607ad1d3 100644 --- a/Mage.Sets/src/mage/cards/y/YorionSkyNomad.java +++ b/Mage.Sets/src/mage/cards/y/YorionSkyNomad.java @@ -52,8 +52,7 @@ public final class YorionSkyNomad extends CardImpl { // Flying this.addAbility(FlyingAbility.getInstance()); - // When Yorion enters the battlefield, exile any number of other nonland permanents you own - // and control. Return those cards to the battlefield at the beginning of the next end step. + // When Yorion enters the battlefield, exile any number of other nonland permanents you own and control. Return those cards to the battlefield at the beginning of the next end step. this.addAbility(new EntersBattlefieldTriggeredAbility(new OneShotNonTargetEffect( new ExileReturnBattlefieldNextEndStepTargetEffect().setText(ruleText), new TargetPermanent(0, Integer.MAX_VALUE, filter, true)))); diff --git a/Mage.Sets/src/mage/cards/y/YoseiTheMorningStar.java b/Mage.Sets/src/mage/cards/y/YoseiTheMorningStar.java index dab9297934f..1393a2a67bf 100644 --- a/Mage.Sets/src/mage/cards/y/YoseiTheMorningStar.java +++ b/Mage.Sets/src/mage/cards/y/YoseiTheMorningStar.java @@ -69,12 +69,12 @@ class YoseiTheMorningStarTarget extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { Player player = game.getPlayer(source.getFirstTarget()); if (player != null) { this.filter = filterTemplate.copy(); this.filter.add(new ControllerIdPredicate(player.getId())); - return super.canTarget(controllerId, id, source, game); + return super.canTarget(playerId, id, source, game); } return false; } diff --git a/Mage.Sets/src/mage/cards/y/YueTheMoonSpirit.java b/Mage.Sets/src/mage/cards/y/YueTheMoonSpirit.java new file mode 100644 index 00000000000..903a06b5d59 --- /dev/null +++ b/Mage.Sets/src/mage/cards/y/YueTheMoonSpirit.java @@ -0,0 +1,61 @@ +package mage.cards.y; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.cost.CastFromHandForFreeEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.abilities.keyword.WaterbendAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.FilterCard; +import mage.filter.predicate.Predicates; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class YueTheMoonSpirit extends CardImpl { + + private static final FilterCard filter = new FilterCard("a noncreature spell"); + + static { + filter.add(Predicates.not(CardType.CREATURE.getPredicate())); + } + + public YueTheMoonSpirit(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.ALLY); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Waterbend {5}, {T}: You may cast a noncreature spell from your hand without paying its mana cost. + Ability ability = new WaterbendAbility(new CastFromHandForFreeEffect(filter), new GenericManaCost(5)); + ability.addCost(new TapSourceCost()); + this.addAbility(ability); + } + + private YueTheMoonSpirit(final YueTheMoonSpirit card) { + super(card); + } + + @Override + public YueTheMoonSpirit copy() { + return new YueTheMoonSpirit(this); + } +} diff --git a/Mage.Sets/src/mage/cards/y/YunasDecision.java b/Mage.Sets/src/mage/cards/y/YunasDecision.java index a65e84c1b05..c8b11ef013c 100644 --- a/Mage.Sets/src/mage/cards/y/YunasDecision.java +++ b/Mage.Sets/src/mage/cards/y/YunasDecision.java @@ -127,7 +127,7 @@ class YunasDecisionTarget extends TargetCardInHand { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, null, game)); + possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, source, game)); return possibleTargets; } } diff --git a/Mage.Sets/src/mage/cards/z/ZamWesell.java b/Mage.Sets/src/mage/cards/z/ZamWesell.java index 297c533e777..e2c5ad1166d 100644 --- a/Mage.Sets/src/mage/cards/z/ZamWesell.java +++ b/Mage.Sets/src/mage/cards/z/ZamWesell.java @@ -74,7 +74,7 @@ class ZamWesselEffect extends OneShotEffect { if (targetPlayer != null && you != null) { if (!targetPlayer.getHand().isEmpty()) { TargetCard target = new TargetCard(0, 1, Zone.HAND, StaticFilters.FILTER_CARD_CREATURE); - if (you.chooseTarget(Outcome.Benefit, targetPlayer.getHand(), target, source, game)) { + if (you.choose(Outcome.Benefit, targetPlayer.getHand(), target, source, game)) { Card card = game.getCard(target.getFirstTarget()); if (card != null) { ContinuousEffect copyEffect = new CopyEffect(Duration.EndOfGame, card.getMainCard(), source.getSourceId()); diff --git a/Mage.Sets/src/mage/cards/z/ZaraRenegadeRecruiter.java b/Mage.Sets/src/mage/cards/z/ZaraRenegadeRecruiter.java index 31d9612fb30..b1abd5885c2 100644 --- a/Mage.Sets/src/mage/cards/z/ZaraRenegadeRecruiter.java +++ b/Mage.Sets/src/mage/cards/z/ZaraRenegadeRecruiter.java @@ -18,6 +18,7 @@ import mage.filter.predicate.permanent.ControllerIdPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; +import mage.target.TargetCard; import mage.target.common.TargetCardInHand; import mage.target.common.TargetPlayerOrPlaneswalker; import mage.target.targetpointer.FixedTarget; @@ -83,10 +84,8 @@ class ZaraRenegadeRecruiterEffect extends OneShotEffect { if (controller == null || player == null || player.getHand().isEmpty()) { return false; } - TargetCardInHand targetCard = new TargetCardInHand( - 0, 1, StaticFilters.FILTER_CARD_CREATURE - ); - controller.choose(outcome, player.getHand(), targetCard, source, game); + TargetCard targetCard = new TargetCard(0, 1, Zone.HAND, StaticFilters.FILTER_CARD_CREATURE); + controller.choose(Outcome.Detriment, player.getHand(), targetCard, source, game); Card card = game.getCard(targetCard.getFirstTarget()); if (card == null) { return false; diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index 4cfe962a30a..856aa95c928 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -1,13 +1,18 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.constants.Rarity; import mage.constants.SetType; +import java.util.Arrays; +import java.util.List; + /** * @author TheElk801 */ public final class AvatarTheLastAirbender extends ExpansionSet { + private static final List unfinished = Arrays.asList("Aang's Iceberg", "Avatar Aang", "Aang, Master of Elements", "Katara, Water Tribe's Hope", "Yue, the Moon Spirit"); private static final AvatarTheLastAirbender instance = new AvatarTheLastAirbender(); public static AvatarTheLastAirbender getInstance() { @@ -18,6 +23,32 @@ public final class AvatarTheLastAirbender extends ExpansionSet { super("Avatar: The Last Airbender", "TLA", ExpansionSet.buildDate(2025, 11, 21), SetType.EXPANSION); this.blockName = "Avatar: The Last Airbender"; // for sorting in GUI this.rotationSet = true; - this.hasBasicLands = false; // temporary + this.hasBasicLands = true; + + cards.add(new SetCardInfo("Aang's Iceberg", 336, Rarity.RARE, mage.cards.a.AangsIceberg.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Aang's Iceberg", 5, Rarity.RARE, mage.cards.a.AangsIceberg.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Appa, Steadfast Guardian", 10, Rarity.MYTHIC, mage.cards.a.AppaSteadfastGuardian.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Appa, Steadfast Guardian", 316, Rarity.MYTHIC, mage.cards.a.AppaSteadfastGuardian.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Avatar Enthusiasts", 11, Rarity.COMMON, mage.cards.a.AvatarEnthusiasts.class)); + cards.add(new SetCardInfo("Earthbending Lesson", 176, Rarity.COMMON, mage.cards.e.EarthbendingLesson.class)); + cards.add(new SetCardInfo("Fire Nation Attacks", 133, Rarity.UNCOMMON, mage.cards.f.FireNationAttacks.class)); + cards.add(new SetCardInfo("Forest", 291, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Island", 288, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Katara, the Fearless", 230, Rarity.RARE, mage.cards.k.KataraTheFearless.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Katara, the Fearless", 350, Rarity.RARE, mage.cards.k.KataraTheFearless.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Katara, the Fearless", 361, Rarity.RARE, mage.cards.k.KataraTheFearless.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 290, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Plains", 287, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Redirect Lightning", 151, Rarity.RARE, mage.cards.r.RedirectLightning.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Redirect Lightning", 343, Rarity.RARE, mage.cards.r.RedirectLightning.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Sokka's Haiku", 71, Rarity.UNCOMMON, mage.cards.s.SokkasHaiku.class)); + cards.add(new SetCardInfo("Sokka, Bold Boomeranger", 240, Rarity.RARE, mage.cards.s.SokkaBoldBoomeranger.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Sokka, Bold Boomeranger", 383, Rarity.RARE, mage.cards.s.SokkaBoldBoomeranger.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Southern Air Temple", 36, Rarity.UNCOMMON, mage.cards.s.SouthernAirTemple.class)); + cards.add(new SetCardInfo("Swamp", 289, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Yue, the Moon Spirit", 338, Rarity.RARE, mage.cards.y.YueTheMoonSpirit.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Yue, the Moon Spirit", 83, Rarity.RARE, mage.cards.y.YueTheMoonSpirit.class, NON_FULL_USE_VARIOUS)); + + cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); } } diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java new file mode 100644 index 00000000000..9c5f7bae80e --- /dev/null +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java @@ -0,0 +1,26 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * @author TheElk801 + */ +public final class AvatarTheLastAirbenderEternal extends ExpansionSet { + + private static final AvatarTheLastAirbenderEternal instance = new AvatarTheLastAirbenderEternal(); + + public static AvatarTheLastAirbenderEternal getInstance() { + return instance; + } + + private AvatarTheLastAirbenderEternal() { + super("Avatar: The Last Airbender Eternal", "TLE", ExpansionSet.buildDate(2025, 11, 21), SetType.SUPPLEMENTAL); + this.blockName = "Avatar: The Last Airbender"; // for sorting in GUI + this.rotationSet = true; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Katara, Waterbending Master", 93, Rarity.MYTHIC, mage.cards.k.KataraWaterbendingMaster.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/CowboyBebop.java b/Mage.Sets/src/mage/sets/CowboyBebop.java new file mode 100644 index 00000000000..f87880484e0 --- /dev/null +++ b/Mage.Sets/src/mage/sets/CowboyBebop.java @@ -0,0 +1,29 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/pcbb + */ +public class CowboyBebop extends ExpansionSet { + + private static final CowboyBebop instance = new CowboyBebop(); + + public static CowboyBebop getInstance() { + return instance; + } + + private CowboyBebop() { + super("Cowboy Bebop", "PCBB", ExpansionSet.buildDate(2024, 8, 2), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Disdainful Stroke", 2, Rarity.RARE, mage.cards.d.DisdainfulStroke.class)); + cards.add(new SetCardInfo("Go for the Throat", 3, Rarity.RARE, mage.cards.g.GoForTheThroat.class)); + cards.add(new SetCardInfo("Lightning Strike", 4, Rarity.RARE, mage.cards.l.LightningStrike.class)); + cards.add(new SetCardInfo("Ossification", 1, Rarity.RARE, mage.cards.o.Ossification.class)); + cards.add(new SetCardInfo("Snakeskin Veil", 5, Rarity.RARE, mage.cards.s.SnakeskinVeil.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/EdgeOfEternitiesStellarSights.java b/Mage.Sets/src/mage/sets/EdgeOfEternitiesStellarSights.java index 5079f74d997..6a159997a4d 100644 --- a/Mage.Sets/src/mage/sets/EdgeOfEternitiesStellarSights.java +++ b/Mage.Sets/src/mage/sets/EdgeOfEternitiesStellarSights.java @@ -170,6 +170,7 @@ public final class EdgeOfEternitiesStellarSights extends ExpansionSet { cards.add(new SetCardInfo("Shambling Vent", 38, Rarity.RARE, mage.cards.s.ShamblingVent.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Shambling Vent", 83, Rarity.RARE, mage.cards.s.ShamblingVent.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Shambling Vent", 128, Rarity.RARE, mage.cards.s.ShamblingVent.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Shambling Vent", 173, Rarity.RARE, mage.cards.s.ShamblingVent.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Stirring Wildwood", 39, Rarity.RARE, mage.cards.s.StirringWildwood.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Stirring Wildwood", 84, Rarity.RARE, mage.cards.s.StirringWildwood.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Stirring Wildwood", 129, Rarity.RARE, mage.cards.s.StirringWildwood.class, NON_FULL_USE_VARIOUS)); @@ -177,7 +178,6 @@ public final class EdgeOfEternitiesStellarSights extends ExpansionSet { cards.add(new SetCardInfo("Strip Mine", 40, Rarity.MYTHIC, mage.cards.s.StripMine.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Strip Mine", 85, Rarity.MYTHIC, mage.cards.s.StripMine.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Strip Mine", 130, Rarity.MYTHIC, mage.cards.s.StripMine.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Strip Mine", 173, Rarity.MYTHIC, mage.cards.s.StripMine.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Strip Mine", 175, Rarity.MYTHIC, mage.cards.s.StripMine.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sunken Citadel", 41, Rarity.RARE, mage.cards.s.SunkenCitadel.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sunken Citadel", 86, Rarity.RARE, mage.cards.s.SunkenCitadel.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Sets/src/mage/sets/FINStandardShowdown.java b/Mage.Sets/src/mage/sets/FINStandardShowdown.java new file mode 100644 index 00000000000..91d220266fd --- /dev/null +++ b/Mage.Sets/src/mage/sets/FINStandardShowdown.java @@ -0,0 +1,26 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/pss5 + */ +public class FINStandardShowdown extends ExpansionSet { + + private static final FINStandardShowdown instance = new FINStandardShowdown(); + + public static FINStandardShowdown getInstance() { + return instance; + } + + private FINStandardShowdown() { + super("FIN Standard Showdown", "PSS5", ExpansionSet.buildDate(2025, 6, 13), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Squall, SeeD Mercenary", 2, Rarity.RARE, mage.cards.s.SquallSeeDMercenary.class)); + cards.add(new SetCardInfo("Ultima", 1, Rarity.RARE, mage.cards.u.Ultima.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/LoveYourLGS2025.java b/Mage.Sets/src/mage/sets/LoveYourLGS2025.java new file mode 100644 index 00000000000..5e764d71467 --- /dev/null +++ b/Mage.Sets/src/mage/sets/LoveYourLGS2025.java @@ -0,0 +1,26 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/plg25 + */ +public class LoveYourLGS2025 extends ExpansionSet { + + private static final LoveYourLGS2025 instance = new LoveYourLGS2025(); + + public static LoveYourLGS2025 getInstance() { + return instance; + } + + private LoveYourLGS2025() { + super("Love Your LGS 2025", "PLG25", ExpansionSet.buildDate(2025, 3, 26), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Chromatic Lantern", 1, Rarity.RARE, mage.cards.c.ChromaticLantern.class)); + cards.add(new SetCardInfo("Wastes", 2, Rarity.RARE, mage.cards.w.Wastes.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/MagicFest2025.java b/Mage.Sets/src/mage/sets/MagicFest2025.java index cbcac3efb35..072c4c22240 100644 --- a/Mage.Sets/src/mage/sets/MagicFest2025.java +++ b/Mage.Sets/src/mage/sets/MagicFest2025.java @@ -25,6 +25,12 @@ public class MagicFest2025 extends ExpansionSet { cards.add(new SetCardInfo("Ponder", 2, Rarity.RARE, mage.cards.p.Ponder.class)); cards.add(new SetCardInfo("Serra the Benevolent", 1, Rarity.MYTHIC, mage.cards.s.SerraTheBenevolent.class, RETRO_ART)); cards.add(new SetCardInfo("The First Sliver", 3, Rarity.MYTHIC, mage.cards.t.TheFirstSliver.class)); - cards.add(new SetCardInfo("Yoshimaru, Ever Faithful", 4, Rarity.MYTHIC, mage.cards.y.YoshimaruEverFaithful.class)); + cards.add(new SetCardInfo("Yoshimaru, Ever Faithful", 5, Rarity.MYTHIC, mage.cards.y.YoshimaruEverFaithful.class)); + cards.add(new SetCardInfo("Ugin, the Spirit Dragon", 6, Rarity.MYTHIC, mage.cards.u.UginTheSpiritDragon.class, RETRO_ART)); + cards.add(new SetCardInfo("Sliver Hive", 7, Rarity.RARE, mage.cards.s.SliverHive.class, RETRO_ART)); + cards.add(new SetCardInfo("Tifa Lockhart", 9, Rarity.RARE, mage.cards.t.TifaLockhart.class)); + cards.add(new SetCardInfo("Arcane Signet", 10, Rarity.RARE, mage.cards.a.ArcaneSignet.class)); + cards.add(new SetCardInfo("Rograkh, Son of Rohgahh", 11, Rarity.RARE, mage.cards.r.RograkhSonOfRohgahh.class)); + cards.add(new SetCardInfo("Swords to Plowshares", 13, Rarity.RARE, mage.cards.s.SwordsToPlowshares.class)); } } diff --git a/Mage.Sets/src/mage/sets/MagicOnlinePromos.java b/Mage.Sets/src/mage/sets/MagicOnlinePromos.java index ff038e56ea6..c58cb67c6a9 100644 --- a/Mage.Sets/src/mage/sets/MagicOnlinePromos.java +++ b/Mage.Sets/src/mage/sets/MagicOnlinePromos.java @@ -1209,7 +1209,7 @@ public class MagicOnlinePromos extends ExpansionSet { cards.add(new SetCardInfo("Helm of Kaldra", 31989, Rarity.RARE, mage.cards.h.HelmOfKaldra.class)); cards.add(new SetCardInfo("Helm of Obedience", 65642, Rarity.RARE, mage.cards.h.HelmOfObedience.class)); cards.add(new SetCardInfo("Hengegate Pathway", 88408, Rarity.RARE, mage.cards.h.HengegatePathway.class)); - cards.add(new SetCardInfo("Henzie Toolbox Torre", 99789, Rarity.MYTHIC, mage.cards.h.HenzieToolboxTorre.class)); + cards.add(new SetCardInfo("Henzie \"Toolbox\" Torre", 99789, Rarity.MYTHIC, mage.cards.h.HenzieToolboxTorre.class)); cards.add(new SetCardInfo("Herd Migration", 103470, Rarity.RARE, mage.cards.h.HerdMigration.class)); cards.add(new SetCardInfo("Hermit Druid", 36080, Rarity.RARE, mage.cards.h.HermitDruid.class)); cards.add(new SetCardInfo("Hero of Bladehold", 39646, Rarity.MYTHIC, mage.cards.h.HeroOfBladehold.class)); @@ -1486,7 +1486,7 @@ public class MagicOnlinePromos extends ExpansionSet { cards.add(new SetCardInfo("Kolvori, God of Kinship", 88348, Rarity.RARE, mage.cards.k.KolvoriGodOfKinship.class)); cards.add(new SetCardInfo("Koma, Cosmos Serpent", 88356, Rarity.MYTHIC, mage.cards.k.KomaCosmosSerpent.class)); cards.add(new SetCardInfo("Komainu Battle Armor", 97995, Rarity.RARE, mage.cards.k.KomainuBattleArmor.class)); - cards.add(new SetCardInfo("Kongming, Sleeping Dragon", 33442, Rarity.RARE, mage.cards.k.KongmingSleepingDragon.class, RETRO_ART)); + cards.add(new SetCardInfo("Kongming, \"Sleeping Dragon\"", 33442, Rarity.RARE, mage.cards.k.KongmingSleepingDragon.class, RETRO_ART)); cards.add(new SetCardInfo("Kor Duelist", 36212, Rarity.UNCOMMON, mage.cards.k.KorDuelist.class)); cards.add(new SetCardInfo("Kor Firewalker", 43574, Rarity.UNCOMMON, mage.cards.k.KorFirewalker.class)); cards.add(new SetCardInfo("Kor Skyfisher", 43548, Rarity.COMMON, mage.cards.k.KorSkyfisher.class)); diff --git a/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java b/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java index 746e298113e..f2d899ed330 100644 --- a/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java +++ b/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java @@ -40,10 +40,12 @@ public final class MarvelsSpiderMan extends ExpansionSet { cards.add(new SetCardInfo("Guy in the Chair", 102, Rarity.COMMON, mage.cards.g.GuyInTheChair.class)); cards.add(new SetCardInfo("Iron Spider, Stark Upgrade", 166, Rarity.RARE, mage.cards.i.IronSpiderStarkUpgrade.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Iron Spider, Stark Upgrade", 279, Rarity.RARE, mage.cards.i.IronSpiderStarkUpgrade.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Island", 190, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Island", 190, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Island", 195, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Kapow!", 103, Rarity.COMMON, mage.cards.k.Kapow.class)); cards.add(new SetCardInfo("Kraven's Cats", 104, Rarity.COMMON, mage.cards.k.KravensCats.class)); + cards.add(new SetCardInfo("Kraven's Last Hunt", 105, Rarity.RARE, mage.cards.k.KravensLastHunt.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Kraven's Last Hunt", 226, Rarity.RARE, mage.cards.k.KravensLastHunt.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Lurking Lizards", 107, Rarity.COMMON, mage.cards.l.LurkingLizards.class)); cards.add(new SetCardInfo("Masked Meower", 82, Rarity.COMMON, mage.cards.m.MaskedMeower.class)); cards.add(new SetCardInfo("Mechanical Mobster", 168, Rarity.COMMON, mage.cards.m.MechanicalMobster.class)); diff --git a/Mage.Sets/src/mage/sets/MastersEditionIII.java b/Mage.Sets/src/mage/sets/MastersEditionIII.java index 1743064f236..6a9b8b9978a 100644 --- a/Mage.Sets/src/mage/sets/MastersEditionIII.java +++ b/Mage.Sets/src/mage/sets/MastersEditionIII.java @@ -140,7 +140,7 @@ public final class MastersEditionIII extends ExpansionSet { cards.add(new SetCardInfo("Kobold Overlord", 105, Rarity.UNCOMMON, mage.cards.k.KoboldOverlord.class, RETRO_ART)); cards.add(new SetCardInfo("Kobold Taskmaster", 106, Rarity.COMMON, mage.cards.k.KoboldTaskmaster.class, RETRO_ART)); cards.add(new SetCardInfo("Kobolds of Kher Keep", 107, Rarity.COMMON, mage.cards.k.KoboldsOfKherKeep.class, RETRO_ART)); - cards.add(new SetCardInfo("Kongming, Sleeping Dragon", 16, Rarity.RARE, mage.cards.k.KongmingSleepingDragon.class, RETRO_ART)); + cards.add(new SetCardInfo("Kongming, \"Sleeping Dragon\"", 16, Rarity.RARE, mage.cards.k.KongmingSleepingDragon.class, RETRO_ART)); cards.add(new SetCardInfo("Labyrinth Minotaur", 39, Rarity.COMMON, mage.cards.l.LabyrinthMinotaur.class, RETRO_ART)); cards.add(new SetCardInfo("Lady Caleria", 157, Rarity.UNCOMMON, mage.cards.l.LadyCaleria.class, RETRO_ART)); cards.add(new SetCardInfo("Lady Evangela", 158, Rarity.UNCOMMON, mage.cards.l.LadyEvangela.class, RETRO_ART)); diff --git a/Mage.Sets/src/mage/sets/MediaAndCollaborationPromos.java b/Mage.Sets/src/mage/sets/MediaAndCollaborationPromos.java index b655cb8f7fc..04b60e4a656 100644 --- a/Mage.Sets/src/mage/sets/MediaAndCollaborationPromos.java +++ b/Mage.Sets/src/mage/sets/MediaAndCollaborationPromos.java @@ -24,6 +24,7 @@ public class MediaAndCollaborationPromos extends ExpansionSet { cards.add(new SetCardInfo("Ajani, Mentor of Heroes", "2024-3", Rarity.MYTHIC, mage.cards.a.AjaniMentorOfHeroes.class)); cards.add(new SetCardInfo("Ancestral Mask", "2025-2", Rarity.RARE, mage.cards.a.AncestralMask.class)); + cards.add(new SetCardInfo("Anti-Venom, Horrifying Healer", "2025-15", Rarity.MYTHIC, mage.cards.a.AntiVenomHorrifyingHealer.class)); cards.add(new SetCardInfo("Archangel", "2000-4", Rarity.RARE, mage.cards.a.Archangel.class, RETRO_ART)); cards.add(new SetCardInfo("Ascendant Evincar", "2000-7", Rarity.RARE, mage.cards.a.AscendantEvincar.class, RETRO_ART)); cards.add(new SetCardInfo("Avalanche Riders", "2023-5", Rarity.RARE, mage.cards.a.AvalancheRiders.class)); @@ -32,6 +33,7 @@ public class MediaAndCollaborationPromos extends ExpansionSet { cards.add(new SetCardInfo("Cast Down", "2019-1", Rarity.UNCOMMON, mage.cards.c.CastDown.class)); cards.add(new SetCardInfo("Chandra's Outrage", "2010-3", Rarity.COMMON, mage.cards.c.ChandrasOutrage.class)); cards.add(new SetCardInfo("Chandra's Spitfire", "2010-4", Rarity.UNCOMMON, mage.cards.c.ChandrasSpitfire.class)); + cards.add(new SetCardInfo("Cloud, Planet's Champion", "2025-13", Rarity.MYTHIC, mage.cards.c.CloudPlanetsChampion.class)); cards.add(new SetCardInfo("Counterspell", "2021-1", Rarity.RARE, mage.cards.c.Counterspell.class)); cards.add(new SetCardInfo("Crop Rotation", "2020-7", Rarity.RARE, mage.cards.c.CropRotation.class)); cards.add(new SetCardInfo("Culling the Weak", "2023-8", Rarity.RARE, mage.cards.c.CullingTheWeak.class)); @@ -39,22 +41,27 @@ public class MediaAndCollaborationPromos extends ExpansionSet { cards.add(new SetCardInfo("Dark Ritual", "2020-4", Rarity.RARE, mage.cards.d.DarkRitual.class)); cards.add(new SetCardInfo("Darksteel Juggernaut", "2010-1", Rarity.RARE, mage.cards.d.DarksteelJuggernaut.class)); cards.add(new SetCardInfo("Daxos, Blessed by the Sun", "2020-2", Rarity.UNCOMMON, mage.cards.d.DaxosBlessedByTheSun.class)); - cards.add(new SetCardInfo("Diabolic Edict", "2024-5", Rarity.RARE, mage.cards.d.DiabolicEdict.class)); + cards.add(new SetCardInfo("Diabolic Edict", "2019-2", Rarity.RARE, mage.cards.d.DiabolicEdict.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Diabolic Edict", "2024-5", Rarity.RARE, mage.cards.d.DiabolicEdict.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Disenchant", "2022-1", Rarity.RARE, mage.cards.d.Disenchant.class)); - cards.add(new SetCardInfo("Duress", "2025-7", Rarity.RARE, mage.cards.d.Duress.class)); + cards.add(new SetCardInfo("Duress", "2019-6", Rarity.RARE, mage.cards.d.Duress.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Duress", "2025-7", Rarity.RARE, mage.cards.d.Duress.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Fireball", "1995-1", Rarity.COMMON, mage.cards.f.Fireball.class, RETRO_ART)); cards.add(new SetCardInfo("Frantic Search", "2022-4", Rarity.RARE, mage.cards.f.FranticSearch.class)); cards.add(new SetCardInfo("Gingerbrute", "2023-3", Rarity.RARE, mage.cards.g.Gingerbrute.class, RETRO_ART)); cards.add(new SetCardInfo("Gush", "2024-4", Rarity.RARE, mage.cards.g.Gush.class)); cards.add(new SetCardInfo("Harald, King of Skemfar", "2021-3", Rarity.RARE, mage.cards.h.HaraldKingOfSkemfar.class)); cards.add(new SetCardInfo("Heliod's Pilgrim", "2020-6", Rarity.RARE, mage.cards.h.HeliodsPilgrim.class)); + cards.add(new SetCardInfo("Huntmaster of the Fells", "2025-17", Rarity.RARE, mage.cards.h.HuntmasterOfTheFells.class)); cards.add(new SetCardInfo("Hypnotic Sprite", "2019-5", Rarity.RARE, mage.cards.h.HypnoticSprite.class)); + cards.add(new SetCardInfo("Iron Spider, Stark Upgrade", "2025-18", Rarity.RARE, mage.cards.i.IronSpiderStarkUpgrade.class)); cards.add(new SetCardInfo("Jace Beleren", "2009-1", Rarity.MYTHIC, mage.cards.j.JaceBeleren.class)); cards.add(new SetCardInfo("Jace, Memory Adept", "2024-2", Rarity.MYTHIC, mage.cards.j.JaceMemoryAdept.class)); cards.add(new SetCardInfo("Jamuraan Lion", "1996-3", Rarity.COMMON, mage.cards.j.JamuraanLion.class, RETRO_ART)); cards.add(new SetCardInfo("Kuldotha Phoenix", "2010-5", Rarity.RARE, mage.cards.k.KuldothaPhoenix.class)); cards.add(new SetCardInfo("Lava Coil", "2019-4", Rarity.UNCOMMON, mage.cards.l.LavaCoil.class)); cards.add(new SetCardInfo("Lightning Hounds", "2000-1", Rarity.COMMON, mage.cards.l.LightningHounds.class, RETRO_ART)); + cards.add(new SetCardInfo("Lightning, Security Sergeant", "2025-12", Rarity.RARE, mage.cards.l.LightningSecuritySergeant.class)); cards.add(new SetCardInfo("Liliana of the Dark Realms", "2024-8", Rarity.MYTHIC, mage.cards.l.LilianaOfTheDarkRealms.class)); cards.add(new SetCardInfo("Mental Misstep", "2023-1", Rarity.RARE, mage.cards.m.MentalMisstep.class)); cards.add(new SetCardInfo("Nicol Bolas, Planeswalker", "2025-10", Rarity.MYTHIC, mage.cards.n.NicolBolasPlaneswalker.class)); @@ -63,15 +70,19 @@ public class MediaAndCollaborationPromos extends ExpansionSet { cards.add(new SetCardInfo("Phantasmal Dragon", "2011-1", Rarity.UNCOMMON, mage.cards.p.PhantasmalDragon.class)); cards.add(new SetCardInfo("Phyrexian Rager", "2000-5", Rarity.COMMON, mage.cards.p.PhyrexianRager.class, RETRO_ART)); cards.add(new SetCardInfo("Pyromancer's Gauntlet", "2023-6", Rarity.RARE, mage.cards.p.PyromancersGauntlet.class, RETRO_ART)); + cards.add(new SetCardInfo("Ravager of the Fells", "2025-17", Rarity.RARE, mage.cards.r.RavagerOfTheFells.class)); cards.add(new SetCardInfo("Ruin Crab", "2023-4", Rarity.RARE, mage.cards.r.RuinCrab.class, RETRO_ART)); cards.add(new SetCardInfo("Sandbar Crocodile", "1996-1", Rarity.COMMON, mage.cards.s.SandbarCrocodile.class, RETRO_ART)); cards.add(new SetCardInfo("Scent of Cinder", "1999-1", Rarity.COMMON, mage.cards.s.ScentOfCinder.class, RETRO_ART)); + cards.add(new SetCardInfo("Sephiroth, Planet's Heir", "2025-14", Rarity.MYTHIC, mage.cards.s.SephirothPlanetsHeir.class)); cards.add(new SetCardInfo("Shield Wall", "1997-3", Rarity.COMMON, mage.cards.s.ShieldWall.class, RETRO_ART)); cards.add(new SetCardInfo("Shivan Dragon", "2001-2", Rarity.RARE, mage.cards.s.ShivanDragon.class, RETRO_ART)); - cards.add(new SetCardInfo("Shock", "2025-1", Rarity.RARE, mage.cards.s.Shock.class)); + cards.add(new SetCardInfo("Shock", "2019-3", Rarity.RARE, mage.cards.s.Shock.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Shock", "2025-1", Rarity.RARE, mage.cards.s.Shock.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Shrieking Drake", "1997-2", Rarity.COMMON, mage.cards.s.ShriekingDrake.class, RETRO_ART)); cards.add(new SetCardInfo("Silver Drake", "2000-2", Rarity.COMMON, mage.cards.s.SilverDrake.class, RETRO_ART)); cards.add(new SetCardInfo("Snuff Out", "2024-1", Rarity.RARE, mage.cards.s.SnuffOut.class)); + cards.add(new SetCardInfo("Spectacular Spider-Man", "2025-16", Rarity.RARE, mage.cards.s.SpectacularSpiderMan.class)); cards.add(new SetCardInfo("Spined Wurm", "2001-1", Rarity.COMMON, mage.cards.s.SpinedWurm.class, RETRO_ART)); cards.add(new SetCardInfo("Sprite Dragon", "2020-5", Rarity.RARE, mage.cards.s.SpriteDragon.class)); cards.add(new SetCardInfo("Staggering Insight", "2020-3", Rarity.RARE, mage.cards.s.StaggeringInsight.class)); @@ -79,8 +90,10 @@ public class MediaAndCollaborationPromos extends ExpansionSet { cards.add(new SetCardInfo("Talruum Champion", "1997-1", Rarity.COMMON, mage.cards.t.TalruumChampion.class, RETRO_ART)); cards.add(new SetCardInfo("Tangled Florahedron", "2020-8", Rarity.UNCOMMON, mage.cards.t.TangledFlorahedron.class)); cards.add(new SetCardInfo("Thorn Elemental", "2000-3", Rarity.RARE, mage.cards.t.ThornElemental.class, RETRO_ART)); + cards.add(new SetCardInfo("Ultimecia, Temporal Threat", "2025-11", Rarity.RARE, mage.cards.u.UltimeciaTemporalThreat.class)); cards.add(new SetCardInfo("Usher of the Fallen", "2022-3", Rarity.RARE, mage.cards.u.UsherOfTheFallen.class)); - cards.add(new SetCardInfo("Voltaic Key", "2024-6", Rarity.RARE, mage.cards.v.VoltaicKey.class)); + cards.add(new SetCardInfo("Voltaic Key", "2020-1", Rarity.RARE, mage.cards.v.VoltaicKey.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Voltaic Key", "2024-6", Rarity.RARE, mage.cards.v.VoltaicKey.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Warmonger", "1999-2", Rarity.UNCOMMON, mage.cards.w.Warmonger.class, RETRO_ART)); cards.add(new SetCardInfo("Wild Growth", "2022-2", Rarity.RARE, mage.cards.w.WildGrowth.class)); cards.add(new SetCardInfo("Winged Boots", "2023-7", Rarity.RARE, mage.cards.w.WingedBoots.class)); diff --git a/Mage.Sets/src/mage/sets/PortalThreeKingdoms.java b/Mage.Sets/src/mage/sets/PortalThreeKingdoms.java index f1f31081167..bdc621429af 100644 --- a/Mage.Sets/src/mage/sets/PortalThreeKingdoms.java +++ b/Mage.Sets/src/mage/sets/PortalThreeKingdoms.java @@ -85,7 +85,7 @@ public final class PortalThreeKingdoms extends ExpansionSet { cards.add(new SetCardInfo("Island", 170, Rarity.LAND, mage.cards.basiclands.Island.class, RETRO_ART_USE_VARIOUS)); cards.add(new SetCardInfo("Island", 171, Rarity.LAND, mage.cards.basiclands.Island.class, RETRO_ART_USE_VARIOUS)); cards.add(new SetCardInfo("Kongming's Contraptions", 10, Rarity.RARE, mage.cards.k.KongmingsContraptions.class, RETRO_ART)); - cards.add(new SetCardInfo("Kongming, Sleeping Dragon", 9, Rarity.RARE, mage.cards.k.KongmingSleepingDragon.class, RETRO_ART)); + cards.add(new SetCardInfo("Kongming, \"Sleeping Dragon\"", 9, Rarity.RARE, mage.cards.k.KongmingSleepingDragon.class, RETRO_ART)); cards.add(new SetCardInfo("Lady Sun", 45, Rarity.RARE, mage.cards.l.LadySun.class, RETRO_ART)); cards.add(new SetCardInfo("Lady Zhurong, Warrior Queen", 139, Rarity.RARE, mage.cards.l.LadyZhurongWarriorQueen.class, RETRO_ART)); cards.add(new SetCardInfo("Liu Bei, Lord of Shu", 11, Rarity.RARE, mage.cards.l.LiuBeiLordOfShu.class, RETRO_ART)); @@ -106,7 +106,7 @@ public final class PortalThreeKingdoms extends ExpansionSet { cards.add(new SetCardInfo("Mountain Bandit", 117, Rarity.COMMON, mage.cards.m.MountainBandit.class, RETRO_ART)); cards.add(new SetCardInfo("Mystic Denial", 49, Rarity.UNCOMMON, mage.cards.m.MysticDenial.class, RETRO_ART)); cards.add(new SetCardInfo("Overwhelming Forces", 79, Rarity.RARE, mage.cards.o.OverwhelmingForces.class, RETRO_ART)); - cards.add(new SetCardInfo("Pang Tong, Young Phoenix", 14, Rarity.RARE, mage.cards.p.PangTongYoungPhoenix.class, RETRO_ART)); + cards.add(new SetCardInfo("Pang Tong, \"Young Phoenix\"", 14, Rarity.RARE, mage.cards.p.PangTongYoungPhoenix.class, RETRO_ART)); cards.add(new SetCardInfo("Peach Garden Oath", 15, Rarity.UNCOMMON, mage.cards.p.PeachGardenOath.class, RETRO_ART)); cards.add(new SetCardInfo("Plains", 166, Rarity.LAND, mage.cards.basiclands.Plains.class, RETRO_ART_USE_VARIOUS)); cards.add(new SetCardInfo("Plains", 167, Rarity.LAND, mage.cards.basiclands.Plains.class, RETRO_ART_USE_VARIOUS)); diff --git a/Mage.Sets/src/mage/sets/ProTourPromos.java b/Mage.Sets/src/mage/sets/ProTourPromos.java index 5f3ba1eb777..2ae9e4e7546 100644 --- a/Mage.Sets/src/mage/sets/ProTourPromos.java +++ b/Mage.Sets/src/mage/sets/ProTourPromos.java @@ -27,10 +27,12 @@ public final class ProTourPromos extends ExpansionSet { * https://github.com/magefree/mage/pull/6190#issuecomment-582353697 * https://github.com/magefree/mage/pull/6190#issuecomment-582354790 */ + cards.add(new SetCardInfo("Aerith Gainsborough", "2025-3", Rarity.RARE, mage.cards.a.AerithGainsborough.class)); cards.add(new SetCardInfo("Aether Vial", "2020-3", Rarity.RARE, mage.cards.a.AetherVial.class)); cards.add(new SetCardInfo("Ajani Goldmane", 2011, Rarity.MYTHIC, mage.cards.a.AjaniGoldmane.class)); cards.add(new SetCardInfo("Arcbound Ravager", 2019, Rarity.RARE, mage.cards.a.ArcboundRavager.class)); cards.add(new SetCardInfo("Avatar of Woe", 2010, Rarity.RARE, mage.cards.a.AvatarOfWoe.class)); + cards.add(new SetCardInfo("Cloud, Midgar Mercenary", "2025-1", Rarity.MYTHIC, mage.cards.c.CloudMidgarMercenary.class)); cards.add(new SetCardInfo("Cryptic Command", "2020-1", Rarity.RARE, mage.cards.c.CrypticCommand.class)); cards.add(new SetCardInfo("Emrakul, the Aeons Torn", 2017, Rarity.MYTHIC, mage.cards.e.EmrakulTheAeonsTorn.class)); cards.add(new SetCardInfo("Eternal Dragon", 2007, Rarity.RARE, mage.cards.e.EternalDragon.class)); diff --git a/Mage.Sets/src/mage/sets/SecretLairDrop.java b/Mage.Sets/src/mage/sets/SecretLairDrop.java index 8bc16a19e10..e1b5bf885e5 100644 --- a/Mage.Sets/src/mage/sets/SecretLairDrop.java +++ b/Mage.Sets/src/mage/sets/SecretLairDrop.java @@ -1952,8 +1952,6 @@ public class SecretLairDrop extends ExpansionSet { cards.add(new SetCardInfo("Reckoner Bankbuster", "1967b", Rarity.RARE, mage.cards.r.ReckonerBankbuster.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Smuggler's Copter", 1968, Rarity.RARE, mage.cards.s.SmugglersCopter.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Smuggler's Copter", "1968b", Rarity.RARE, mage.cards.s.SmugglersCopter.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Mechtitan Core", 1969, Rarity.COMMON, mage.cards.m.MechtitanCore.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Mechtitan Core", "1969b", Rarity.COMMON, mage.cards.m.MechtitanCore.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Dragonlord Atarka", 1970, Rarity.MYTHIC, mage.cards.d.DragonlordAtarka.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Dragonlord Atarka", "1970b", Rarity.MYTHIC, mage.cards.d.DragonlordAtarka.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Dragonlord Dromoka", 1971, Rarity.MYTHIC, mage.cards.d.DragonlordDromoka.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Sets/src/mage/sets/SpotlightSeries.java b/Mage.Sets/src/mage/sets/SpotlightSeries.java new file mode 100644 index 00000000000..92d42cff327 --- /dev/null +++ b/Mage.Sets/src/mage/sets/SpotlightSeries.java @@ -0,0 +1,32 @@ + +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/pspl + * @author ReSech + */ +public final class SpotlightSeries extends ExpansionSet { + + private static final SpotlightSeries instance = new SpotlightSeries(); + + public static SpotlightSeries getInstance() { + return instance; + } + + private SpotlightSeries() { + super("Spotlight Series", "PSPL", ExpansionSet.buildDate(2025, 1, 3), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Cloud, Midgar Mercenary", 5, Rarity.MYTHIC, mage.cards.c.CloudMidgarMercenary.class)); + cards.add(new SetCardInfo("Get Lost", 6, Rarity.RARE, mage.cards.g.GetLost.class)); + cards.add(new SetCardInfo("Kaldra Compleat", 2, Rarity.MYTHIC, mage.cards.k.KaldraCompleat.class)); + cards.add(new SetCardInfo("Sword of Forge and Frontier", 3, Rarity.MYTHIC, mage.cards.s.SwordOfForgeAndFrontier.class)); + cards.add(new SetCardInfo("Terror of the Peaks", 1, Rarity.RARE, mage.cards.t.TerrorOfThePeaks.class)); + } + +} diff --git a/Mage.Sets/src/mage/sets/Starter1999.java b/Mage.Sets/src/mage/sets/Starter1999.java index 05b8fa121e8..923d2a20aef 100644 --- a/Mage.Sets/src/mage/sets/Starter1999.java +++ b/Mage.Sets/src/mage/sets/Starter1999.java @@ -5,7 +5,6 @@ import mage.constants.Rarity; import mage.constants.SetType; /** - * * @author LevelX2 */ public final class Starter1999 extends ExpansionSet { @@ -114,7 +113,7 @@ public final class Starter1999 extends ExpansionSet { cards.add(new SetCardInfo("Man-o'-War", 41, Rarity.UNCOMMON, mage.cards.m.ManOWar.class, RETRO_ART)); cards.add(new SetCardInfo("Merfolk of the Pearl Trident", 42, Rarity.COMMON, mage.cards.m.MerfolkOfThePearlTrident.class, RETRO_ART)); cards.add(new SetCardInfo("Mind Rot", 83, Rarity.COMMON, mage.cards.m.MindRot.class, RETRO_ART)); - cards.add(new SetCardInfo("Mons's Goblin Raiders", 112, Rarity.RARE, mage.cards.m.MonssGoblinRaiders.class, RETRO_ART)); + cards.add(new SetCardInfo("Mons's Goblin Raiders", 112, Rarity.COMMON, mage.cards.m.MonssGoblinRaiders.class, RETRO_ART)); cards.add(new SetCardInfo("Monstrous Growth", 132, Rarity.COMMON, mage.cards.m.MonstrousGrowth.class, RETRO_ART)); cards.add(new SetCardInfo("Moon Sprite", 133, Rarity.UNCOMMON, mage.cards.m.MoonSprite.class, RETRO_ART)); cards.add(new SetCardInfo("Mountain", 166, Rarity.LAND, mage.cards.basiclands.Mountain.class, RETRO_ART_USE_VARIOUS)); diff --git a/Mage.Sets/src/mage/sets/StoreChampionships.java b/Mage.Sets/src/mage/sets/StoreChampionships.java new file mode 100644 index 00000000000..dd7571ca9b4 --- /dev/null +++ b/Mage.Sets/src/mage/sets/StoreChampionships.java @@ -0,0 +1,68 @@ + +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/sch + * @author ReSech + */ +public final class StoreChampionships extends ExpansionSet { + + private static final StoreChampionships instance = new StoreChampionships(); + + public static StoreChampionships getInstance() { + return instance; + } + + private StoreChampionships() { + super("Store Championships", "SCH", ExpansionSet.buildDate(2022, 7, 9), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Aether Channeler", 11, Rarity.RARE, mage.cards.a.AetherChanneler.class)); + cards.add(new SetCardInfo("Angel of Despair", 22, Rarity.RARE, mage.cards.a.AngelOfDespair.class)); + cards.add(new SetCardInfo("Annex Sentry", 7, Rarity.RARE, mage.cards.a.AnnexSentry.class)); + cards.add(new SetCardInfo("Archmage's Charm", 2, Rarity.RARE, mage.cards.a.ArchmagesCharm.class)); + cards.add(new SetCardInfo("Blazing Rootwalla", 24, Rarity.RARE, mage.cards.b.BlazingRootwalla.class)); + cards.add(new SetCardInfo("Cauldron Familiar", 18, Rarity.RARE, mage.cards.c.CauldronFamiliar.class)); + cards.add(new SetCardInfo("Charming Scoundrel", 37, Rarity.RARE, mage.cards.c.CharmingScoundrel.class)); + cards.add(new SetCardInfo("Chromatic Sphere", 30, Rarity.RARE, mage.cards.c.ChromaticSphere.class)); + cards.add(new SetCardInfo("City of Brass", 41, Rarity.RARE, mage.cards.c.CityOfBrass.class)); + cards.add(new SetCardInfo("Dark Confidant", 3, Rarity.RARE, mage.cards.d.DarkConfidant.class, FULL_ART)); + cards.add(new SetCardInfo("Dark Petition", 19, Rarity.RARE, mage.cards.d.DarkPetition.class)); + cards.add(new SetCardInfo("Dauthi Voidwalker", "23e", Rarity.RARE, mage.cards.d.DauthiVoidwalker.class, FULL_ART)); + cards.add(new SetCardInfo("Death's Shadow", 40, Rarity.RARE, mage.cards.d.DeathsShadow.class)); + cards.add(new SetCardInfo("Deep-Cavern Bat", 33, Rarity.RARE, mage.cards.d.DeepCavernBat.class)); + cards.add(new SetCardInfo("Eidolon of the Great Revel", 14, Rarity.RARE, mage.cards.e.EidolonOfTheGreatRevel.class)); + cards.add(new SetCardInfo("Flame Slash", 1, Rarity.RARE, mage.cards.f.FlameSlash.class)); + cards.add(new SetCardInfo("Gifted Aetherborn", 13, Rarity.RARE, mage.cards.g.GiftedAetherborn.class)); + cards.add(new SetCardInfo("Gilded Goose", 5, Rarity.RARE, mage.cards.g.GildedGoose.class)); + cards.add(new SetCardInfo("Gleeful Demolition", 36, Rarity.RARE, mage.cards.g.GleefulDemolition.class)); + cards.add(new SetCardInfo("Goddric, Cloaked Reveler", 38, Rarity.RARE, mage.cards.g.GoddricCloakedReveler.class, FULL_ART)); + cards.add(new SetCardInfo("Hollow One", 25, Rarity.RARE, mage.cards.h.HollowOne.class)); + cards.add(new SetCardInfo("Koth, Fire of Resistance", 9, Rarity.RARE, mage.cards.k.KothFireOfResistance.class)); + cards.add(new SetCardInfo("Lier, Disciple of the Drowned", 20, Rarity.RARE, mage.cards.l.LierDiscipleOfTheDrowned.class)); + cards.add(new SetCardInfo("Memory Deluge", 8, Rarity.RARE, mage.cards.m.MemoryDeluge.class)); + cards.add(new SetCardInfo("Monastery Swiftspear", 27, Rarity.RARE, mage.cards.m.MonasterySwiftspear.class)); + cards.add(new SetCardInfo("Moonshaker Cavalry", 17, Rarity.MYTHIC, mage.cards.m.MoonshakerCavalry.class, FULL_ART)); + cards.add(new SetCardInfo("Mortify", 21, Rarity.RARE, mage.cards.m.Mortify.class)); + cards.add(new SetCardInfo("Omnath, Locus of Creation", 6, Rarity.MYTHIC, mage.cards.o.OmnathLocusOfCreation.class, FULL_ART)); + cards.add(new SetCardInfo("Preacher of the Schism", 34, Rarity.RARE, mage.cards.p.PreacherOfTheSchism.class)); + cards.add(new SetCardInfo("Preordain", 39, Rarity.RARE, mage.cards.p.Preordain.class)); + cards.add(new SetCardInfo("Reality Smasher", 31, Rarity.RARE, mage.cards.r.RealitySmasher.class)); + cards.add(new SetCardInfo("Shark Typhoon", 28, Rarity.RARE, mage.cards.s.SharkTyphoon.class)); + cards.add(new SetCardInfo("Spell Pierce", 4, Rarity.RARE, mage.cards.s.SpellPierce.class)); + cards.add(new SetCardInfo("Strangle", 10, Rarity.RARE, mage.cards.s.Strangle.class)); + cards.add(new SetCardInfo("Tail Swipe", 15, Rarity.RARE, mage.cards.t.TailSwipe.class)); + cards.add(new SetCardInfo("Thalia and The Gitrog Monster", 12, Rarity.MYTHIC, mage.cards.t.ThaliaAndTheGitrogMonster.class, FULL_ART)); + cards.add(new SetCardInfo("Transcendent Message", 16, Rarity.RARE, mage.cards.t.TranscendentMessage.class)); + cards.add(new SetCardInfo("Urza's Saga", 29, Rarity.RARE, mage.cards.u.UrzasSaga.class, FULL_ART)); + cards.add(new SetCardInfo("Vengevine", 26, Rarity.RARE, mage.cards.v.Vengevine.class, FULL_ART)); + cards.add(new SetCardInfo("Virtue of Persistence", 35, Rarity.MYTHIC, mage.cards.v.VirtueOfPersistence.class)); + cards.add(new SetCardInfo("Void Winnower", 32, Rarity.RARE, mage.cards.v.VoidWinnower.class, FULL_ART)); + } + +} diff --git a/Mage.Sets/src/mage/sets/TheBrothersWar.java b/Mage.Sets/src/mage/sets/TheBrothersWar.java index 954cf1e1849..c1c78f27745 100644 --- a/Mage.Sets/src/mage/sets/TheBrothersWar.java +++ b/Mage.Sets/src/mage/sets/TheBrothersWar.java @@ -360,7 +360,7 @@ public final class TheBrothersWar extends ExpansionSet { cards.add(new SetCardInfo("Terror Ballista", 290, Rarity.RARE, mage.cards.t.TerrorBallista.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Terror Ballista", 375, Rarity.RARE, mage.cards.t.TerrorBallista.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Fall of Kroog", 133, Rarity.UNCOMMON, mage.cards.t.TheFallOfKroog.class)); - cards.add(new SetCardInfo("The Mightstone and Weakstone", "238a", Rarity.RARE, mage.cards.t.TheMightstoneAndWeakstone.class)); + cards.add(new SetCardInfo("The Mightstone and Weakstone", "238", Rarity.RARE, mage.cards.t.TheMightstoneAndWeakstone.class)); cards.add(new SetCardInfo("The Stasis Coffin", 245, Rarity.RARE, mage.cards.t.TheStasisCoffin.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Stasis Coffin", 366, Rarity.RARE, mage.cards.t.TheStasisCoffin.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Stone Brain", 247, Rarity.RARE, mage.cards.t.TheStoneBrain.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Sets/src/mage/sets/URLConventionPromos.java b/Mage.Sets/src/mage/sets/URLConventionPromos.java index 8f10cb5898c..429984c3a09 100644 --- a/Mage.Sets/src/mage/sets/URLConventionPromos.java +++ b/Mage.Sets/src/mage/sets/URLConventionPromos.java @@ -23,7 +23,7 @@ public class URLConventionPromos extends ExpansionSet { cards.add(new SetCardInfo("Aeronaut Tinkerer", 8, Rarity.COMMON, mage.cards.a.AeronautTinkerer.class)); cards.add(new SetCardInfo("Bloodthrone Vampire", 3, Rarity.RARE, mage.cards.b.BloodthroneVampire.class)); cards.add(new SetCardInfo("Chandra's Fury", 5, Rarity.RARE, mage.cards.c.ChandrasFury.class)); - cards.add(new SetCardInfo("Kor Skyfisher", 2, Rarity.RARE, mage.cards.k.KorSkyfisher.class)); + cards.add(new SetCardInfo("Kor Skyfisher", 23, Rarity.RARE, mage.cards.k.KorSkyfisher.class)); cards.add(new SetCardInfo("Merfolk Mesmerist", 4, Rarity.RARE, mage.cards.m.MerfolkMesmerist.class)); // Italian-only printing //cards.add(new SetCardInfo("Relentless Rats", 9, Rarity.RARE, mage.cards.r.RelentlessRats.class)); @@ -31,5 +31,6 @@ public class URLConventionPromos extends ExpansionSet { //cards.add(new SetCardInfo("Shepherd of the Lost", "34*", Rarity.UNCOMMON, mage.cards.s.ShepherdOfTheLost.class)); cards.add(new SetCardInfo("Stealer of Secrets", 7, Rarity.RARE, mage.cards.s.StealerOfSecrets.class)); cards.add(new SetCardInfo("Steward of Valeron", 1, Rarity.RARE, mage.cards.s.StewardOfValeron.class)); + cards.add(new SetCardInfo("Counterspell", 2, Rarity.RARE, mage.cards.c.Counterspell.class)); } } diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2025.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2025.java index 94b5e9e2916..3918977766b 100644 --- a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2025.java +++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2025.java @@ -20,8 +20,13 @@ public class WizardsPlayNetwork2025 extends ExpansionSet { this.hasBoosters = false; this.hasBasicLands = false; - cards.add(new SetCardInfo("Dragon's Hoard", 2, Rarity.RARE, mage.cards.d.DragonsHoard.class, RETRO_ART)); + cards.add(new SetCardInfo("Culling Ritual", 4, Rarity.RARE, mage.cards.c.CullingRitual.class)); + cards.add(new SetCardInfo("Despark", 2, Rarity.UNCOMMON, mage.cards.d.Despark.class)); + cards.add(new SetCardInfo("Dragon's Hoard", "1p", Rarity.RARE, mage.cards.d.DragonsHoard.class, RETRO_ART)); cards.add(new SetCardInfo("Dragonspeaker Shaman", 3, Rarity.RARE, mage.cards.d.DragonspeakerShaman.class)); + cards.add(new SetCardInfo("Palladium Myr", 6, Rarity.RARE, mage.cards.p.PalladiumMyr.class, RETRO_ART)); cards.add(new SetCardInfo("Rishkar's Expertise", 1, Rarity.RARE, mage.cards.r.RishkarsExpertise.class, RETRO_ART)); + cards.add(new SetCardInfo("Spectacular Spider-Man", 7, Rarity.RARE, mage.cards.s.SpectacularSpiderMan.class)); + cards.add(new SetCardInfo("Zidane, Tantalus Thief", 5, Rarity.RARE, mage.cards.z.ZidaneTantalusThief.class)); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/CopyAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/CopyAITest.java index a7840516228..07204312358 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/CopyAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/CopyAITest.java @@ -116,7 +116,7 @@ public class CopyAITest extends CardTestPlayerBaseWithAIHelps { // addCard(Zone.GRAVEYARD, playerB, "Balduvian Bears", 1); // 2/2 - // copy (AI must choose most valueable permanent - own) + // copy (AI must choose most valuable permanent - own) aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); setStopAt(1, PhaseStep.END_TURN); diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java index c8ce253a707..b0fd1385fc5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java @@ -20,8 +20,9 @@ import java.util.List; *

* TODO: add tests and implement best choice selection on timeout * (AI must make any good/bad choice on timeout with game log - not a skip) + * + * TODO: AI do not support game sims from triggered (it's run, but do not use results) *

- * TODO: AI do not support game simulations for target options in triggered * * @author JayDi85 */ @@ -213,4 +214,29 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI { // 4 damage to x2 bears and 1 damage to damaged bear runManyTargetOptionsInActivate("5 target creatures with one damaged", 5, 3, true, 20); } + + @Test + public void test_ElderDeepFiend_TooManyUpToChoices() { + // bug: game freeze with 100% CPU usage + // https://github.com/magefree/mage/issues/9518 + int cardsCount = 2; // 2+ cards will generate too much target options for simulations + + // Boulderfall deals 5 damage divided as you choose among any number of targets. + // Flash + // Emerge {5}{U}{U} (You may cast this spell by sacrificing a creature and paying the emerge cost reduced by that creature's mana value.) + // When you cast this spell, tap up to four target permanents. + addCard(Zone.HAND, playerA, "Elder Deep-Fiend", cardsCount); // {8} + addCard(Zone.BATTLEFIELD, playerA, "Island", 8 * cardsCount); + // + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 2); + addCard(Zone.BATTLEFIELD, playerB, "Kitesail Corsair", 2); + addCard(Zone.BATTLEFIELD, playerB, "Alpha Tyrranax", 2); + addCard(Zone.BATTLEFIELD, playerB, "Abbey Griffin", 2); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Elder Deep-Fiend", cardsCount); // ai must cast it + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationStabilityAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationStabilityAITest.java index d1aa17af189..4ee5d9fbf8e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationStabilityAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationStabilityAITest.java @@ -28,6 +28,8 @@ public class SimulationStabilityAITest extends CardTestPlayerBaseWithAIHelps { @Before public void prepare() { + // WARNING, for some reason java 8 sometime can't compile and run test with updated AI settings, so it's ok to freeze on it + // comment it to enable AI code debug Assert.assertFalse("AI stability tests must be run under release config", ComputerPlayer.COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS); } diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationTriggersAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationTriggersAITest.java new file mode 100644 index 00000000000..a13f26171be --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationTriggersAITest.java @@ -0,0 +1,84 @@ +package org.mage.test.AI.basic; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * Make sure AI can simulate priority with triggers resolve + * + * @author JayDi85 + */ +public class SimulationTriggersAITest extends CardTestPlayerBaseWithAIHelps { + + @Test + @Ignore + // TODO: trigger's target options supported on priority sim, but do not used for some reason + // see addTargetOptions, node.children, ComputerPlayer6->targets, etc + public void test_DeepglowSkate_MustBeSimulated() { + // make sure targets choosing on trigger use same game sims and best results + + // When Deepglow Skate enters the battlefield, double the number of each kind of counter on any number + // of target permanents. + addCard(Zone.HAND, playerA, "Deepglow Skate", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + // + addCard(Zone.BATTLEFIELD, playerA, "Ajani, Adversary of Tyrants", 1); // x4 loyalty + addCard(Zone.BATTLEFIELD, playerA, "Ajani, Caller of the Pride", 1); // x4 loyalty + // + // This creature enters with a -1/-1 counter on it. + addCard(Zone.BATTLEFIELD, playerA, "Bloodied Ghost", 1); // 3/3 + // + // Players can't activate planeswalkers' loyalty abilities. + addCard(Zone.BATTLEFIELD, playerA, "The Immortal Sun", 1); // disable planeswalkers usage by AI + + // AI must cast boost and ignore doubling of -1/-1 counters on own creatures due bad score + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Deepglow Skate", 1); + assertCounterCount(playerA, "Ajani, Adversary of Tyrants", CounterType.LOYALTY, 4 * 2); + assertCounterCount(playerA, "Ajani, Caller of the Pride", CounterType.LOYALTY, 4 * 2); + assertCounterCount(playerA, "Bloodied Ghost", CounterType.M1M1, 1); // make sure AI will not double bad counters + } + + @Test + public void test_DeepglowSkate_PerformanceOnTooManyChoices() { + // bug: game freeze with 100% CPU usage + // https://github.com/magefree/mage/issues/9438 + int cardsCount = 2; // 2+ cards will generate too much target options for simulations + int boostMultiplier = (int) Math.pow(2, cardsCount); + + // When Deepglow Skate enters the battlefield, double the number of each kind of counter on any number + // of target permanents. + addCard(Zone.HAND, playerA, "Deepglow Skate", cardsCount); // {4}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 5 * cardsCount); + // + addCard(Zone.BATTLEFIELD, playerA, "Ajani, Adversary of Tyrants", 1); // x4 loyalty + addCard(Zone.BATTLEFIELD, playerA, "Ajani, Caller of the Pride", 1); // x4 loyalty + addCard(Zone.BATTLEFIELD, playerB, "Ajani Goldmane", 1); // x4 loyalty + addCard(Zone.BATTLEFIELD, playerB, "Ajani, Inspiring Leader", 1); // x5 loyalty + // + // Players can't activate planeswalkers' loyalty abilities. + addCard(Zone.BATTLEFIELD, playerA, "The Immortal Sun", 1); // disable planeswalkers usage by AI + + // AI must cast multiple booster spells and double only own counters and only good + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Deepglow Skate", cardsCount); + assertCounterCount(playerA, "Ajani, Adversary of Tyrants", CounterType.LOYALTY, 4 * boostMultiplier); + assertCounterCount(playerA, "Ajani, Caller of the Pride", CounterType.LOYALTY, 4 * boostMultiplier); + assertCounterCount(playerB, "Ajani Goldmane", CounterType.LOYALTY, 4); + assertCounterCount(playerB, "Ajani, Inspiring Leader", CounterType.LOYALTY, 5); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/ValakutTheMoltenPinnacleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/ValakutTheMoltenPinnacleTest.java index d9bea5640c2..247e30aef40 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/ValakutTheMoltenPinnacleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/ValakutTheMoltenPinnacleTest.java @@ -69,6 +69,7 @@ public class ValakutTheMoltenPinnacleTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scapeshift"); setChoice(playerA, "Forest^Forest^Forest^Forest^Forest^Forest"); // to sac + setChoice(playerA, TestPlayer.CHOICE_SKIP); addTarget(playerA, "Mountain^Mountain^Mountain^Mountain^Mountain^Mountain"); // to search setChoice(playerA, "Whenever a Mountain", 6 - 1); // x6 triggers from valakut addTarget(playerA, playerB, 6); // to deal damage diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java index 98b5e6a3b1b..59885e2e729 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java @@ -337,4 +337,20 @@ public class BargainTest extends CardTestPlayerBase { assertLife(playerA, 20 + 3); assertTappedCount("Forest", true, 7); } + + @Test + public void testCantBargainWithRestriction() { + setStrictChooseMode(true); + // Players can’t pay life or sacrifice nonland permanents to cast spells or activate abilities. + addCard(Zone.BATTLEFIELD, playerB, "Yasharn, Implacable Earth"); + addCard(Zone.HAND, playerA, glutton); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, relic); + + checkPlayableAbility("restricted by Yasharn", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hamlet Glutton", false); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java index 457ecad62cf..9eed0be4e7a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CumulativeUpkeepTest.java @@ -61,32 +61,37 @@ public class CumulativeUpkeepTest extends CardTestPlayerBase { // Whenever Kor Celebrant or another creature you control enters, you gain 1 life. addCard(Zone.HAND, playerB, "Kor Celebrant", 1); // Creature {2}{W} addCard(Zone.BATTLEFIELD, playerB, "Plains", 3); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + // // Cumulative upkeep {2} - // When Illusions of Grandeur enters the battlefield, you gain 20 life. + // At the beginning of your upkeep, put an age counter on this permanent, + // then sacrifice it unless you pay its upkeep cost for each age counter on it. // When Illusions of Grandeur leaves the battlefield, you lose 20 life. addCard(Zone.HAND, playerA, "Illusions of Grandeur"); // Enchantment {3}{U} - - // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + // + // At the beginning of your upkeep, you may exchange control of target nonland permanent you control + // and target nonland permanent an opponent controls with an equal or lesser converted mana cost. addCard(Zone.HAND, playerA, "Puca's Mischief"); // Enchantment {3}{U} - + // turn 1, 2 - prepare cumulative upkeep and gain life triggers castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Illusions of Grandeur"); - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Kor Celebrant"); - - // Illusions of Grandeur - CumulativeUpkeepAbility: Cumulative upkeep {2} - setChoice(playerA, true); // Pay {2}? - + + // turn 3 - upkeep trigger and prepare control card + setChoice(playerA, true); // use upkeep cost {2} castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Puca's Mischief"); - - setChoice(playerA, "Cumulative upkeep"); // Triggered list (total 2) which trigger goes first on the stack - addTarget(playerA, "Illusions of Grandeur"); // Own target permanent of Puca's Mischief - addTarget(playerA, "Kor Celebrant"); // Opponent's target permanent of Puca's Mischief - - setChoice(playerA, true); // At the beginning of your upkeep, you may exchange control of target nonland permanent you control and target nonland permanent an opponent controls with an equal or lesser converted mana cost. - setChoice(playerA, false); // Pay {2}{2}? + + // turn 5 - multiple upkeep triggers + // first put upkeep, then put change control - so control will be resolved before upkeep cost + setChoice(playerA, "Cumulative upkeep"); + + // resolve change control first + addTarget(playerA, "Illusions of Grandeur"); // own target + addTarget(playerA, "Kor Celebrant"); // opponent target + setChoice(playerA, true); // yes, resolve change control + + // resolve upkeep + setChoice(playerA, false); // no, do not pay upkeep cost checkPermanentCounters("Age counters", 5, PhaseStep.PRECOMBAT_MAIN, playerB, "Illusions of Grandeur", CounterType.AGE, 2); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisguiseTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisguiseTest.java index 3ad59bf8d70..15afb526c3a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisguiseTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisguiseTest.java @@ -55,7 +55,7 @@ public class DisguiseTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dog Walker using Disguise"); runCode("face up on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { Assert.assertEquals("stack, server - can't find spell", 1, currentGame.getStack().size()); - SpellAbility spellAbility = (SpellAbility) currentGame.getStack().getFirst().getStackAbility(); + SpellAbility spellAbility = (SpellAbility) currentGame.getStack().getFirstOrNull().getStackAbility(); Assert.assertEquals("stack, server - can't find spell", "Cast Dog Walker using Disguise", spellAbility.getName()); CardView spellView = getGameView(playerA).getStack().values().stream().findFirst().orElse(null); Assert.assertNotNull("stack, client: can't find spell", spellView); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisturbTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisturbTest.java index 31c9ba2e104..9bc7356225d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisturbTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisturbTest.java @@ -40,7 +40,7 @@ public class DisturbTest extends CardTestPlayerBase { checkStackObject("on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb", 1); runCode("check stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { // Stack must contain another card side, so spell/card characteristics must be diff from main side (only mana value is same) - Spell spell = (Spell) game.getStack().getFirst(); + Spell spell = (Spell) game.getStack().getFirstOrNull(); Assert.assertEquals("Hook-Haunt Drifter", spell.getName()); Assert.assertEquals(1, spell.getCardType(game).size()); Assert.assertEquals(CardType.CREATURE, spell.getCardType(game).get(0)); @@ -91,7 +91,7 @@ public class DisturbTest extends CardTestPlayerBase { checkStackObject("on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Waildrifter using Disturb", 1); runCode("check stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { // Stack must contain another card side, so spell/card characteristics must be diff from main side (only mana value is same) - Spell spell = (Spell) game.getStack().getFirst(); + Spell spell = (Spell) game.getStack().getFirstOrNull(); Assert.assertEquals("Waildrifter", spell.getName()); Assert.assertEquals(1, spell.getCardType(game).size()); Assert.assertEquals(CardType.CREATURE, spell.getCardType(game).get(0)); @@ -187,7 +187,7 @@ public class DisturbTest extends CardTestPlayerBase { // cast with disturb activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb"); runCode("check stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { - Spell spell = (Spell) game.getStack().getFirst(); + Spell spell = (Spell) game.getStack().getFirstOrNull(); Assert.assertEquals("mana value must be from main side", 2, spell.getManaValue()); Assert.assertEquals("mana cost to pay must be modified", "{U}", spell.getSpellAbility().getManaCostsToPay().getText()); }); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EscalateTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EscalateTest.java index ce3c5451784..875883533cc 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EscalateTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EscalateTest.java @@ -4,6 +4,7 @@ package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -27,7 +28,9 @@ public class EscalateTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Savage Alliance", "mode=2Silvercoat Lion"); setModeChoice(playerA, "2"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); + setStrictChooseMode(true); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); @@ -52,7 +55,9 @@ public class EscalateTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Collective Defiance", playerB); setModeChoice(playerA, "3"); // deal 3 dmg to opponent + setModeChoice(playerA, TestPlayer.MODE_SKIP); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -78,10 +83,12 @@ public class EscalateTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Collective Defiance"); // {1}{R}{R} sorcery addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Collective Defiance", "mode=2Gaddock Teeg"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Collective Defiance", "mode=2Gaddock Teeg^mode=3targetPlayer=PlayerB"); setModeChoice(playerA, "2"); // deal 4 dmg to target creature (gaddock teeg) setModeChoice(playerA, "3"); // deal 3 dmg to opponent + setModeChoice(playerA, TestPlayer.MODE_SKIP); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -109,8 +116,6 @@ public class EscalateTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Collective Defiance"); // {1}{R}{R} sorcery addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); - setStrictChooseMode(true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Collective Defiance", "mode=2Wall of Omens"); setModeChoice(playerA, "1"); // opponent discards hand and draws that many setModeChoice(playerA, "2"); // deal 4 dmg to target creature (Wall of Omens) @@ -120,6 +125,8 @@ public class EscalateTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Spell Queller"); addTarget(playerB, "Collective Defiance"); + + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java index d42dd419e14..63bf73b6070 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -50,6 +51,7 @@ public class KickerWithAnyNumberModesAbilityTest extends CardTestPlayerBase { setChoice(playerA, true); // use kicker setModeChoice(playerA, "2"); setModeChoice(playerA, "1"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); addTarget(playerA, playerA); // gain x life addTarget(playerA, "Balduvian Bears"); // get counters @@ -79,6 +81,7 @@ public class KickerWithAnyNumberModesAbilityTest extends CardTestPlayerBase { setChoice(playerA, true); // use kicker setModeChoice(playerA, "2"); setModeChoice(playerA, "1"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); addTarget(playerA, playerA); // gain x life addTarget(playerA, "Balduvian Bears"); // get counters @@ -108,6 +111,7 @@ public class KickerWithAnyNumberModesAbilityTest extends CardTestPlayerBase { setChoice(playerA, true); // use kicker setModeChoice(playerA, "2"); setModeChoice(playerA, "1"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); addTarget(playerA, playerA); // gain x life addTarget(playerA, "Balduvian Bears"); // get counters @@ -139,6 +143,7 @@ public class KickerWithAnyNumberModesAbilityTest extends CardTestPlayerBase { setChoice(playerA, true); // use kicker setModeChoice(playerA, "2"); setModeChoice(playerA, "1"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); addTarget(playerA, playerA); // gain x life addTarget(playerA, "Balduvian Bears"); // get counters diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/LandfallTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/LandfallTest.java index fd5b5811759..f52e53f7474 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/LandfallTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/LandfallTest.java @@ -1,15 +1,12 @@ - package org.mage.test.cards.abilities.keywords; import mage.abilities.keyword.IntimidateAbility; import mage.constants.PhaseStep; import mage.constants.Zone; -import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ public class LandfallTest extends CardTestPlayerBase { @@ -26,9 +23,12 @@ public class LandfallTest extends CardTestPlayerBase { playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plains"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rest for the Weary"); + addTarget(playerA, playerA); castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Rest for the Weary"); + addTarget(playerA, playerA); + setStrictChooseMode(true); setStopAt(2, PhaseStep.BEGIN_COMBAT); execute(); @@ -43,7 +43,6 @@ public class LandfallTest extends CardTestPlayerBase { * If you Hive Mind an opponent's Rest for the Weary and redirect its target * to yourself when it's not your turn, the game spits out this message and * rolls back to before Rest for the Weary was cast. - * */ @Test public void testHiveMind() { @@ -57,15 +56,17 @@ public class LandfallTest extends CardTestPlayerBase { // Landfall - If you had a land enter the battlefield under your control this turn, that player gains 8 life instead. addCard(Zone.HAND, playerA, "Rest for the Weary", 1); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rest for the Weary"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rest for the Weary", playerA); + setChoice(playerB, true); // change target of copied spell + addTarget(playerB, playerB); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); assertGraveyardCount(playerA, "Rest for the Weary", 1); assertLife(playerA, 24); assertLife(playerB, 24); - } @Test @@ -76,6 +77,7 @@ public class LandfallTest extends CardTestPlayerBase { playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plains"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -104,10 +106,14 @@ public class LandfallTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + // prepare landfall playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mountain"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Searing Blaze"); + addTarget(playerA, playerB); + addTarget(playerA, "Silvercoat Lion"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -133,6 +139,7 @@ public class LandfallTest extends CardTestPlayerBase { attack(2, playerB, "Silvercoat Lion"); castSpell(2, PhaseStep.DECLARE_ATTACKERS, playerB, "Groundswell", "Silvercoat Lion"); + setStrictChooseMode(true); setStopAt(2, PhaseStep.END_COMBAT); execute(); @@ -179,6 +186,7 @@ public class LandfallTest extends CardTestPlayerBase { attack(2, playerB, "Silvercoat Lion"); castSpell(2, PhaseStep.DECLARE_ATTACKERS, playerB, "Groundswell", "Silvercoat Lion"); + setStrictChooseMode(true); setStopAt(2, PhaseStep.END_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/PrototypeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/PrototypeTest.java index 26edf9e5db8..fc9123a883e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/PrototypeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/PrototypeTest.java @@ -16,6 +16,7 @@ import mage.filter.predicate.mageobject.*; import mage.game.permanent.Permanent; import org.junit.Assert; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -319,6 +320,7 @@ public class PrototypeTest extends CardTestPlayerBase { castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, epiphany); setModeChoice(playerA, "3"); // Return target nonland permanent to its owner's hand. setModeChoice(playerA, "4"); // Create a token that's a copy of target creature you control. + setModeChoice(playerA, TestPlayer.MODE_SKIP); addTarget(playerA, automaton); addTarget(playerA, automaton); @@ -340,6 +342,7 @@ public class PrototypeTest extends CardTestPlayerBase { castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, epiphany); setModeChoice(playerA, "3"); // Return target nonland permanent to its owner's hand. setModeChoice(playerA, "4"); // Create a token that's a copy of target creature you control. + setModeChoice(playerA, TestPlayer.MODE_SKIP); addTarget(playerA, automaton); addTarget(playerA, automaton); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SaddleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SaddleTest.java index 33051479103..ab6a016413a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SaddleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SaddleTest.java @@ -8,6 +8,7 @@ import mage.game.permanent.Permanent; import mage.watchers.common.SaddledMountWatcher; import org.junit.Assert; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -90,6 +91,7 @@ public class SaddleTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Saddle"); setChoice(playerA, bear); // to saddle cost + setChoice(playerA, TestPlayer.CHOICE_SKIP); attack(1, playerA, possum, playerB); setChoice(playerA, bear); // to return @@ -116,6 +118,7 @@ public class SaddleTest extends CardTestPlayerBase { // turn 1 - saddle x2 and trigger on attack activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Saddle"); setChoice(playerA, bear + "^" + lion); + setChoice(playerA, TestPlayer.CHOICE_SKIP); attack(1, playerA, possum, playerB); setChoice(playerA, elf); // to return (try to choose a wrong creature, so game must not allow to choose it) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpreeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpreeTest.java index d15f224a4a2..97c0b402a5b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpreeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpreeTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -24,8 +25,9 @@ public class SpreeTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, bear, 1); addCard(Zone.HAND, playerA, accident); - setModeChoice(playerA, "1"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, accident, bear); + setModeChoice(playerA, "1"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -41,8 +43,9 @@ public class SpreeTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1 + 1); addCard(Zone.HAND, playerA, accident); - setModeChoice(playerA, "2"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, accident); + setModeChoice(playerA, "2"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -58,9 +61,10 @@ public class SpreeTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, bear, 1); addCard(Zone.HAND, playerA, accident); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, accident, bear); setModeChoice(playerA, "1"); setModeChoice(playerA, "2"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, accident, bear); + setModeChoice(playerA, TestPlayer.MODE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -80,9 +84,10 @@ public class SpreeTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, electromancer, 1); addCard(Zone.HAND, playerA, accident); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, accident, bear); setModeChoice(playerA, "1"); setModeChoice(playerA, "2"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, accident, bear); + setModeChoice(playerA, TestPlayer.MODE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/OneShotNonTargetTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/OneShotNonTargetTest.java index 0475b151603..95f1e8ceb06 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/OneShotNonTargetTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/OneShotNonTargetTest.java @@ -5,22 +5,31 @@ import mage.constants.Zone; import org.junit.Test; import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; + /** - * * @author notgreat */ public class OneShotNonTargetTest extends CardTestPlayerBase { + @Test public void YorionChooseAfterTriggerTest() { + // When Yorion enters the battlefield, exile any number of other nonland permanents you own and control. + // Return those cards to the battlefield at the beginning of the next end step. addCard(Zone.HAND, playerA, "Yorion, Sky Nomad"); addCard(Zone.BATTLEFIELD, playerA, "Plains", 7); + // + // When Resolute Reinforcements enters the battlefield, create a 1/1 white Soldier creature token. addCard(Zone.HAND, playerA, "Resolute Reinforcements"); + // prepare yori and put trigger on stack castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Yorion, Sky Nomad"); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); checkPermanentCount("Yorion on battlefield", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Yorion, Sky Nomad", 1); + + // prepare another create before yori trigger resolve castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Resolute Reinforcements"); - setChoice(playerA, "Resolute Reinforcements"); + setChoice(playerA, "Resolute Reinforcements"); // 1 of 2 target for yori trigger + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -30,6 +39,7 @@ public class OneShotNonTargetTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Resolute Reinforcements", 1); assertTappedCount("Plains", true, 7); } + @Test public void NonTargetAdjusterTest() { addCard(Zone.HAND, playerA, "Temporal Firestorm"); @@ -52,21 +62,30 @@ public class OneShotNonTargetTest extends CardTestPlayerBase { assertGraveyardCount(playerA, "Python", 0); assertGraveyardCount(playerA, "Watchwolf", 1); } + @Test public void ModeSelectionTest() { - addCard(Zone.HAND, playerA, "SOLDIER Military Program"); + // At the beginning of combat on your turn, choose one. If you control a commander, you may choose both instead. + // * Create a 1/1 white Soldier creature token. + // * Put a +1/+1 counter on each of up to two Soldiers you control. + addCard(Zone.HAND, playerA, "SOLDIER Military Program"); // {2}{W} addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); - addCard(Zone.BATTLEFIELD, playerA, "Squire", 1); + addCard(Zone.BATTLEFIELD, playerA, "Squire", 1); // 1/2 + // prepare mode ability castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "SOLDIER Military Program"); + + // turn 1 combat - put counter setModeChoice(playerA, "2"); setChoice(playerA, "Squire"); - setChoice(playerA, TestPlayer.CHOICE_SKIP); + // turn 3 combat - create token setModeChoice(playerA, "1"); + // turn 5 combat - put counter setModeChoice(playerA, "2"); - setChoice(playerA, "Squire^Soldier Token"); + setChoice(playerA, "Squire"); + setChoice(playerA, "Soldier Token"); setStrictChooseMode(true); setStopAt(5, PhaseStep.END_TURN); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AngelOfJubilationTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AngelOfJubilationTest.java index 2655cdda368..54852c869d3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AngelOfJubilationTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AngelOfJubilationTest.java @@ -1,7 +1,13 @@ package org.mage.test.cards.continuous; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.permanent.token.FoodToken; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -18,12 +24,14 @@ import org.mage.test.serverside.base.CardTestPlayerBase; */ public class AngelOfJubilationTest extends CardTestPlayerBase { + public static final String angelOfJubilation = "Angel of Jubilation"; + /** * Tests boosting other non black creatures */ @Test public void testBoost() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerA, "Devout Chaplain"); addCard(Zone.BATTLEFIELD, playerA, "Corpse Traders"); @@ -33,7 +41,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { assertLife(playerA, 20); assertLife(playerB, 20); - assertPowerToughness(playerA, "Angel of Jubilation", 3, 3); + assertPowerToughness(playerA, angelOfJubilation, 3, 3); assertPowerToughness(playerA, "Devout Chaplain", 3, 3); assertPowerToughness(playerA, "Corpse Traders", 3, 3); } @@ -43,14 +51,14 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { */ @Test public void testNoBoostOnBattlefieldLeave() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerA, "Devout Chaplain"); addCard(Zone.BATTLEFIELD, playerA, "Corpse Traders"); addCard(Zone.HAND, playerA, "Lightning Bolt"); addCard(Zone.BATTLEFIELD, playerA, "Mountain"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Angel of Jubilation"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", angelOfJubilation); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -58,14 +66,14 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { assertLife(playerA, 20); assertLife(playerB, 20); - assertPermanentCount(playerA, "Angel of Jubilation", 0); + assertPermanentCount(playerA, angelOfJubilation, 0); assertPowerToughness(playerA, "Devout Chaplain", 2, 2); assertPowerToughness(playerA, "Corpse Traders", 3, 3); } @Test public void testOpponentCantSacrificeCreatures() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk"); addCard(Zone.BATTLEFIELD, playerB, "Corpse Traders"); @@ -81,7 +89,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { @Test public void testOpponentCanSacrificeNonCreaturePermanents() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerA, "Savannah Lions"); addCard(Zone.BATTLEFIELD, playerB, "Barrin, Master Wizard"); addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk"); @@ -89,20 +97,20 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Food Chain"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{2}, Sacrifice a permanent: Return target creature to its owner's hand."); - addTarget(playerB, "Angel of Jubilation"); // return to hand + addTarget(playerB, angelOfJubilation); // return to hand setChoice(playerB, "Food Chain"); // sacrifice cost setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); - assertPermanentCount(playerA, "Angel of Jubilation", 0); + assertPermanentCount(playerA, angelOfJubilation, 0); assertPermanentCount(playerB, "Food Chain", 0); } @Test public void testOpponentCantSacrificeCreaturesAsPartOfPermanentsOptions() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerB, "Barrin, Master Wizard"); addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk"); addCard(Zone.BATTLEFIELD, playerB, "Llanowar Elves", 2); @@ -115,13 +123,13 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.END_TURN); execute(); - assertPermanentCount(playerA, "Angel of Jubilation", 1); + assertPermanentCount(playerA, angelOfJubilation, 1); assertPermanentCount(playerB, "Nantuko Husk", 1); } @Test public void testOpponentCantSacrificeAll() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk"); addCard(Zone.BATTLEFIELD, playerB, "Corpse Traders"); addCard(Zone.HAND, playerB, "Soulblast"); @@ -142,7 +150,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { @Test public void testOpponentCantSacrificeCreatureSource() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerB, "Children of Korlis"); checkPlayableAbility("Can't sac", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Sacrifice", false); @@ -155,7 +163,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { @Test public void testOpponentCanSacrificeAllLands() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerB, "Tomb of Urami"); addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4); @@ -169,7 +177,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { @Test public void testOpponentCanSacrificeNonCreatureSource() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerA, "Tundra"); addCard(Zone.BATTLEFIELD, playerB, "Wasteland"); @@ -195,7 +203,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { setStrictChooseMode(true); // Other nonblack creatures you control get +1/+1. // Players can't pay life or sacrifice creatures to cast spells or activate abilities - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); // Indestructible @@ -234,7 +242,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { setStrictChooseMode(true); // Other nonblack creatures you control get +1/+1. // Players can't pay life or sacrifice creatures to cast spells or activate abilities - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); // Pay 7 life: Draw seven cards. addCard(Zone.BATTLEFIELD, playerB, "Griselbrand"); @@ -244,4 +252,90 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); } + + @Test + public void testCanSacrificeTriggeredAbility() { + /* + Unscrupulous Contractor + {2}{B} + Creature — Human Assassin + When this creature enters, you may sacrifice a creature. When you do, target player draws two cards and loses 2 life. + Plot {2}{B} + 3/2 + */ + String contractor = "Unscrupulous Contractor"; + /* + Bear Cub + {1}{G} + Creature - Bear + 2/2 + */ + String cub = "Bear Cub"; + + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); + addCard(Zone.HAND, playerA, contractor); + addCard(Zone.HAND, playerB, contractor); + addCard(Zone.BATTLEFIELD, playerA, cub); + addCard(Zone.BATTLEFIELD, playerB, cub); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, contractor); + setChoice(playerA, true); + setChoice(playerA, cub); + addTarget(playerA, playerA); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, contractor); + setChoice(playerB, true); + setChoice(playerB, cub); + addTarget(playerB, playerB); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 2); + assertLife(playerA, 20 - 2); + assertGraveyardCount(playerA, cub, 1); + + assertHandCount(playerB, 1 + 2); //draw + contractor effect + assertLife(playerB, 20 - 2); + assertGraveyardCount(playerB, cub, 1); + } + + @Test + public void canSacToMondrakWithArtifacts() { + setStrictChooseMode(true); + //Mondrak, Glory Dominus + //{2}{W}{W} + //Legendary Creature — Phyrexian Horror + //If one or more tokens would be created under your control, twice that many of those tokens are created instead. + //{1}{W/P}{W/P}, Sacrifice two other artifacts and/or creatures: Put an indestructible counter on Mondrak. + String mondrak = "Mondrak, Glory Dominus"; + Ability ability = new SimpleActivatedAbility( + Zone.ALL, + new CreateTokenEffect(new FoodToken(), 2), + new ManaCostsImpl<>("") + ); + addCustomCardWithAbility("Token-maker", playerA, ability); + + addCard(Zone.BATTLEFIELD, playerA, mondrak); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); + addCard(Zone.BATTLEFIELD, playerA, "Bear Cub", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + + checkPlayableAbility("Can't activate Mondrak", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W/P}{W/P}, Sacrifice", false); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "create two"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W/P}{W/P}, Sacrifice"); + setChoice(playerA, "Food Token", 2); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mondrak, CounterType.INDESTRUCTIBLE, 1); + assertPermanentCount(playerA, "Bear Cub", 2); + assertPermanentCount(playerA, "Food Token", 2); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/OracleEnVecNextTurnTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/OracleEnVecNextTurnTest.java index 47aa71944e6..c59ff09ec30 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/OracleEnVecNextTurnTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/OracleEnVecNextTurnTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.continuous; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -22,7 +23,9 @@ public class OracleEnVecNextTurnTest extends CardTestPlayerBase { // 1 - activate activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target opponent", playerB); - setChoice(playerB, "Balduvian Bears^Angelic Wall"); + setChoice(playerB, "Balduvian Bears"); + setChoice(playerB, "Angelic Wall"); + setChoice(playerB, TestPlayer.CHOICE_SKIP); // checkLife("turn 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, 20); checkPermanentCount("turn 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Balduvian Bears", 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java index 6abb3d2a9ac..f1ae5bc4a96 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java @@ -235,7 +235,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive"); setChoice(playerB, true); // continue after new control addTarget(playerB, "Balduvian Bears"); - addTarget(playerB, TestPlayer.TARGET_SKIP); + //addTarget(playerB, TestPlayer.TARGET_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); checkGraveyardCount("after grave a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 0); checkGraveyardCount("after grave b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears", 0); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/RemoveCounterCostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/RemoveCounterCostTest.java index 1d6ba250fcb..7cec4f73b4f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/RemoveCounterCostTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/RemoveCounterCostTest.java @@ -23,7 +23,6 @@ public class RemoveCounterCostTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}, Remove two +1/+1 counters"); setChoice(playerA, "Novijen Sages"); // counters to remove - setChoice(playerA, TestPlayer.CHOICE_SKIP); setChoice(playerA, "X=2"); // counters to remove setStrictChooseMode(true); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java index 10f2bc5eb3c..bf0e4d40959 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java @@ -427,18 +427,20 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { String savageBeating = "Savage Beating"; skipInitShuffling(); - addCard(Zone.LIBRARY, playerA, savageBeating, 2); + addCard(Zone.LIBRARY, playerA, savageBeating, 2); // to free cast addCard(Zone.HAND, playerA, jeleva); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); addCard(Zone.BATTLEFIELD, playerA, "Island", 3); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // prepare and exile cards from libs castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, jeleva); + // attack and use free cast from exile attack(3, playerA, jeleva); - setChoice(playerA, true); // opt to use Jeleva ability - setChoice(playerA, savageBeating); // choose to cast Savage Beating for free - setChoice(playerA, false); // opt not to pay entwine cost + setChoice(playerA, savageBeating); // to free cast + setChoice(playerA, true); // confirm free cast + setChoice(playerA, false); // ignore entwine cost setModeChoice(playerA, "1"); // use first mode of Savage Beating granting double strike setStopAt(3, PhaseStep.END_COMBAT); @@ -448,6 +450,5 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { assertTapped(jeleva, true); assertLife(playerB, 18); assertAbility(playerA, jeleva, DoubleStrikeAbility.getInstance(), true); - } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaced/ModalDoubleFacedCardsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaced/ModalDoubleFacedCardsTest.java index a229bb39472..257cae4f572 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaced/ModalDoubleFacedCardsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaced/ModalDoubleFacedCardsTest.java @@ -945,7 +945,7 @@ public class ModalDoubleFacedCardsTest extends CardTestPlayerBase { // cast as copy of mdf card castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zam Wesell"); addTarget(playerA, playerB); // target opponent - addTarget(playerA, "Akoum Warrior"); // creature card to copy + setChoice(playerA, "Akoum Warrior"); // creature card to copy waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akoum Warrior", 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/dungeons/DungeonTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/dungeons/DungeonTest.java index 548ecea0a7c..126f783ade5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/dungeons/DungeonTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/dungeons/DungeonTest.java @@ -20,7 +20,7 @@ import java.util.stream.Stream; */ public class DungeonTest extends CardTestPlayerBase { - private static final String TOMB_OF_ANNIHILATION = "Tomb of Annihilation"; + private static final String TOMB_OF_ANNIHILATION = "Tomb of Annihilation"; // TODO: miss test private static final String LOST_MINE_OF_PHANDELVER = "Lost Mine of Phandelver"; private static final String DUNGEON_OF_THE_MAD_MAGE = "Dungeon of the Mad Mage"; private static final String FLAMESPEAKER_ADEPT = "Flamespeaker Adept"; diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/modal/OneOrBothTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/modal/OneOrBothTest.java index 9ea9a2ee029..c9dcc5a9b48 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/modal/OneOrBothTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/modal/OneOrBothTest.java @@ -4,6 +4,7 @@ package org.mage.test.cards.modal; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -25,7 +26,7 @@ public class OneOrBothTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Subtle Strike", "Pillarfield Ox"); setModeChoice(playerA, "1"); - setModeChoice(playerA, null); + setModeChoice(playerA, TestPlayer.MODE_SKIP); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -47,7 +48,7 @@ public class OneOrBothTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Subtle Strike", "Pillarfield Ox"); setModeChoice(playerA, "2"); - setModeChoice(playerA, null); + setModeChoice(playerA, TestPlayer.MODE_SKIP); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/modal/OneOrMoreTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/modal/OneOrMoreTest.java index 8d65219d629..dd02ce056d6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/modal/OneOrMoreTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/modal/OneOrMoreTest.java @@ -6,6 +6,7 @@ import mage.game.permanent.Permanent; import mage.game.permanent.PermanentToken; import org.junit.Assert; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -39,7 +40,7 @@ public class OneOrMoreTest extends CardTestPlayerBase { addTarget(playerA, "Silvercoat Lion"); // for 3 addTarget(playerA, "Silvercoat Lion"); // for 4 addTarget(playerA, playerB); // for 5 - setModeChoice(playerA, null); + setModeChoice(playerA, TestPlayer.MODE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); @@ -76,7 +77,7 @@ public class OneOrMoreTest extends CardTestPlayerBase { addTarget(playerA, "Silvercoat Lion"); // for 3 addTarget(playerA, "Silvercoat Lion"); // for 4 addTarget(playerA, playerB); // for 5 - setModeChoice(playerA, null); + setModeChoice(playerA, TestPlayer.MODE_SKIP); setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/modal/PawPrintsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/modal/PawPrintsTest.java index b302b9cea0d..1e7f01f5e0c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/modal/PawPrintsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/modal/PawPrintsTest.java @@ -3,7 +3,9 @@ package org.mage.test.cards.modal; import mage.constants.PhaseStep; import mage.constants.Zone; import mage.counters.CounterType; +import org.junit.Assert; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -61,25 +63,17 @@ public class PawPrintsTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Season of Gathering"); setModeChoice(playerA, "1"); setModeChoice(playerA, "2"); - setModeChoice(playerA, "3"); // Will be unused + setModeChoice(playerA, "3"); // no more paws for mode 3, see exception below addTarget(playerA, "Memnite"); // for 1 setChoice(playerA, "Enchantment"); setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - // Add one more counter from Hardened Scales, which was still on the battlefield when the counter placing effect triggered - assertPowerToughness(playerA, "Memnite", 3, 3); - assertCounterCount("Memnite", CounterType.P1P1, 2); - - // But not anymore... - assertPermanentCount(playerA, "Hardened Scales", 0); - assertGraveyardCount(playerA, "Hardened Scales", 1); - - // Draw effect didnt trigger - assertHandCount(playerA, 0); - + try { + execute(); + } catch (AssertionError e) { + Assert.assertTrue("mode 3 must not be able to choose due total paws cost", e.getMessage().contains("Can't use mode: 3")); + } } @Test diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/rules/AttachmentTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/rules/AttachmentTest.java index 7da50462f5d..8f2c735588f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/rules/AttachmentTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/rules/AttachmentTest.java @@ -282,9 +282,9 @@ public class AttachmentTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Island", 3); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Show and Tell"); - setChoice(playerA, true); - setChoice(playerB, false); + setChoice(playerA, true); // yes, put from hand to battle addTarget(playerA, "Aether Tunnel"); + setChoice(playerB, false); // no, do not put setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/MantleOfTheAncientsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/MantleOfTheAncientsTest.java index e425d65dc53..5245a558120 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/MantleOfTheAncientsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/MantleOfTheAncientsTest.java @@ -50,6 +50,9 @@ public class MantleOfTheAncientsTest extends CardTestPlayerBase { addTarget(playerA, "Gate Smasher^Aether Tunnel"); addTarget(playerA, TestPlayer.TARGET_SKIP); + + showBattlefield("after", 1, PhaseStep.END_TURN, playerA); + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java index 844ca5cfb0a..b8140e785d9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java @@ -314,15 +314,14 @@ public class ShareTheSpoilsTest extends CardTestCommander4Players { // Cast an adventure card from hand castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Dizzying Swoop"); addTarget(playerA, "Prosper, Tome-Bound"); - addTarget(playerA, TestPlayer.TARGET_SKIP); // tap 1 of 2 targets + //addTarget(playerA, TestPlayer.TARGET_SKIP); // only 1 creature, so tap 1 of 2, no need in skip waitStackResolved(5, PhaseStep.PRECOMBAT_MAIN); // Make sure the creature card can't be played from exile since there isn't the {W}{W} for it checkPlayableAbility("creature cast", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ardenvale Tactician", false); - setStopAt(5, PhaseStep.POSTCOMBAT_MAIN); - setStrictChooseMode(true); + setStopAt(5, PhaseStep.POSTCOMBAT_MAIN); execute(); // 1 exiled with Share the Spoils diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/ApproachOfTheSecondSunTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/ApproachOfTheSecondSunTest.java index bbc8a10ac93..de0b7d560c5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/ApproachOfTheSecondSunTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/ApproachOfTheSecondSunTest.java @@ -189,9 +189,9 @@ public class ApproachOfTheSecondSunTest extends CardTestPlayerBase { // first may have been cast from anywhere. castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Finale of Promise", true); setChoice(playerA, "X=7"); // each with mana value X or less + addTarget(playerA, TARGET_SKIP); // skip instant target + addTarget(playerA, "Approach of the Second Sun"); // use sorcery target setChoice(playerA, "Yes"); // You may cast - addTarget(playerA, TARGET_SKIP); // up to one target instant card - addTarget(playerA, "Approach of the Second Sun"); // and/or up to one target sorcery card from your graveyard checkLife("Approach of the Second Sun cast from graveyard gains life", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 27); checkLibraryCount("Approach of the Second Sun cast from graveyard goes to library", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Approach of the Second Sun", 1); @@ -200,9 +200,9 @@ public class ApproachOfTheSecondSunTest extends CardTestPlayerBase { // The second Approach of the Second Sun that you cast must be cast from your hand, castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Finale of Promise", true); setChoice(playerA, "X=7"); // each with mana value X or less - setChoice(playerA, "Yes"); // You may cast addTarget(playerA, TARGET_SKIP); // up to one target instant card addTarget(playerA, "Approach of the Second Sun"); // and/or up to one target sorcery card from your graveyard + setChoice(playerA, "Yes"); // You may cast checkLife("Approach of the Second Sun cast from graveyard gains life", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 34); checkLibraryCount("Approach of the Second Sun cast from graveyard goes to library", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Approach of the Second Sun", 2); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/BrutalExpulsionTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/BrutalExpulsionTest.java index cbaf8d3479d..41701e21958 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/BrutalExpulsionTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/bfz/BrutalExpulsionTest.java @@ -4,6 +4,7 @@ package org.mage.test.cards.single.bfz; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -27,7 +28,7 @@ public class BrutalExpulsionTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Brutal Expulsion", "mode=2Augmenting Automaton"); setModeChoice(playerA, "2"); - setModeChoice(playerA, null); // ignore last one mode + setModeChoice(playerA, TestPlayer.MODE_SKIP); // ignore last one mode //addTarget(playerA, "mode=2Augmenting Automaton"); // doesn't work with mode setStopAt(1, PhaseStep.END_COMBAT); @@ -56,7 +57,7 @@ public class BrutalExpulsionTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Brutal Expulsion", "mode=2Kiora, the Crashing Wave"); setModeChoice(playerA, "2"); - setModeChoice(playerA, null); // ignore last one mode + setModeChoice(playerA, TestPlayer.MODE_SKIP); // ignore last one mode setStopAt(1, PhaseStep.END_COMBAT); execute(); @@ -116,7 +117,7 @@ public class BrutalExpulsionTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Brutal Expulsion", "mode=2Gideon, Ally of Zendikar"); setModeChoice(playerA, "2"); - setModeChoice(playerA, null); // ignore last one mode + setModeChoice(playerA, TestPlayer.MODE_SKIP); // ignore last one mode castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shock", "Gideon, Ally of Zendikar"); setStopAt(1, PhaseStep.END_COMBAT); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/GeodeGolemTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/GeodeGolemTest.java index fe28717b08c..a15c9f866aa 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/GeodeGolemTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/GeodeGolemTest.java @@ -27,8 +27,8 @@ public class GeodeGolemTest extends CardTestCommanderDuelBase { // attack and cast first time (free) attack(1, playerA, "Geode Golem"); - setChoice(playerA, true); // cast commander setChoice(playerA, "Grizzly Bears"); // commander choice + setChoice(playerA, true); // cast commander waitStackResolved(1, PhaseStep.COMBAT_DAMAGE); checkPermanentCount("after 1", 1, PhaseStep.COMBAT_DAMAGE, playerA, "Grizzly Bears", 1); checkPermanentTapped("after 1", 1, PhaseStep.COMBAT_DAMAGE, playerA, "Forest", true, 0); @@ -40,8 +40,8 @@ public class GeodeGolemTest extends CardTestCommanderDuelBase { // turn 3 - second cast (1x tax) attack(3, playerA, "Geode Golem"); - setChoice(playerA, true); // cast commander setChoice(playerA, "Grizzly Bears"); // commander choice + setChoice(playerA, true); // cast commander waitStackResolved(3, PhaseStep.COMBAT_DAMAGE); checkPermanentCount("after 2", 3, PhaseStep.COMBAT_DAMAGE, playerA, "Grizzly Bears", 1); checkPermanentTapped("after 2", 3, PhaseStep.COMBAT_DAMAGE, playerA, "Forest", true, 2); // 1x tax @@ -53,8 +53,8 @@ public class GeodeGolemTest extends CardTestCommanderDuelBase { // turn 5 - third cast (2x tax) attack(5, playerA, "Geode Golem"); - setChoice(playerA, true); // cast commander setChoice(playerA, "Grizzly Bears"); // commander choice + setChoice(playerA, true); // cast commander waitStackResolved(5, PhaseStep.COMBAT_DAMAGE); checkPermanentCount("after 3", 5, PhaseStep.COMBAT_DAMAGE, playerA, "Grizzly Bears", 1); checkPermanentTapped("after 3", 5, PhaseStep.COMBAT_DAMAGE, playerA, "Forest", true, 2 * 2); // 2x tax @@ -85,8 +85,8 @@ public class GeodeGolemTest extends CardTestCommanderDuelBase { // attack and cast first time (free) attack(1, playerA, "Geode Golem"); - setChoice(playerA, true); // cast commander setChoice(playerA, "Akoum Warrior"); // commander choice + setChoice(playerA, true); // cast commander waitStackResolved(1, PhaseStep.COMBAT_DAMAGE); checkPermanentCount("after 1", 1, PhaseStep.COMBAT_DAMAGE, playerA, "Akoum Warrior", 1); checkPermanentTapped("after 1", 1, PhaseStep.COMBAT_DAMAGE, playerA, "Mountain", true, 0); @@ -98,8 +98,8 @@ public class GeodeGolemTest extends CardTestCommanderDuelBase { // turn 3 - second cast (1x tax) attack(3, playerA, "Geode Golem"); - setChoice(playerA, true); // cast commander setChoice(playerA, "Akoum Warrior"); // commander choice + setChoice(playerA, true); // cast commander waitStackResolved(3, PhaseStep.COMBAT_DAMAGE); checkPermanentCount("after 2", 3, PhaseStep.COMBAT_DAMAGE, playerA, "Akoum Warrior", 1); checkPermanentTapped("after 2", 3, PhaseStep.COMBAT_DAMAGE, playerA, "Mountain", true, 2); // 1x tax @@ -111,8 +111,8 @@ public class GeodeGolemTest extends CardTestCommanderDuelBase { // turn 5 - third cast (2x tax) attack(5, playerA, "Geode Golem"); - setChoice(playerA, true); // cast commander setChoice(playerA, "Akoum Warrior"); // commander choice + setChoice(playerA, true); // cast commander waitStackResolved(5, PhaseStep.COMBAT_DAMAGE); checkPermanentCount("after 3", 5, PhaseStep.COMBAT_DAMAGE, playerA, "Akoum Warrior", 1); checkPermanentTapped("after 3", 5, PhaseStep.COMBAT_DAMAGE, playerA, "Mountain", true, 2 * 2); // 2x tax @@ -144,8 +144,8 @@ public class GeodeGolemTest extends CardTestCommanderDuelBase { // attack and cast first time (free) attack(1, playerA, "Geode Golem"); - setChoice(playerA, true); // cast commander setChoice(playerA, "Birgi, God of Storytelling"); // commander choice + setChoice(playerA, true); // cast commander setChoice(playerA, "Cast Birgi, God of Storytelling"); // spell choice waitStackResolved(1, PhaseStep.COMBAT_DAMAGE); checkPermanentCount("after 1", 1, PhaseStep.COMBAT_DAMAGE, playerA, "Birgi, God of Storytelling", 1); @@ -158,8 +158,8 @@ public class GeodeGolemTest extends CardTestCommanderDuelBase { // turn 3 - second cast, LEFT side (1x tax) attack(3, playerA, "Geode Golem"); - setChoice(playerA, true); // cast commander setChoice(playerA, "Birgi, God of Storytelling"); // commander choice + setChoice(playerA, true); // cast commander setChoice(playerA, "Cast Birgi, God of Storytelling"); // spell choice waitStackResolved(3, PhaseStep.COMBAT_DAMAGE); checkPermanentCount("after 2", 3, PhaseStep.COMBAT_DAMAGE, playerA, "Birgi, God of Storytelling", 1); @@ -172,8 +172,8 @@ public class GeodeGolemTest extends CardTestCommanderDuelBase { // turn 5 - third cast, RIGHT side (2x tax) attack(5, playerA, "Geode Golem"); - setChoice(playerA, true); // cast commander setChoice(playerA, "Birgi, God of Storytelling"); // commander choice + setChoice(playerA, true); // cast commander setChoice(playerA, "Cast Harnfel, Horn of Bounty"); // spell choice waitStackResolved(5, PhaseStep.COMBAT_DAMAGE); checkPermanentCount("after 3", 5, PhaseStep.COMBAT_DAMAGE, playerA, "Harnfel, Horn of Bounty", 1); @@ -186,8 +186,8 @@ public class GeodeGolemTest extends CardTestCommanderDuelBase { // turn 7 - fourth cast, RIGHT side (3x tax) attack(7, playerA, "Geode Golem"); - setChoice(playerA, true); // cast commander setChoice(playerA, "Birgi, God of Storytelling"); // commander choice + setChoice(playerA, true); // cast commander setChoice(playerA, "Cast Harnfel, Horn of Bounty"); // spell choice waitStackResolved(7, PhaseStep.COMBAT_DAMAGE); checkPermanentCount("after 4", 7, PhaseStep.COMBAT_DAMAGE, playerA, "Harnfel, Horn of Bounty", 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/clu/SludgeTitanTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/clu/SludgeTitanTest.java index 9b22b8e5954..67d4275faee 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/clu/SludgeTitanTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/clu/SludgeTitanTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.single.clu; import mage.constants.PhaseStep; @@ -6,6 +5,7 @@ import mage.constants.Zone; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -41,7 +41,6 @@ public class SludgeTitanTest extends CardTestPlayerBase { addCard(Zone.LIBRARY, playerA, divination, 5); attack(1, playerA, titan, playerB); - setChoice(playerA, playerA.CHOICE_SKIP); setStopAt(1, PhaseStep.DECLARE_BLOCKERS); execute(); @@ -49,29 +48,6 @@ public class SludgeTitanTest extends CardTestPlayerBase { assertGraveyardCount(playerA, divination, 5); } - @Test - public void testNoValidChoiceInvalid() { - setStrictChooseMode(true); - skipInitShuffling(); - - addCard(Zone.BATTLEFIELD, playerA, titan); - addCard(Zone.LIBRARY, playerA, divination, 5); - - attack(1, playerA, titan, playerB); - setChoice(playerA, divination); // invalid, titan doesn't allow for choosing sorcery - - setStopAt(1, PhaseStep.DECLARE_BLOCKERS); - - try { - execute(); - Assert.fail("must throw exception on execute"); - } catch (Throwable e) { - if (!e.getMessage().startsWith("Missing CHOICE def")) { - Assert.fail("Unexpected exception " + e.getMessage()); - } - } - } - @Test public void testCreatureOnly_ChooseNone() { setStrictChooseMode(true); @@ -81,7 +57,7 @@ public class SludgeTitanTest extends CardTestPlayerBase { addCard(Zone.LIBRARY, playerA, piker, 5); attack(1, playerA, titan, playerB); - setChoice(playerA, playerA.CHOICE_SKIP); + setChoice(playerA, TestPlayer.CHOICE_SKIP); setStopAt(1, PhaseStep.DECLARE_BLOCKERS); execute(); @@ -207,6 +183,8 @@ public class SludgeTitanTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, titan); addCard(Zone.LIBRARY, playerA, brownscale, 5); + // must able to select only 1 creature, so after first choice it must auto-finish + // if you see miss choice here then something broken in TestPlayer's choose method attack(1, playerA, titan, playerB); setChoice(playerA, brownscale); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java index 5ad1afd3b6f..3f76a1a73b6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java @@ -2,7 +2,6 @@ package org.mage.test.cards.single.dmu; import mage.constants.PhaseStep; import mage.constants.Zone; -import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -76,4 +75,26 @@ public class KarnsSylexTest extends CardTestPlayerBase { assertLife(playerB, 20 - 3); assertGraveyardCount(playerA, "Lightning Bolt", 1); } + + /** + * Test that it does not work with mana abilities, e.g. Thran Portal, with Yasharn, Implacable Earth. + */ + @Test + public void blockedManaAbilitiesWithYasharn() { + addCard(Zone.HAND, playerA, "Thran Portal"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, karnsSylex); + addCard(Zone.BATTLEFIELD, playerA, "Yasharn, Implacable Earth"); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thran Portal"); + setChoice(playerA, "Thran"); + setChoice(playerA, "Mountain"); + + checkPlayableAbility("restricted by Yasharn", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", false); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertLife(playerA, 20); + assertLife(playerB, 20); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java index 214a6985c81..881f45d1e0d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java @@ -5,8 +5,6 @@ import mage.constants.Zone; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; -import java.lang.annotation.Target; - /** * {@link mage.cards.t.ThranPortal Thran Portal} * Land Gate diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/emn/TamiyoFieldResearcherTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/emn/TamiyoFieldResearcherTest.java index 8f5316c9411..31a24c63200 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/emn/TamiyoFieldResearcherTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/emn/TamiyoFieldResearcherTest.java @@ -78,7 +78,7 @@ public class TamiyoFieldResearcherTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Choose up to two"); addTarget(playerA, "Bronze Sable"); - addTarget(playerA, TestPlayer.TARGET_SKIP); + //addTarget(playerA, TestPlayer.TARGET_SKIP); // there are only 1 creature, so choose 1 of 2, no need in skip attack(1, playerA, "Bronze Sable"); @@ -240,7 +240,7 @@ public class TamiyoFieldResearcherTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tamiyo, Field Researcher", true); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Choose up to two"); addTarget(playerA, "Bronze Sable"); - addTarget(playerA, TestPlayer.TARGET_SKIP); + //addTarget(playerA, TestPlayer.TARGET_SKIP); // there are only 1 creature, so choose 1 of 2, no need in skip attack(2, playerB, "Bronze Sable"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/fin/GarnetPrincessOfAlexandriaTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/fin/GarnetPrincessOfAlexandriaTest.java new file mode 100644 index 00000000000..9f308e91789 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/fin/GarnetPrincessOfAlexandriaTest.java @@ -0,0 +1,91 @@ +package org.mage.test.cards.single.fin; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class GarnetPrincessOfAlexandriaTest extends CardTestPlayerBase { + + @Test + public void test_Targets_0() { + // 2/2 + // Whenever Garnet attacks, you may remove a lore counter from each of any number of Sagas you control. + // Put a +1/+1 counter on Garnet for each lore counter removed this way. + addCard(Zone.BATTLEFIELD, playerA, "Garnet, Princess of Alexandria"); + + // nothing to select after attack + attack(1, playerA, "Garnet, Princess of Alexandria", playerB); + //setChoice(playerA, TestPlayer.CHOICE_SKIP); // no valid choices, so do not show choose dialog + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 2); + } + + @Test + public void test_Targets_1() { + // 2/2 + // Whenever Garnet attacks, you may remove a lore counter from each of any number of Sagas you control. + // Put a +1/+1 counter on Garnet for each lore counter removed this way. + addCard(Zone.BATTLEFIELD, playerA, "Garnet, Princess of Alexandria"); + // + // (As this Saga enters and after your draw step, add a lore counter. Sacrifice after IV.) + // I, II, III, IV -- Stampede! -- Other creatures you control get +1/+0 until end of turn. + addCard(Zone.HAND, playerA, "Summon: Choco/Mog"); // {2}{W} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + + // prepare x1 saga + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Summon: Choco/Mog"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // can select 1 target only + attack(1, playerA, "Garnet, Princess of Alexandria", playerB); + setChoice(playerA, "Summon: Choco/Mog"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.PRECOMBAT_MAIN); + execute(); + + // 2/2 attack + 1/1 saga's boost + 1/0 counter remove boost + assertCounterCount(playerA, "Summon: Choco/Mog", CounterType.LORE, 0); + assertLife(playerB, 20 - 2 - 1 - 1); + } + + @Test + public void test_Targets_2() { + // 2/2 + // Whenever Garnet attacks, you may remove a lore counter from each of any number of Sagas you control. + // Put a +1/+1 counter on Garnet for each lore counter removed this way. + addCard(Zone.BATTLEFIELD, playerA, "Garnet, Princess of Alexandria"); + // + // (As this Saga enters and after your draw step, add a lore counter. Sacrifice after IV.) + // I, II, III, IV -- Stampede! -- Other creatures you control get +1/+0 until end of turn. + addCard(Zone.HAND, playerA, "Summon: Choco/Mog", 2); // {2}{W} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3 * 2); + + // prepare x2 sagas + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Summon: Choco/Mog"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Summon: Choco/Mog"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // can select 2 targets + attack(1, playerA, "Garnet, Princess of Alexandria", playerB); + setChoice(playerA, "Summon: Choco/Mog", 2); // x2 targets + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.PRECOMBAT_MAIN); + execute(); + + // 2/2 attack + x2 1/1 saga's boost + x2 1/0 counter remove boost + assertCounterCount(playerA, "Summon: Choco/Mog", CounterType.LORE, 0); + assertLife(playerB, 20 - 2 - 2 - 2); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/WandOfVertebraeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/WandOfVertebraeTest.java index 66824c5bb01..be2d8afaacb 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/WandOfVertebraeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/grn/WandOfVertebraeTest.java @@ -27,13 +27,13 @@ public class WandOfVertebraeTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, wandOfVertebrae); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); addCard(Zone.GRAVEYARD, playerA, lavaCoil); - - setStrictChooseMode(true); + addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 10); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, {T}"); addTarget(playerA, lavaCoil); addTarget(playerA, TestPlayer.TARGET_SKIP); // must choose 1 of 5 + setStrictChooseMode(true); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/gtc/DiluvianPrimordialTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/gtc/DiluvianPrimordialTest.java index 8d8353cf3a1..cb5d923bdd2 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/gtc/DiluvianPrimordialTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/gtc/DiluvianPrimordialTest.java @@ -20,9 +20,9 @@ public class DiluvianPrimordialTest extends CardTestPlayerBase { */ private static final String primordial = "Diluvian Primordial"; - // Bug: NPE on casting Valakut Awakening @Test public void test_MDFC() { + // possible bug: NPE on casting Valakut Awakening setStrictChooseMode(true); addCard(Zone.HAND, playerA, primordial); @@ -32,7 +32,7 @@ public class DiluvianPrimordialTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, primordial); addTarget(playerA, "Valakut Awakening"); setChoice(playerA, true); // Yes to "You may" - setChoice(playerA, TestPlayer.CHOICE_SKIP); // No choice for Awakening's effect + //setChoice(playerA, TestPlayer.CHOICE_SKIP); // no need in skip, cause no valid choices for Awakening's effect setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lcc/RipplesOfPotentialTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lcc/RipplesOfPotentialTest.java index 15473da96c3..fc073e3f4b8 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/lcc/RipplesOfPotentialTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lcc/RipplesOfPotentialTest.java @@ -4,6 +4,7 @@ import mage.constants.PhaseStep; import mage.constants.Zone; import mage.counters.CounterType; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -32,9 +33,10 @@ public class RipplesOfPotentialTest extends CardTestPlayerBase { setChoice(playerA, "Blast Zone^Arcbound Javelineer^Chandra, Pyromaster^Atraxa's Skitterfang"); // Phase out choice setChoice(playerA, "Blast Zone^Arcbound Javelineer^Chandra, Pyromaster"); + setChoice(playerA, TestPlayer.CHOICE_SKIP); // keep Atraxa's Skitterfang + setChoice(playerA, false); // Atraxa's Skitterfang ability - do not remove oil setStrictChooseMode(true); - setChoice(playerA, false); // Atraxa's Skitterfang ability setStopAt(1, PhaseStep.END_TURN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/BreakingOfTheFellowshipTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/BreakingOfTheFellowshipTest.java new file mode 100644 index 00000000000..6abe5a0d95a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/BreakingOfTheFellowshipTest.java @@ -0,0 +1,83 @@ +package org.mage.test.cards.single.ltr; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ + +public class BreakingOfTheFellowshipTest extends CardTestPlayerBaseWithAIHelps { + + @Test + public void test_PossibleTargets() { + // Target creature an opponent controls deals damage equal to its power to another target creature that player controls. + addCard(Zone.HAND, playerA, "Breaking of the Fellowship"); // {1}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // + addCard(Zone.HAND, playerB, "Grizzly Bears"); // 2/2, {1}{G} + addCard(Zone.HAND, playerB, "Spectral Bears"); // 3/3, {1}{G} + addCard(Zone.BATTLEFIELD, playerB, "Forest", 2 * 2); + + // turn 1 - no targets, can't play + checkPlayableAbility("no targets - can't cast", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Breaking of the Fellowship", false); + + // turn 3 - 1 of 2 targets, can't play + castSpell(3 - 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Grizzly Bears"); + checkPlayableAbility("1 of 2 targets - can't cast", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Breaking of the Fellowship", false); + + // turn 5 - 2 of 2 targets, can play + castSpell(5 - 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Spectral Bears"); + checkPlayableAbility("2 of 2 targets - can cast", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Breaking of the Fellowship", true); + + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Breaking of the Fellowship", "Grizzly Bears^Spectral Bears"); + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.END_TURN); + execute(); + + assertDamageReceived(playerB, "Spectral Bears", 2); + } + + @Test + public void test_Normal_AI() { + // Target creature an opponent controls deals damage equal to its power to another target creature that player controls. + addCard(Zone.HAND, playerA, "Breaking of the Fellowship"); // {1}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears"); // 3/3 + + // AI must choose 3/3 to kill 2/2 + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, "Grizzly Bears", 1); + } + + @Test + public void test_Protection_AI() { + // Target creature an opponent controls deals damage equal to its power to another target creature that player controls. + addCard(Zone.HAND, playerA, "Breaking of the Fellowship"); // {1}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears"); // 3/3 + addCard(Zone.BATTLEFIELD, playerB, "Lavinia of the Tenth"); // 4/4, Protection from red + + // can't choose 4/4 to kill 3/3 due protection from red + // AI must choose 3/3 to kill 2/2 + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, "Grizzly Bears", 1); + } +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/GlamdringTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/GlamdringTest.java index 030bdfe7d6b..a469a2d31c6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/GlamdringTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/GlamdringTest.java @@ -2,6 +2,7 @@ package org.mage.test.cards.single.ltr; import mage.constants.PhaseStep; import mage.constants.Zone; +import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -29,14 +30,16 @@ public class GlamdringTest extends CardTestPlayerBase { attack(1, playerA, "Blur Sliver"); setChoice(playerA, "In Garruk's Wake"); // 9 mana, so we shouldn't be able to choose it - setChoice(playerA, "Yes"); try { setStopAt(1, PhaseStep.FIRST_COMBAT_DAMAGE); execute(); } catch (AssertionError e) { - assert(e.getMessage().contains("Missing CHOICE def for turn 1, step FIRST_COMBAT_DAMAGE, PlayerA")); + Assert.assertTrue( + "catch wrong exception: " + e.getMessage(), + e.getMessage().contains("Found wrong choice command") && e.getMessage().contains("In Garruk's Wake") + ); return; } fail("Was able to pick [[In Garruk's Wake]] but it costs more than 2"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/EnthrallingHoldTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/EnthrallingHoldTest.java index 1d135208a16..a1dd9da6925 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/EnthrallingHoldTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/m21/EnthrallingHoldTest.java @@ -10,6 +10,14 @@ public class EnthrallingHoldTest extends CardTestPlayerBase { @Test public void testTappedTarget_untapped_doesNotFizzle() { // Traxos, Scourge of Kroog enters the battlefield tapped and doesn't untap during your untap step. + + // The middle ability of Enthralling Hold affects only the choice of target as the spell is cast. + // If the creature becomes untapped before the spell resolves, it still resolves. If a player is + // allowed to change the spell's target while it's on the stack, they may choose an untapped + // creature. If you put Enthralling Hold onto the battlefield without casting it, you may attach + // it to an untapped creature. + // (2020-06-23) + addCard(Zone.BATTLEFIELD, playerB, "Traxos, Scourge of Kroog"); addCard(Zone.BATTLEFIELD, playerA, "Island", 6); @@ -26,10 +34,11 @@ public class EnthrallingHoldTest extends CardTestPlayerBase { */ addCard(Zone.HAND, playerA, "Twiddle"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Enthralling Hold", "Traxos, Scourge of Kroog"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Twiddle", "Traxos, Scourge of Kroog"); - - setChoice(playerA, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Enthralling Hold"); + addTarget(playerA, "Traxos, Scourge of Kroog"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Twiddle"); + addTarget(playerA, "Traxos, Scourge of Kroog"); + setChoice(playerA, true); // untap traxos setStrictChooseMode(true); setStopAt(1, PhaseStep.END_COMBAT); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/BarrowgoyfTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/BarrowgoyfTest.java index e561f89d04e..755799fee58 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/BarrowgoyfTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/BarrowgoyfTest.java @@ -100,7 +100,6 @@ public class BarrowgoyfTest extends CardTestPlayerBase { attack(1, playerA, barrowgoyf, playerB); setChoice(playerA, true); - setChoice(playerA, TestPlayer.CHOICE_SKIP); // decide to not return. There was no choice anyway. setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mat/NahiriForgedInFuryTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mat/NahiriForgedInFuryTest.java new file mode 100644 index 00000000000..39cce6a4160 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mat/NahiriForgedInFuryTest.java @@ -0,0 +1,98 @@ +package org.mage.test.cards.single.mat; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + + +/** + * @author correl + */ +public class NahiriForgedInFuryTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.n.NahiriForgedInFury} {4}{R}{W} + * Legendary Creature - Kor Artificer + * + * Affinity for Equipment + * + * Whenever an equipped creature you control attacks, exile the top card of + * your library. You may play that card this turn. You may cast Equipment + * spells this way without paying their mana costs. + */ + private static final String nahiri = "Nahiri, Forged in Fury"; + + private final String boots = "Swiftfoot Boots"; + private final String cleaver = "The Reaver Cleaver"; + private final String giant = "Hill Giant"; + private final String greaves = "Lightning Greaves"; + private final String lions = "Savannah Lions"; + private final String sword = "Sword of Feast and Famine"; + + @Test + public void test_CostReducedByEquipment() { + // Nahiri in hand, four equipment in play, and enough to pay RW + addCard(Zone.HAND, playerA, nahiri); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerA, boots); + addCard(Zone.BATTLEFIELD, playerA, cleaver); + addCard(Zone.BATTLEFIELD, playerA, greaves); + addCard(Zone.BATTLEFIELD, playerA, sword); + + // Cast for RW (Reduced by 4) + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, nahiri); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + + @Test + public void test_EquippedAttackTriggers() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, nahiri); + addCard(Zone.BATTLEFIELD, playerA, lions); + addCard(Zone.BATTLEFIELD, playerA, giant); + addCard(Zone.BATTLEFIELD, playerA, boots); + addCard(Zone.BATTLEFIELD, playerA, greaves); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip {0}", lions); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip {1}", giant); + + // Attack with three creatures, two of which are equipped + attack(1, playerA, nahiri); + attack(1, playerA, lions); + attack(1, playerA, giant); + + // Order the 2 Nahiri triggers + setChoice(playerA, "Whenever an equipped creature you control attacks"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // Triggered twice, exiling two cards + assertExileCount(playerA, 2); + } + + @Test + public void test_CanCastExiledEquipmentForFree() { + addCard(Zone.BATTLEFIELD, playerA, nahiri); + addCard(Zone.BATTLEFIELD, playerA, greaves); + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, sword); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip {0}", nahiri); + + // Attack with one equipped creature, exiling the sword + attack(1, playerA, nahiri); + + // Cast sword for free + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, sword); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/IzzetGeneratoriumTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/IzzetGeneratoriumTest.java index 4141d0e485d..2eb0d77ac76 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/IzzetGeneratoriumTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/IzzetGeneratoriumTest.java @@ -6,6 +6,7 @@ import mage.counters.CounterType; import mage.players.Player; import org.junit.Assert; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -82,6 +83,7 @@ public class IzzetGeneratoriumTest extends CardTestPlayerBase { checkPlayableAbility("1: condition not met before losing counters", 2, PhaseStep.UPKEEP, playerA, "{T}: Draw", false); castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Final Act"); setModeChoice(playerB, "5"); // each opponent loses all counters. + setModeChoice(playerB, TestPlayer.MODE_SKIP); waitStackResolved(2, PhaseStep.PRECOMBAT_MAIN); runCode("energy counter is 0", 2, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> checkEnergyCount(info, player, 0)); checkPlayableAbility("2: condition met after losing counters", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Draw", true); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/NethergoyfTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/NethergoyfTest.java index 658445c4daa..1edd03bd76b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/NethergoyfTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/NethergoyfTest.java @@ -183,6 +183,7 @@ public class NethergoyfTest extends CardTestPlayerBaseWithAIHelps { // The same is true for permanent spells you control and nonland permanent cards you own that aren’t on the battlefield. addCard(Zone.HAND, playerA, "Encroaching Mycosynth"); + // AI must be able to choose good targets combination aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); setStopAt(1, PhaseStep.BEGIN_COMBAT); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/KayaSpiritsJusticeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/KayaSpiritsJusticeTest.java index 20cc546706d..119db90c629 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/KayaSpiritsJusticeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/KayaSpiritsJusticeTest.java @@ -1,9 +1,13 @@ package org.mage.test.cards.single.mkm; +import mage.abilities.Mode; +import mage.abilities.effects.common.ExileAllEffect; import mage.abilities.keyword.FlyingAbility; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.filter.StaticFilters; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -21,6 +25,11 @@ public class KayaSpiritsJusticeTest extends CardTestPlayerBase { addCard(Zone.GRAVEYARD, playerA, "Fyndhorn Elves"); addCard(Zone.HAND, playerA, "Thraben Inspector"); addCard(Zone.HAND, playerA, "Astrid Peth"); + // Choose one or more — + // • Exile all artifacts. + // • Exile all creatures. + // • Exile all enchantments. + // • Exile all graveyards. addCard(Zone.HAND, playerA, "Farewell"); // Creates a Clue token @@ -35,6 +44,7 @@ public class KayaSpiritsJusticeTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Farewell"); setModeChoice(playerA, "2"); setModeChoice(playerA, "4"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); // Kaya's first ability triggers twice, so choose which is put on the stack: diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mom/InvasionOfFioraTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mom/InvasionOfFioraTest.java index e312c636ce9..88a9e75abd7 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mom/InvasionOfFioraTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mom/InvasionOfFioraTest.java @@ -1,6 +1,7 @@ package org.mage.test.cards.single.mom; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; import mage.constants.PhaseStep; @@ -47,6 +48,7 @@ public class InvasionOfFioraTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, invasion); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); setModeChoice(playerA, "1"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); checkPermanentCount("Battle on battlefield", 1, PhaseStep.PRECOMBAT_MAIN, playerA, invasion, 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java index 2daabf2511d..a17b75ab343 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java @@ -1,9 +1,16 @@ package org.mage.test.cards.single.znr; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.FlyingAbility; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.permanent.token.FoodToken; +import mage.game.permanent.token.TreasureToken; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -23,21 +30,26 @@ public class YasharnImplacableEarthTest extends CardTestPlayerBase { * Test that players can't pay life to cast a spell. */ @Test - @Ignore public void cantPayLifeToCast() { // {W}{B} // As an additional cost to cast this spell, pay 5 life or sacrifice a creature or enchantment. // Destroy target creature. addCard(Zone.HAND, playerA, "Final Payment"); + //{4}{B/P}{B/P}{B/P} + //Legendary Creature — Phyrexian Horror Minion + //2/2 + //Lifelink + //For each {B} in a cost, you may pay 2 life rather than pay that mana. + //Whenever you cast a black spell, put a +1/+1 counter on K'rrik. + addCard(Zone.HAND, playerA, "K'rrik, Son of Yawgmoth"); addCard(Zone.BATTLEFIELD, playerA, yasharn); - addCard(Zone.BATTLEFIELD, playerA, "Swamp"); - addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); setStrictChooseMode(true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Final Payment", yasharn); - setChoice(playerA, "No"); - setChoice(playerA, "Silvercoat Lion"); + checkPlayableAbility("Can't cast Final Payment", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Final Payment", false); + checkPlayableAbility("Can't cast K'rrik", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast K'rrik, Son of Yawgmoth", false); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); @@ -71,22 +83,20 @@ public class YasharnImplacableEarthTest extends CardTestPlayerBase { * Test that players can't sacrifice a nonland permanent to cast a spell. */ @Test - @Ignore public void cantSacrificeNonlandToCast() { // {1}{B} // As an additional cost to cast this spell, sacrifice an artifact or creature. // Draw two cards and create a Treasure token. addCard(Zone.HAND, playerA, "Deadly Dispute"); addCard(Zone.BATTLEFIELD, playerA, yasharn); + addCard(Zone.BATTLEFIELD, playerA, "Bear Cub"); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); setStrictChooseMode(true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Deadly Dispute"); - setChoice(playerA, yasharn); + checkPlayableAbility("Can't cast Deadly Dispute", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Deadly Dispute", false); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); - assertPermanentCount(playerA, "Treasure Token", 0); } /** @@ -117,9 +127,13 @@ public class YasharnImplacableEarthTest extends CardTestPlayerBase { */ @Test public void canSacrificeLandToCast() { + //Crop Rotation + //{G} + //As an additional cost to cast this spell, sacrifice a land. + //Search your library for a land card, put that card onto the battlefield, then shuffle. + addCard(Zone.HAND, playerA, "Crop Rotation"); addCard(Zone.BATTLEFIELD, playerA, yasharn); addCard(Zone.BATTLEFIELD, playerA, "Forest"); - addCard(Zone.HAND, playerA, "Crop Rotation"); addCard(Zone.LIBRARY, playerA, "Mountain"); setStrictChooseMode(true); @@ -156,4 +170,159 @@ public class YasharnImplacableEarthTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Island", 1); assertGraveyardCount(playerA, "Evolving Wilds", 1); } + + /** + * Test that a player cannot sacrifice artifacts or creatures to activate abilities + */ + @Test + public void cantSacToMondrakWithArtifacts() { + setStrictChooseMode(true); + //Mondrak, Glory Dominus + //{2}{W}{W} + //Legendary Creature — Phyrexian Horror + //If one or more tokens would be created under your control, twice that many of those tokens are created instead. + //{1}{W/P}{W/P}, Sacrifice two other artifacts and/or creatures: Put an indestructible counter on Mondrak. + String mondrak = "Mondrak, Glory Dominus"; + + Ability ability = new SimpleActivatedAbility( + Zone.ALL, + new CreateTokenEffect(new TreasureToken(), 2), + new ManaCostsImpl<>("") + ); + ability.addEffect(new CreateTokenEffect(new FoodToken(), 2)); + + addCustomCardWithAbility("Token-maker", playerA, ability); + addCard(Zone.BATTLEFIELD, playerA, mondrak); + addCard(Zone.BATTLEFIELD, playerA, yasharn); + addCard(Zone.BATTLEFIELD, playerA, "Bear Cub", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + + checkPlayableAbility("Can't activate Mondrak with creatures", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W/P}{W/P}, Sacrifice", false); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "create two"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + checkPlayableAbility("Can't activate Mondrak with creatures or artifacts", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W/P}{W/P}, Sacrifice", false); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Bear Cub", 2); + assertPermanentCount(playerA, "Treasure Token", 4); + assertPermanentCount(playerA, "Food Token", 4); + } + + @Test + public void canSacrificeTriggeredAbility() { + /* + Unscrupulous Contractor + {2}{B} + Creature — Human Assassin + When this creature enters, you may sacrifice a creature. When you do, target player draws two cards and loses 2 life. + Plot {2}{B} + 3/2 + */ + String contractor = "Unscrupulous Contractor"; + /* + Bear Cub + {1}{G} + Creature - Bear + 2/2 + */ + String cub = "Bear Cub"; + + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, yasharn); + addCard(Zone.HAND, playerA, contractor); + addCard(Zone.HAND, playerB, contractor); + addCard(Zone.BATTLEFIELD, playerA, cub); + addCard(Zone.BATTLEFIELD, playerB, cub); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, contractor); + setChoice(playerA, true); + setChoice(playerA, cub); + addTarget(playerA, playerA); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, contractor); + setChoice(playerB, true); + setChoice(playerB, cub); + addTarget(playerB, playerB); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 2); + assertLife(playerA, 20 - 2); + assertGraveyardCount(playerA, cub, 1); + + assertHandCount(playerB, 1 + 2); //draw + contractor effect + assertLife(playerB, 20 - 2); + assertGraveyardCount(playerB, cub, 1); + } + + @Test + public void canPayLifeForTriggeredAbility() { + /* + Arrogant Poet + {1}{B} + Creature — Human Warlock + Whenever this creature attacks, you may pay 2 life. If you do, it gains flying until end of turn. + 2/1 + */ + String poet = "Arrogant Poet"; + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, poet); + addCard(Zone.BATTLEFIELD, playerA, yasharn); + + attack(1, playerA, poet); + setChoice(playerA, true); // pay 2 life + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 2); // combat damage + assertLife(playerA, 20 - 2); // paid life + assertAbility(playerA, poet, FlyingAbility.getInstance(), true); + } + + @Test + public void canSacWithGrist() { + /* + Grist, the Hunger Tide + {1}{B}{G} + Legendary Planeswalker — Grist + As long as Grist isn’t on the battlefield, it’s a 1/1 Insect creature in addition to its other types. + +1: Create a 1/1 black and green Insect creature token, then mill a card. If an Insect card was milled this way, put a loyalty counter on Grist and repeat this process. + −2: You may sacrifice a creature. When you do, destroy target creature or planeswalker. + −5: Each opponent loses life equal to the number of creature cards in your graveyard. + Loyalty: 3 + */ + String grist = "Grist, the Hunger Tide"; + /* + Bear Cub + {1}{G} + Creature - Bear + 2/2 + */ + String cub = "Bear Cub"; + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, grist); + addCard(Zone.BATTLEFIELD, playerA, yasharn); + addCard(Zone.BATTLEFIELD, playerA, cub); + addCard(Zone.BATTLEFIELD, playerB, grist + "@gristB"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-2:"); + setChoice(playerA, true); + setChoice(playerA, cub); + addTarget(playerA, "@gristB"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertCounterCount(grist, CounterType.LOYALTY, 1); + assertGraveyardCount(playerB, grist, 1); + assertGraveyardCount(playerA, cub, 1); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionBaseTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionBaseTest.java index 7681ef14fd4..2863182baec 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionBaseTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetsSelectionBaseTest.java @@ -54,11 +54,10 @@ public class TargetsSelectionBaseTest extends CardTestPlayerBaseWithAIHelps { checkHandCardCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", availableCardsCount); checkExileCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", 0); + // cause minTarget = 0, so ability can be activated at any time + checkPlayableAbility("can activate on any targets", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, true); if (availableCardsCount > 0) { - checkPlayableAbility("can activate on non-zero targets", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, true); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText); - } else { - checkPlayableAbility("can't activate on zero targets", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, false); } if (chooseCardsCount > 0) { @@ -74,7 +73,8 @@ public class TargetsSelectionBaseTest extends CardTestPlayerBaseWithAIHelps { // - 2 of 3 - yes // - 3 of 3 - no, it's auto-finish on last select // - 3 of 5 - no, it's auto-finish on last select - if (chooseCardsCount < maxTarget) { + int maxPossibleChoose = Math.min(availableCardsCount, maxTarget); + if (chooseCardsCount < maxPossibleChoose) { addTarget(playerA, TestPlayer.TARGET_SKIP); } } else { @@ -128,20 +128,18 @@ public class TargetsSelectionBaseTest extends CardTestPlayerBaseWithAIHelps { targetCards.add("Swamp"); }); setChoice(playerA, String.join("^", targetCards)); - } else { - // need skip - // on 0 cards there are must be dialog with done button anyway - - // end selection: - // - x of 0 - yes - // - 1 of 3 - yes - // - 2 of 3 - yes - // - 3 of 3 - no, it's auto-finish on last select - // - 3 of 5 - no, it's auto-finish on last select - if (chooseCardsCount < maxTarget) { - setChoice(playerA, TestPlayer.CHOICE_SKIP); - } + } + // choose skip + // end selection condition: + // - x of 0 - yes + // - 1 of 3 - yes + // - 2 of 3 - yes + // - 3 of 3 - no, it's auto-finish on last select + // - 3 of 5 - no, it's auto-finish on last select and nothing to choose + int canSelectCount = Math.min(maxTarget, availableCardsCount); + if (canSelectCount > 0 && chooseCardsCount < canSelectCount) { + setChoice(playerA, TestPlayer.CHOICE_SKIP); } if (DEBUG_ENABLE_DETAIL_LOGS) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/BecomesTargetTriggerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/BecomesTargetTriggerTest.java index cdaed27a013..47f8a695997 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/BecomesTargetTriggerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/BecomesTargetTriggerTest.java @@ -7,6 +7,7 @@ import mage.constants.Zone; import mage.counters.CounterType; import mage.filter.Filter; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -478,6 +479,7 @@ public class BecomesTargetTriggerTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hostility); setModeChoice(playerA, "1"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); addTarget(playerA, protector); setStrictChooseMode(true); @@ -502,6 +504,7 @@ public class BecomesTargetTriggerTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hostility); setModeChoice(playerA, "2"); + setModeChoice(playerA, TestPlayer.MODE_SKIP); addTarget(playerA, dragon); setStrictChooseMode(true); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/DrawNCardsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/DrawNCardsTest.java new file mode 100644 index 00000000000..1838e9e39b7 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/DrawNCardsTest.java @@ -0,0 +1,32 @@ +package org.mage.test.cards.triggers; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class DrawNCardsTest extends CardTestPlayerBase { + + private static final String snacker = "Sneaky Snacker"; + private static final String mists = "Reach Through Mists"; + private static final String looting = "Faithless Looting"; + + @Test + public void testSnacker() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 1 + 1); + addCard(Zone.GRAVEYARD, playerA, snacker); + addCard(Zone.HAND, playerA, mists); + addCard(Zone.HAND, playerA, looting); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mists); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, looting); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTapped(snacker, true); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/EnterLeaveBattlefieldExileTargetTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/EnterLeaveBattlefieldExileTargetTest.java index 2217cb4ee82..2837a7e6380 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/EnterLeaveBattlefieldExileTargetTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/EnterLeaveBattlefieldExileTargetTest.java @@ -24,7 +24,6 @@ public class EnterLeaveBattlefieldExileTargetTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Angel of Serenity"); addTarget(playerA, "Silvercoat Lion^Pillarfield Ox"); - addTarget(playerA, TestPlayer.TARGET_SKIP); setChoice(playerA, true); setStrictChooseMode(true); diff --git a/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java b/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java index 7a41de3d740..406f8580e8b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java @@ -6,6 +6,7 @@ import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.continuous.PlayAdditionalLandsAllEffect; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.util.ConsoleUtil; import mage.utils.testers.TestableDialog; import mage.utils.testers.TestableDialogsRunner; import org.junit.Assert; @@ -107,7 +108,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps { @Test @Ignore // debug only - run single dialog by reg number public void test_RunSingle_Debugging() { - int needRegNumber = 557; + int needRegNumber = 5; prepareCards(); @@ -233,15 +234,15 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps { if (resAssert == null) { totalUnknown++; status = "?"; - coloredTexts.put("?", asYellow("?")); + coloredTexts.put("?", ConsoleUtil.asYellow("?")); } else if (resAssert.isEmpty()) { totalGood++; status = "OK"; - coloredTexts.put("OK", asGreen("OK")); + coloredTexts.put("OK", ConsoleUtil.asGreen("OK")); } else { totalBad++; status = "FAIL"; - coloredTexts.put("FAIL", asRed("FAIL")); + coloredTexts.put("FAIL", ConsoleUtil.asRed("FAIL")); assertError = resAssert; } if (!assertError.isEmpty()) { @@ -256,8 +257,8 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps { // print dialog error if (!assertError.isEmpty()) { coloredTexts.clear(); - coloredTexts.put(resAssert, asRed(resAssert)); - coloredTexts.put(resDebugSource, asRed(resDebugSource)); + coloredTexts.put(resAssert, ConsoleUtil.asRed(resAssert)); + coloredTexts.put(resDebugSource, ConsoleUtil.asRed(resDebugSource)); String badAssert = getColoredRow(totalsRightFormat, coloredTexts, resAssert); String badDebugSource = getColoredRow(totalsRightFormat, coloredTexts, resDebugSource); if (firstBadDialog == null) { @@ -283,15 +284,15 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps { String badStats = String.format("%d bad", totalBad); String unknownStats = String.format("%d unknown", totalUnknown); coloredTexts.clear(); - coloredTexts.put(goodStats, String.format("%s good", asGreen(String.valueOf(totalGood)))); - coloredTexts.put(badStats, String.format("%s bad", asRed(String.valueOf(totalBad)))); - coloredTexts.put(unknownStats, String.format("%s unknown", asYellow(String.valueOf(totalUnknown)))); + coloredTexts.put(goodStats, String.format("%s good", ConsoleUtil.asGreen(String.valueOf(totalGood)))); + coloredTexts.put(badStats, String.format("%s bad", ConsoleUtil.asRed(String.valueOf(totalBad)))); + coloredTexts.put(unknownStats, String.format("%s unknown", ConsoleUtil.asYellow(String.valueOf(totalUnknown)))); System.out.print(getColoredRow(totalsLeftFormat, coloredTexts, String.format("Total results: %s, %s, %s", goodStats, badStats, unknownStats))); // first error for fast access in big list if (totalDialogs > 1 && firstBadDialog != null) { System.out.println(horizontalBorder); - System.out.print(getColoredRow(totalsRightFormat, coloredTexts, "First bad dialog: " + firstBadDialog.getRegNumber())); + System.out.print(getColoredRow(totalsRightFormat, coloredTexts, "First bad dialog: " + firstBadDialog.getRegNumber() + " (debug it by test_RunSingle_Debugging)")); System.out.print(getColoredRow(totalsRightFormat, coloredTexts, firstBadDialog.getName() + " - " + firstBadDialog.getDescription())); System.out.print(firstBadAssert); System.out.print(firstBadDebugSource); @@ -313,16 +314,4 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps { } return line; } - - private String asRed(String text) { - return "\u001B[31m" + text + "\u001B[0m"; - } - - private String asGreen(String text) { - return "\u001B[32m" + text + "\u001B[0m"; - } - - private String asYellow(String text) { - return "\u001B[33m" + text + "\u001B[0m"; - } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index ca3a223ea3b..b60def4c78a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -16,6 +16,7 @@ import mage.cards.Cards; import mage.cards.CardsImpl; import mage.cards.decks.Deck; import mage.choices.Choice; +import mage.collectors.DataCollectorServices; import mage.constants.*; import mage.counters.Counter; import mage.counters.CounterType; @@ -41,6 +42,7 @@ import mage.game.stack.StackAbility; import mage.game.stack.StackObject; import mage.game.tournament.Tournament; import mage.player.ai.ComputerPlayer; +import mage.player.ai.PossibleTargetsSelector; import mage.players.*; import mage.players.net.UserData; import mage.target.*; @@ -51,7 +53,6 @@ import mage.util.RandomUtil; import mage.watchers.common.AttackedOrBlockedThisCombatWatcher; import org.apache.log4j.Logger; import org.junit.Assert; -import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; import java.io.Serializable; import java.util.*; @@ -59,6 +60,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; + /** * Basic implementation of testable player * @@ -72,6 +75,7 @@ public class TestPlayer implements Player { public static final String TARGET_SKIP = "[target_skip]"; // stop/skip targeting public static final String CHOICE_SKIP = "[choice_skip]"; // stop/skip choice + public static final String MODE_SKIP = "[mode_skip]"; // stop/skip mode selection public static final String CHOICE_NORMAL_COST = "Cast with no alternative cost: "; // when there is the possibility for an alternative cost, use the normal cost instead. public static final String MANA_CANCEL = "[mana_cancel]"; // cancel payment public static final String SKIP_FAILED_COMMAND = "[skip_failed_command]"; // skip next command in player's queue (can remove cast commands after try to activate) @@ -101,7 +105,10 @@ public class TestPlayer implements Player { private final List choices = new ArrayList<>(); // choices stack for choice private final List targets = new ArrayList<>(); // targets stack for choose (it's uses on empty direct target by cast command) private final Map aliases = new HashMap<>(); // aliases for game objects/players (use it for cards with same name to save and use) - private final List modesSet = new ArrayList<>(); + private final List modes = new ArrayList<>(); + + // debug only: ignore all next commands so choose dialog will raise error with full info + private boolean isSkipAllNextChooseCommands = false; private final ComputerPlayer computerPlayer; // real player @@ -144,11 +151,12 @@ public class TestPlayer implements Player { this.choices.addAll(testPlayer.choices); this.targets.addAll(testPlayer.targets); this.aliases.putAll(testPlayer.aliases); - this.modesSet.addAll(testPlayer.modesSet); + this.modes.addAll(testPlayer.modes); this.computerPlayer = testPlayer.computerPlayer.copy(); if (testPlayer.groupsForTargetHandling != null) { this.groupsForTargetHandling = testPlayer.groupsForTargetHandling.clone(); } + this.isSkipAllNextChooseCommands = testPlayer.isSkipAllNextChooseCommands; this.strictChooseMode = testPlayer.strictChooseMode; } @@ -160,6 +168,10 @@ public class TestPlayer implements Player { Assert.assertNotEquals("Choice can't be empty", "", choice); choice = EmptyNames.replaceTestCommandByObjectName(choice); + if (this.isSkipAllNextChooseCommands) { + return; + } + choices.add(choice); } @@ -184,7 +196,10 @@ public class TestPlayer implements Player { } public void addModeChoice(String mode) { - modesSet.add(mode); + if (this.isSkipAllNextChooseCommands) { + return; + } + modes.add(mode); } public void addTarget(String target) { @@ -194,6 +209,10 @@ public class TestPlayer implements Player { target = EmptyNames.replaceTestCommandByObjectName(target); + if (this.isSkipAllNextChooseCommands) { + return; + } + targets.add(target); } @@ -315,11 +334,8 @@ public class TestPlayer implements Player { return true; } else if (groups[2].startsWith("spellOnTopOfStack=")) { String spellOnTopOFStack = groups[2].substring(18); - if (!game.getStack().isEmpty()) { - StackObject stackObject = game.getStack().getFirst(); - return stackObject != null && stackObject.getStackAbility().toString().contains(spellOnTopOFStack); - } - return false; + StackObject stackObject = game.getStack().getFirstOrNull(); + return stackObject != null && stackObject.getStackAbility().toString().contains(spellOnTopOFStack); } else if (groups[2].startsWith("manaInPool=")) { String manaInPool = groups[2].substring(11); int amountOfMana = Integer.parseInt(manaInPool); @@ -366,6 +382,10 @@ public class TestPlayer implements Player { return true; // targetAmount have to be set by setTargetAmount in the test script } ability.getTargets().get(0).addTarget(player.getId(), ability, game); + + String reason = CardUtil.getSourceLogName(game, "by cast/activate, ability: ", ability, "", ""); + DataCollectorServices.getInstance().onTestsTargetUse(game, player, target, reason); + targetsSet++; break; } @@ -464,9 +484,12 @@ public class TestPlayer implements Player { int index = 0; int targetsSet = 0; for (String targetName : targetList) { + + // choose target for specific mode Mode selectedMode; if (targetName.startsWith("mode=")) { - int modeNr = Integer.parseInt(targetName.substring(5, 6)); + String modeStr = targetName.substring(5, 6); + int modeNr = Integer.parseInt(modeStr); if (modeNr == 0 || modeNr > (ability.getModes().isMayChooseSameModeMoreThanOnce() ? ability.getModes().getSelectedModes().size() : ability.getModes().size())) { throw new UnsupportedOperationException("Given mode number (" + modeNr + ") not available for " + ability.toString()); } @@ -489,18 +512,23 @@ public class TestPlayer implements Player { if (index >= selectedMode.getTargets().size()) { break; // this can happen if targets should be set but can't be used because of hexproof e.g. } + + // choose player Target currentTarget = selectedMode.getTargets().get(index); if (targetName.startsWith("targetPlayer=")) { target = targetName.substring(targetName.indexOf("targetPlayer=") + 13); for (Player player : game.getPlayers().values()) { if (player.getName().equals(target)) { currentTarget.addTarget(player.getId(), ability, game); + String reason = CardUtil.getSourceLogName(game, "by cast/activate, ability: ", ability, "", ""); + DataCollectorServices.getInstance().onTestsTargetUse(game, this, target, reason); index++; targetsSet++; break; } } } else { + // choose object boolean originOnly = false; boolean copyOnly = false; if (targetName.endsWith("]")) { @@ -551,6 +579,10 @@ public class TestPlayer implements Player { currentTarget.addTarget(id, ability, game); targetsSet++; } + + String reason = CardUtil.getSourceLogName(game, "by cast/activate, ability: ", ability, "", ""); + DataCollectorServices.getInstance().onTestsTargetUse(game, this, targetName, reason); + if (currentTarget.getTargets().size() == currentTarget.getMaxNumberOfTargets()) { index++; } @@ -628,7 +660,7 @@ public class TestPlayer implements Player { // skip failed command if (!choices.isEmpty() && choices.get(0).equals(SKIP_FAILED_COMMAND)) { actions.remove(action); - choices.remove(0); + choicesRemoveCurrent(game, "on priority"); return true; } } @@ -1267,7 +1299,7 @@ public class TestPlayer implements Player { .map(c -> (((c instanceof PermanentToken) ? "[T] " : "[C] ") + c.getIdName() + (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "") - + " class " + c.getMainCard().getClass().getSimpleName() + "" + + " class " + getSimpleClassName(c.getMainCard().getClass()) + "" + " - " + c.getPower().getValue() + "/" + c.getToughness().getValue() + (c.isPlaneswalker(game) ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "") + ", " + (c.isTapped() ? "Tapped" : "Untapped") @@ -1307,7 +1339,7 @@ public class TestPlayer implements Player { + (a.toString().startsWith("Cast ") ? "[" + a.getManaCostsToPay().getText() + "] -> " : "") // printed cost, not modified + (a.toString().length() > 0 ? CardUtil.substring(a.toString(), 40, "...") - : a.getClass().getSimpleName()) + : getSimpleClassName(a.getClass())) )) .sorted() .collect(Collectors.toList()); @@ -2091,13 +2123,13 @@ public class TestPlayer implements Player { } private String getInfo(MageObject o) { - return "Object: " + (o != null ? o.getClass().getSimpleName() + ": " + o.getName() : "null"); + return "Object: " + (o != null ? getSimpleClassName(o.getClass()) + ": " + o.getName() : "null"); } private String getInfo(Ability o, Game game) { if (o != null) { MageObject object = o.getSourceObject(game); - return "Ability: " + (object == null ? "" : object.getIdName() + " - " + o.getClass().getSimpleName() + ": " + o.getRule()); + return "Ability: " + (object == null ? "" : object.getIdName() + " - " + getSimpleClassName(o.getClass()) + ": " + o.getRule()); } return "Ability: null"; } @@ -2109,8 +2141,8 @@ public class TestPlayer implements Player { UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId()); Set possibleTargets = target.possibleTargets(abilityControllerId, source, game, cards); - return "Target: selected " + target.getSize() + ", possible " + possibleTargets.size() - + ", " + target.getClass().getSimpleName() + ": " + target.getMessage(game); + return "Target: selected " + target.getSize() + ", more possible " + possibleTargets.size() + + ", " + getSimpleClassName(target.getClass()) + ": " + target.getMessage(game); } private void assertAliasSupportInChoices(boolean methodSupportAliases) { @@ -2166,6 +2198,13 @@ public class TestPlayer implements Player { }); printEnd(); } + if (choiceType.equals("mode")) { + printStart(game, "Unused modes"); + if (!modes.isEmpty()) { + System.out.println(String.join("\n", modes)); + } + printEnd(); + } Assert.fail("Missing " + choiceType.toUpperCase(Locale.ENGLISH) + " def for" + " turn " + game.getTurnNum() + ", step " + (game.getStep() != null ? game.getTurnStepType().name() : "not started") @@ -2176,23 +2215,8 @@ public class TestPlayer implements Player { @Override public Mode chooseMode(Modes modes, Ability source, Game game) { - if (!modesSet.isEmpty() && modes.getMaxModes(game, source) > modes.getSelectedModes().size()) { - // set mode to null to select less than maximum modes if multiple modes are allowed - if (modesSet.get(0) == null) { - modesSet.remove(0); - return null; - } - int needMode = Integer.parseInt(modesSet.get(0)); - int i = 1; - for (Mode mode : modes.getAvailableModes(source, game)) { - if (i == needMode) { - modesSet.remove(0); - return mode; - } - i++; - } - } - if (modes.getMinModes() <= modes.getSelectedModes().size()) { + if (modes.getSelectedModes().size() >= modes.getMaxModes(game, source)) { + // TODO: no needs here cause min/max mode must be checked by parent code? try to remove it from here return null; } @@ -2207,6 +2231,27 @@ public class TestPlayer implements Player { i++; } + if (!this.modes.isEmpty()) { + + // skip modes + if (tryToSkipSelection(game, null, this.modes, MODE_SKIP)) { + return null; + } + + String needModeStr = this.modes.get(0); + int needModeNumber = Integer.parseInt(needModeStr); + i = 1; + for (Mode mode : modes.getAvailableModes(source, game)) { + if (i == needModeNumber) { + modesRemoveCurrent(game, mode); + return mode; + } + i++; + } + + Assert.fail("Can't use mode: " + needModeNumber + "\n" + getInfo(source, game) + modesInfo); + } + this.chooseStrictModeFailed("mode", game, getInfo(source, game) + modesInfo); return computerPlayer.chooseMode(modes, source, game); } @@ -2219,9 +2264,8 @@ public class TestPlayer implements Player { String possibleChoice = choices.get(0); // skip choices - if (possibleChoice.equals(CHOICE_SKIP)) { - choices.remove(0); - return false; // false - stop to choose + if (tryToSkipSelection(game, null, choices, CHOICE_SKIP)) { + return false; } if (choice.setChoiceByAnswers(choices, true)) { @@ -2254,7 +2298,7 @@ public class TestPlayer implements Player { int index = 0; for (Map.Entry entry : effectsMap.entrySet()) { if (entry.getValue().startsWith(choice)) { - choices.remove(0); + choicesRemoveCurrent(game, "on choose replacements"); // TODO: add short lists? return index; } index++; @@ -2269,219 +2313,7 @@ public class TestPlayer implements Player { @Override public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map options) { - - // choose itself for starting player all the time - if (target.getMessage(game).equals("Select a starting player")) { - target.add(this.getId(), game); - return true; - } - - UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId()); - - // TODO: warning, some cards call player.choose methods instead target.choose, see #8254 - // most use cases - discard and other cost with choice like that method - // must migrate all choices.remove(xxx) to choices.remove(0), takeMaxTargetsPerChoose can help to find it - - boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish - - assertAliasSupportInChoices(true); - if (!choices.isEmpty()) { - - // skip choices - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - Assert.assertTrue("found skip choice, but it require more choices, needs " - + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", - target.getTargets().size() >= target.getMinNumberOfTargets()); - return false; // false - stop to choose - } - - // TODO: Allow to choose a player with TargetPermanentOrPlayer - if ((target.getOriginalTarget() instanceof TargetPermanent) - || (target.getOriginalTarget() instanceof TargetPermanentOrPlayer)) { // player target not implemented yet - FilterPermanent filterPermanent; - if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) { - filterPermanent = ((TargetPermanentOrPlayer) target.getOriginalTarget()).getFilterPermanent(); - } else { - filterPermanent = ((TargetPermanent) target.getOriginalTarget()).getFilter(); - } - while (!choices.isEmpty()) { // TODO: remove cycle after main commits - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - return false; // stop dialog - } - String choiceRecord = choices.get(0); - String[] targetList = choiceRecord.split("\\^"); - isAddedSomething = false; - for (String targetName : targetList) { - boolean originOnly = false; - boolean copyOnly = false; - if (targetName.endsWith("]")) { - if (targetName.endsWith("[no copy]")) { - originOnly = true; - targetName = targetName.substring(0, targetName.length() - 9); - } - if (targetName.endsWith("[only copy]")) { - copyOnly = true; - targetName = targetName.substring(0, targetName.length() - 11); - } - } - for (Permanent permanent : game.getBattlefield().getActivePermanents(filterPermanent, abilityControllerId, source, game)) { - if (target.contains(permanent.getId())) { - continue; - } - if (hasObjectTargetNameOrAlias(permanent, targetName)) { - if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) { - if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) { - target.add(permanent.getId(), game); - isAddedSomething = true; - break; - } - } - } else if ((permanent.getName() + '-' + permanent.getExpansionSetCode()).equals(targetName)) { // TODO: remove search by exp code? - if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) { - if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) { - target.add(permanent.getId(), game); - isAddedSomething = true; - break; - } - } - } - } - } - - try { - if (isAddedSomething) { - if (target.isChoiceCompleted(abilityControllerId, source, game)) { - return true; - } - } else { - failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); - } - } finally { - choices.remove(0); - } - return isAddedSomething; - } // choices - } - - if (target instanceof TargetPlayer) { - while (!choices.isEmpty()) { // TODO: remove cycle after main commits - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - return false; // stop dialog - } - String choiceRecord = choices.get(0); - isAddedSomething = false; - for (Player player : game.getPlayers().values()) { - if (player.getName().equals(choiceRecord)) { - if (target.canTarget(abilityControllerId, player.getId(), source, game) && !target.contains(player.getId())) { - target.add(player.getId(), game); - isAddedSomething = true; - } - } - } - - try { - if (isAddedSomething) { - if (target.isChoiceCompleted(abilityControllerId, source, game)) { - return true; - } - } else { - failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); - } - } finally { - choices.remove(0); - } - return isAddedSomething; - } // while choices - } - - // TODO: add same choices fixes for other target types (one choice must uses only one time for one target) - if (target.getOriginalTarget() instanceof TargetCard) { - // one choice per target - // only unique targets - //TargetCard targetFull = ((TargetCard) target); - - for (String choiceRecord : new ArrayList<>(choices)) { // TODO: remove cycle after main commits - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - return false; // stop dialog - } - isAddedSomething = false; - String[] possibleChoices = choiceRecord.split("\\^"); - - for (String possibleChoice : possibleChoices) { - Set possibleCards = target.possibleTargets(abilityControllerId, source, game); - for (UUID targetId : possibleCards) { - MageObject targetObject = game.getCard(targetId); - if (hasObjectTargetNameOrAlias(targetObject, possibleChoice)) { - if (target.canTarget(targetObject.getId(), game) && !target.contains(targetObject.getId())) { - target.add(targetObject.getId(), game); - isAddedSomething = true; - break; - } - } - } - } - try { - if (isAddedSomething) { - if (target.isChoiceCompleted(abilityControllerId, source, game)) { - return true; - } - } else { - failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); - } - } finally { - choices.remove(0); - } - return isAddedSomething; - } // for choices - } - - if (target.getOriginalTarget() instanceof TargetSource) { - TargetSource t = ((TargetSource) target.getOriginalTarget()); - Set possibleTargets = t.possibleTargets(abilityControllerId, source, game); - // TODO: enable choices.get first instead all - for (String choiceRecord : new ArrayList<>(choices)) { // TODO: remove cycle after main commits - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - return false; // stop dialog - } - String[] targetList = choiceRecord.split("\\^"); - isAddedSomething = false; - for (String targetName : targetList) { - for (UUID targetId : possibleTargets) { - MageObject targetObject = game.getObject(targetId); - if (targetObject != null) { - if (hasObjectTargetNameOrAlias(targetObject, targetName)) { - if (t.canTarget(targetObject.getId(), game) && !target.contains(targetObject.getId())) { - target.add(targetObject.getId(), game); - isAddedSomething = true; - break; - } - } - } - } - } - try { - if (isAddedSomething) { - if (target.isChoiceCompleted(abilityControllerId, source, game)) { - return true; - } - } else { - failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); - } - } finally { - choices.remove(choiceRecord); - } - return isAddedSomething; - } // for choices - } - - // TODO: enable fail checks and fix tests - if (!target.getTargetName().equals("starting player")) { - assertWrongChoiceUsage(choices.size() > 0 ? choices.get(0) : "empty list"); - } - } - - this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, source, game, null)); - return computerPlayer.choose(outcome, target, source, game, options); + return makeChoose(outcome, target, source, game, null); } private void checkTargetDefinitionMarksSupport(Target needTarget, String targetDefinition, String canSupportChars) { @@ -2502,7 +2334,7 @@ public class TestPlayer implements Player { // how to fix: change target definition for addTarget in test's code or update choose from targets implementation in TestPlayer if ((foundMulti && !canMulti) || (foundSpecialStart && !canSpecialStart) || (foundSpecialClose && !canSpecialClose) || (foundEquals && !canEquals)) { Assert.fail(this.getName() + " - Targets list was setup by addTarget with " + targets + ", but target definition [" + targetDefinition + "]" - + " is not supported by [" + canSupportChars + "] for target class " + needTarget.getClass().getSimpleName()); + + " is not supported by [" + canSupportChars + "] for target class " + getSimpleClassName(needTarget.getClass())); } } @@ -2514,12 +2346,11 @@ public class TestPlayer implements Player { if (!targets.isEmpty()) { // skip targets - if (targets.get(0).equals(TARGET_SKIP)) { + if (tryToSkipSelection(game, target, targets, TARGET_SKIP)) { Assert.assertTrue("found skip target, but it require more targets, needs " + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); - targets.remove(0); - return false; // false - stop to choose + return target.getSize() > 0 && target.isChosen(game); } Set targetCardZonesChecked = new HashSet<>(); // control miss implementation @@ -2538,7 +2369,7 @@ public class TestPlayer implements Player { && target.canTarget(abilityControllerId, player.getId(), source, game) && !target.contains(player.getId())) { target.addTarget(player.getId(), source, game); - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - player"); return true; } } @@ -2590,7 +2421,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - permanent"); return true; } } @@ -2618,7 +2449,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - hand"); return true; } } @@ -2637,7 +2468,7 @@ public class TestPlayer implements Player { } if (filter == null) { Assert.fail("Unsupported exile target filter in TestPlayer: " - + target.getOriginalTarget().getClass().getCanonicalName()); + + getSimpleClassName(target.getOriginalTarget().getClass())); } for (String targetDefinition : targets.stream().limit(takeMaxTargetsPerChoose).collect(Collectors.toList())) { @@ -2656,7 +2487,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - exile"); return true; } } @@ -2681,7 +2512,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - card in battlefield"); return true; } } @@ -2735,7 +2566,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - graveyard"); return true; } } @@ -2762,7 +2593,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - stack"); return true; } } @@ -2772,13 +2603,13 @@ public class TestPlayer implements Player { if (target.getOriginalTarget() instanceof TargetCardInLibrary || (target.getOriginalTarget() instanceof TargetCard && target.getOriginalTarget().getZone() == Zone.LIBRARY)) { // user don't have access to library, so it must be targeted through list/revealed cards - Assert.fail("Library zone is private, you must target through cards list, e.g. revealed: " + target.getOriginalTarget().getClass().getCanonicalName()); + Assert.fail("Library zone is private, you must target through cards list, e.g. revealed: " + getSimpleClassName(target.getOriginalTarget().getClass())); } // uninplemented TargetCard's zone if (target.getOriginalTarget() instanceof TargetCard && !targetCardZonesChecked.contains(target.getOriginalTarget().getZone())) { Assert.fail("Found unimplemented TargetCard's zone or TargetCard's extented class: " - + target.getOriginalTarget().getClass().getCanonicalName() + + getSimpleClassName(target.getOriginalTarget().getClass()) + ", zone " + target.getOriginalTarget().getZone() + ", from " + (source == null ? "unknown source" : source.getSourceObject(game))); } @@ -2793,14 +2624,14 @@ public class TestPlayer implements Player { if (source != null) { message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used" + "\nCard: " + source.getSourceObject(game) - + "\nAbility: " + source.getClass().getSimpleName() + " (" + source.getRule() + ")" - + "\nTarget: selected " + target.getSize() + ", possible " + possibleTargets.size() + ", " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")" + + "\nAbility: " + getSimpleClassName(source.getClass()) + " (" + source.getRule() + ")" + + "\nTarget: selected " + target.getSize() + ", more possible " + possibleTargets.size() + ", " + getSimpleClassName(target.getClass()) + " (" + target.getMessage(game) + ")" + "\nYou must implement target class support in TestPlayer, \"filter instanceof\", or setup good targets"; } else { message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used" + "\nCard: unknown source" + "\nAbility: unknown source" - + "\nTarget: selected " + target.getSize() + ", possible " + possibleTargets.size() + ", " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")" + + "\nTarget: selected " + target.getSize() + ", more possible " + possibleTargets.size() + ", " + getSimpleClassName(target.getClass()) + " (" + target.getMessage(game) + ")" + "\nYou must implement target class support in TestPlayer, \"filter instanceof\", or setup good targets"; } Assert.fail(message); @@ -2816,13 +2647,13 @@ public class TestPlayer implements Player { assertAliasSupportInTargets(false); if (!targets.isEmpty()) { + // skip targets - if (targets.get(0).equals(TARGET_SKIP)) { + if (tryToSkipSelection(game, target, targets, TARGET_SKIP)) { Assert.assertTrue("found skip target, but it require more targets, needs " + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); - targets.remove(0); - return false; // false - stop to choose + return target.getSize() > 0 && target.isChosen(game); } for (String targetDefinition : targets.stream().limit(takeMaxTargetsPerChoose).collect(Collectors.toList())) { String[] targetList = targetDefinition.split("\\^"); @@ -2839,7 +2670,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target from cards"); return true; } } @@ -2861,7 +2692,7 @@ public class TestPlayer implements Player { for (TriggeredAbility ability : abilities) { if (ability.toString().startsWith(choice)) { - choices.remove(0); + choicesRemoveCurrent(game, "on choose triggers"); // TODO: add short lists? return ability; } } @@ -2890,11 +2721,11 @@ public class TestPlayer implements Player { String choice = choices.get(0); if (choice.equals("No")) { - choices.remove(0); + choicesRemoveCurrent(game, source); return false; } if (choice.equals("Yes")) { - choices.remove(0); + choicesRemoveCurrent(game, source); return true; } @@ -2921,7 +2752,7 @@ public class TestPlayer implements Player { if (choices.get(0).startsWith("X=")) { int xValue = Integer.parseInt(choices.get(0).substring(2)); assertXMinMaxValue(game, source, xValue, min, max); - choices.remove(0); + choicesRemoveCurrent(game, source); return xValue; } } @@ -2960,7 +2791,7 @@ public class TestPlayer implements Player { if (!choices.isEmpty()) { if (choices.get(0).startsWith("X=")) { int xValue = Integer.parseInt(choices.get(0).substring(2)); - choices.remove(0); + choicesRemoveCurrent(game, source); return xValue; } } @@ -2986,17 +2817,17 @@ public class TestPlayer implements Player { // must fill all possible choices or skip it for (int i = 0; i < messages.size(); i++) { if (!choices.isEmpty()) { + // skip + if (tryToSkipSelection(game, null, choices, CHOICE_SKIP)) { + break; + } + // normal choice if (choices.get(0).startsWith("X=")) { answer.set(i, Integer.parseInt(choices.get(0).substring(2))); - choices.remove(0); + choicesRemoveCurrent(game, "on multi amount"); // TODO: add info? continue; } - // skip - if (choices.get(0).equals(CHOICE_SKIP)) { - choices.remove(0); - break; - } } Assert.fail(String.format("Missing choice in multi amount: %s (pos %d - %s)", type.getHeader(), i, messages)); } @@ -3054,7 +2885,7 @@ public class TestPlayer implements Player { @Override public void restore(Player player) { if (!(player instanceof TestPlayer)) { - throw new IllegalArgumentException("Wrong code usage: can't restore from player class " + player.getClass().getName()); + throw new IllegalArgumentException("Wrong code usage: can't restore from player class " + getSimpleClassName(player.getClass())); } // no rollback for test player metadata (modesSet, actions, choices, targets, aliases, etc) @@ -3799,10 +3630,10 @@ public class TestPlayer implements Player { if (!choices.isEmpty()) { String nextResult = choices.get(0); if (nextResult.equals(FLIPCOIN_RESULT_TRUE)) { - choices.remove(0); + choicesRemoveCurrent(game, "on flip coin"); return true; } else if (nextResult.equals(FLIPCOIN_RESULT_FALSE)) { - choices.remove(0); + choicesRemoveCurrent(game, "on flip coin"); return false; } } @@ -3823,7 +3654,7 @@ public class TestPlayer implements Player { if (!choices.isEmpty()) { String nextResult = choices.get(0); if (nextResult.startsWith(DIE_ROLL)) { - choices.remove(0); + choicesRemoveCurrent(game, "on roll die"); return Integer.parseInt(nextResult.substring(DIE_ROLL.length())); } } @@ -3929,13 +3760,13 @@ public class TestPlayer implements Player { } @Override - public PayLifeCostLevel getPayLifeCostLevel() { - return computerPlayer.getPayLifeCostLevel(); + public EnumSet getPayLifeCostRestrictions() { + return computerPlayer.getPayLifeCostRestrictions(); } @Override - public void setPayLifeCostLevel(PayLifeCostLevel payLifeCostLevel) { - computerPlayer.setPayLifeCostLevel(payLifeCostLevel); + public void addPayLifeCostRestriction(PayLifeCostRestriction payLifeCostRestriction) { + computerPlayer.addPayLifeCostRestriction(payLifeCostRestriction); } @Override @@ -3943,16 +3774,6 @@ public class TestPlayer implements Player { return computerPlayer.canPaySacrificeCost(permanent, source, controllerId, game); } - @Override - public FilterPermanent getSacrificeCostFilter() { - return computerPlayer.getSacrificeCostFilter(); - } - - @Override - public void setCanPaySacrificeCostFilter(FilterPermanent permanent) { - computerPlayer.setCanPaySacrificeCostFilter(permanent); - } - @Override public boolean canLoseByZeroOrLessLife() { return computerPlayer.canLoseByZeroOrLessLife(); @@ -4266,62 +4087,143 @@ public class TestPlayer implements Player { return choose(outcome, target, source, game, null); } - private boolean tryToSkipSelection(List selections, String selectionMark) { - if (!selections.isEmpty() && selections.get(0).equals(selectionMark)) { + public void skipAllNextChooseCommands() { + this.isSkipAllNextChooseCommands = true; + } + + public boolean isSkipAllNextChooseCommands() { + return this.isSkipAllNextChooseCommands; + } + + private boolean tryToSkipSelection(Game game, Target target, List selections, String skipCommand) { + if (!selections.isEmpty() && selections.get(0).equals(skipCommand)) { + + // mark target as skip, so choose dialog can be stopped on partly selection + if (target != null) { + if (target.getMaxNumberOfTargets() > target.getMinNumberOfTargets()) { + target.setSkipChoice(true); + } else { + Assert.fail("Wrong skip command found - it can be used with up to targets, but used in " + target); + } + } + selections.remove(0); + DataCollectorServices.getInstance().onTestsChoiceUse(game, this, skipCommand, "by skip command"); + return true; } return false; } - @Override - public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { - assertAliasSupportInChoices(false); - if (!choices.isEmpty()) { + private boolean makeChoose(Outcome outcome, Target target, Ability source, Game game, Cards fromCards) { + assertAliasSupportInChoices(true); + // choose itself for starting player all the time + if (target.getMessage(game).equals("Select a starting player")) { + target.add(this.getId(), game); + return true; + } + + + UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId()); + + PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game); + possibleTargetsSelector.findNewTargets(fromCards); + + // nothing to choose, e.g. no valid targets + if (!possibleTargetsSelector.hasAnyTargets()) { + return false; + } + + // can't choose + if (!possibleTargetsSelector.hasMinNumberOfTargets()) { + return false; + } + + while (!choices.isEmpty()) { // skip choices - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - if (cards.isEmpty()) { - // cancel button forced in GUI on no possible choices - // TODO: need research - return false; - } else { - Assert.assertTrue("found skip choice, but it require more choices, needs " - + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", - target.getTargets().size() >= target.getMinNumberOfTargets()); - return false; // stop dialog - } + if (tryToSkipSelection(game, target, choices, CHOICE_SKIP)) { + Assert.assertTrue("found skip choice, but it require more choices, needs " + + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", + target.getTargets().size() >= target.getMinNumberOfTargets()); + return target.getSize() > 0 && target.isChosen(game); } - for (String choose2 : new ArrayList<>(choices)) { - // TODO: More targetting to fix - String[] targetList = choose2.split("\\^"); - boolean targetFound = false; - for (String targetName : targetList) { - for (Card card : cards.getCards(game)) { - if (target.contains(card.getId())) { - continue; - } - if (hasObjectTargetNameOrAlias(card, targetName)) { - if (target.isNotTarget() || target.canTarget(card.getId(), source, game)) { - target.add(card.getId(), game); - targetFound = true; + String choiceRecord = choices.get(0); + String[] targetList = choiceRecord.split("\\^"); + boolean isAddedSomething = false; + for (String targetName : targetList) { + boolean originOnly = false; + boolean copyOnly = false; + if (targetName.endsWith("]")) { + if (targetName.endsWith("[no copy]")) { + originOnly = true; + targetName = targetName.substring(0, targetName.length() - 9); + } + if (targetName.endsWith("[only copy]")) { + copyOnly = true; + targetName = targetName.substring(0, targetName.length() - 11); + } + } + + for (MageItem item : possibleTargetsSelector.getAny()) { + if (target.contains(item.getId())) { + continue; + } + if (item instanceof MageObject) { + MageObject object = (MageObject) item; + if (hasObjectTargetNameOrAlias(object, targetName)) { + if ((object.isCopy() && !originOnly) || (!object.isCopy() && !copyOnly)) { + target.add(object.getId(), game); + isAddedSomething = true; + break; + } + } else if ((object.getName() + '-' + object.getExpansionSetCode()).equals(targetName)) { // TODO: remove search by exp code? + if ((object.isCopy() && !originOnly) || (!object.isCopy() && !copyOnly)) { + target.add(object.getId(), game); + isAddedSomething = true; break; } } + } else if (item instanceof Player) { + Player player = (Player) item; + if (player.getName().equals(choiceRecord)) { + target.add(player.getId(), game); + isAddedSomething = true; + break; + } + } else { + throw new IllegalArgumentException("Unknown possible target type: " + getSimpleClassName(item.getClass())); } } - if (targetFound) { - choices.remove(choose2); - return true; - } } - assertWrongChoiceUsage(choices.size() > 0 ? choices.get(0) : "empty list"); + try { + if (isAddedSomething) { + if (target.isChoiceCompleted(abilityControllerId, source, game, fromCards)) { + return true; + } + // must refresh possible targets after each "add" cause there are dynamic targets like TargetCardAndOrCard from Sludge Titan + possibleTargetsSelector.findNewTargets(fromCards); + } else { + failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); + } + } finally { + choicesRemoveCurrent(game, source); + } } - this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, source, game, cards)); - return computerPlayer.choose(outcome, cards, target, source, game); + this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, source, game, fromCards)); + if (fromCards != null) { + return computerPlayer.choose(outcome, fromCards, (TargetCard) target, source, game); + } else { + return computerPlayer.choose(outcome, target, source, game); + } + } + + @Override + public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { + return makeChoose(outcome, target, source, game, cards); } @Override @@ -4347,12 +4249,11 @@ public class TestPlayer implements Player { while (!targets.isEmpty()) { // skip targets - if (targets.get(0).equals(TARGET_SKIP)) { + if (tryToSkipSelection(game, target, targets, TARGET_SKIP)) { Assert.assertTrue("found skip target, but it require more targets, needs " + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); - targets.remove(0); - return false; // false - stop to choose + return target.getSize() > 0 && target.isChosen(game); } // only target amount needs @@ -4376,7 +4277,7 @@ public class TestPlayer implements Player { Assert.assertTrue("target amount must be <= remaining = " + target.getAmountRemaining() + " " + targetInfo, targetAmount <= target.getAmountRemaining()); if (target.getAmountRemaining() > 0) { - for (UUID possibleTarget : target.possibleTargets(source.getControllerId(), source, game)) { + for (UUID possibleTarget : target.possibleTargets(abilityControllerId, source, game)) { boolean foundTarget = false; // permanent @@ -4392,10 +4293,10 @@ public class TestPlayer implements Player { } if (foundTarget) { - if (!target.contains(possibleTarget) && target.canTarget(possibleTarget, source, game)) { + if (!target.contains(possibleTarget) && target.canTarget(abilityControllerId, possibleTarget, source, game)) { // can select target.addTarget(possibleTarget, targetAmount, source, game); - targets.remove(0); + targetsRemoveCurrent(null, game, "on choose target amount"); // allow test player to choose as much as possible until skip command if (target.getAmountRemaining() <= 0) { return true; @@ -4437,7 +4338,7 @@ public class TestPlayer implements Player { // simulate cancel on mana payment (e.g. user press on cancel button) if (choices.get(0).equals(MANA_CANCEL)) { - choices.remove(0); + choicesRemoveCurrent(game, "on play mana"); // TODO: add info? return false; } @@ -4487,7 +4388,7 @@ public class TestPlayer implements Player { for (SpecialAction specialAction : specialActions.values()) { if (specialAction.getRule(true).startsWith(choice)) { if (specialAction.canActivate(this.getId(), game).canActivate()) { - choices.remove(0); + choicesRemoveCurrent(game, "on play mana"); // TODO: add info? choiceRemoved = true; if (activateAbility(specialAction, game)) { choiceUsed = true; @@ -4501,7 +4402,7 @@ public class TestPlayer implements Player { if (choiceUsed) { if (!choiceRemoved) { - choices.remove(0); + choicesRemoveCurrent(game, "on play mana"); // TODO: add info? } return true; } else { @@ -4682,7 +4583,7 @@ public class TestPlayer implements Player { String choice = choices.get(0); for (SpellAbility ability : useable.values()) { if (ability.toString().startsWith(choice)) { - choices.remove(0); + choicesRemoveCurrent(game, "on choose ability for cast - " + card.getName()); return ability; } } @@ -4714,7 +4615,7 @@ public class TestPlayer implements Player { if (!choices.isEmpty()) { for (ActivatedAbility ability : useable.values()) { if (ability.toString().startsWith(choices.get(0))) { - choices.remove(0); + choicesRemoveCurrent(game, "on choose land or spell ability - " + card.getName()); return ability; } } @@ -4762,13 +4663,52 @@ public class TestPlayer implements Player { } private void assertWrongChoiceUsage(String choice) { - // TODO: enable fail checks and fix tests, it's a part of setStrictChooseMode's implementation to all tests - //Assert.fail("Wrong choice command: " + choice); - LOGGER.warn("Wrong choice command: " + choice); + // TODO: enable and fix all failed tests + assertWrongChoiceUsage(choice, false); + } + + private void assertWrongChoiceUsage(String choice, boolean isRaiseError) { + if (isRaiseError) { + Assert.fail("Wrong choice command: " + choice); + } else { + LOGGER.warn("Wrong choice command: " + choice); + } } @Override public Player getRealPlayer() { return this.computerPlayer; } + + private void choicesRemoveCurrent(Game game, Ability source) { + choicesRemoveCurrent(game, CardUtil.getSourceLogName(game, "ability: ", source, "", "")); + } + + private void choicesRemoveCurrent(Game game, String reason) { + String realReason = reason.isEmpty() ? "SBA" : reason; + DataCollectorServices.getInstance().onTestsChoiceUse(game, this, choices.get(0), "by setChoice, " + realReason); + choices.remove(0); + } + + // TODO: implement after takeMaxTargetsPerChoose fix/remove (targets must use while logic all the time - same as choices) + // TODO: make same logs for AI choices + private void targetsRemoveCurrent(String targetToRemove, Game game, String reason) { + String realReason = reason.isEmpty() ? "SBA" : reason; + DataCollectorServices.getInstance().onTestsTargetUse(game, this, targets.get(0), "by addTarget, " + realReason); + if (targetToRemove == null) { + targets.remove(0); + } else { + targets.remove(targetToRemove); // TODO: must be targets.remove(0), see todos above + } + } + + private void modesRemoveCurrent(Game game, Mode mode) { + DataCollectorServices.getInstance().onTestsChoiceUse(game, this, modes.get(0), "by setMode, mode: " + mode.getEffects().getText(mode)); + modes.remove(0); + } + + private String getSimpleClassName(Class clazz) { + // anon classes don't have names, so return name instead simple + return clazz.getSimpleName().isEmpty() ? clazz.getName() : clazz.getSimpleName(); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 436daf44d55..be58f93c598 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1706,6 +1706,11 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement private void assertAllCommandsUsed() throws AssertionError { for (Player player : currentGame.getPlayers().values()) { TestPlayer testPlayer = (TestPlayer) player; + + if (testPlayer.isSkipAllNextChooseCommands()) { + Assert.fail(testPlayer.getName() + " used skip next choose commands, but game do not call any choose dialog after it. Skip must be removed after debug."); + } + assertActionsMustBeEmpty(testPlayer); assertChoicesCount(testPlayer, 0); assertTargetsCount(testPlayer, 0); @@ -2008,7 +2013,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param step * @param player * @param cardName - * @param targetName for modes you can add "mode=3" before target name; + * @param targetName for non default mode you can add target by "mode=3target_name" style; * multiple targets can be separated by ^; * no target marks as TestPlayer.NO_TARGET; * warning, do not support cards with target adjusters - use addTarget instead @@ -2315,6 +2320,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * spell mode can be used only once like Demonic Pact, the * value has to be set to the number of the remaining modes * (e.g. if only 2 are left the number need to be 1 or 2). + * If you need to partly select then use TestPlayer.MODE_SKIP */ public void setModeChoice(TestPlayer player, String choice) { player.addModeChoice(choice); @@ -2439,6 +2445,26 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement gameOptions.skipInitShuffling = true; } + /** + * Debug only: skip all choose commands after that command. + *

+ * Alternative to comment/uncomment all test commands: + * - insert skip before first choice command; + * - run test and look at error message about miss choice; + * - make sure test use correct choice; + * - move skip command to next test's choice and repeat; + */ + protected void skipAllNextChooseCommands() { + playerA.skipAllNextChooseCommands(); + playerB.skipAllNextChooseCommands(); + if (playerC != null) { + playerC.skipAllNextChooseCommands(); + } + if (playerD != null) { + playerD.skipAllNextChooseCommands(); + } + } + public void assertDamageReceived(Player player, String cardName, int expected) { Permanent p = getPermanent(cardName, player); if (p != null) { diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index f7b6493ce98..67dba8ac8c8 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -1018,6 +1018,9 @@ public class VerifyCardDataTest { // CHECK: unknown set or wrong name for (ExpansionSet set : sets) { + if ("TLE".equals(set.getCode())) { // temporary + continue; + } if (set.getSetType().equals(SetType.CUSTOM_SET)) { // skip unofficial sets like Star Wars continue; @@ -2078,6 +2081,7 @@ public class VerifyCardDataTest { } } } + private void checkSubtypes(Card card, MtgJsonCard ref) { if (skipListHaveName(SKIP_LIST_SUBTYPE, card.getExpansionSetCode(), card.getName())) { return; diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 50340177526..479fe01ef6a 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1227,7 +1227,7 @@ public abstract class AbilityImpl implements Ability { boolean validTargets = true; for (Target target : mode.getTargets()) { UUID abilityControllerId = target.getAffectedAbilityControllerId(controllerId); - if (!target.canChoose(abilityControllerId, ability, game)) { + if (!target.canChooseOrAlreadyChosen(abilityControllerId, ability, game)) { validTargets = false; break; } diff --git a/Mage/src/main/java/mage/abilities/Mode.java b/Mage/src/main/java/mage/abilities/Mode.java index 0cc7e2b1add..20b9bb1c2e5 100644 --- a/Mage/src/main/java/mage/abilities/Mode.java +++ b/Mage/src/main/java/mage/abilities/Mode.java @@ -130,4 +130,9 @@ public class Mode implements Serializable { public int getPawPrintValue() { return pawPrintValue; } + + @Override + public String toString() { + return String.format("%s", this.getEffects().getText(this)); + } } diff --git a/Mage/src/main/java/mage/abilities/SpecialAction.java b/Mage/src/main/java/mage/abilities/SpecialAction.java index ea17f671cc5..911e074c6e5 100644 --- a/Mage/src/main/java/mage/abilities/SpecialAction.java +++ b/Mage/src/main/java/mage/abilities/SpecialAction.java @@ -53,11 +53,9 @@ public abstract class SpecialAction extends ActivatedAbilityImpl { if (isManaAction()) { // limit play mana abilities by steps int currentStepOrder = 0; - if (!game.getStack().isEmpty()) { - StackObject stackObject = game.getStack().getFirst(); - if (stackObject instanceof Spell) { - currentStepOrder = ((Spell) stackObject).getCurrentActivatingManaAbilitiesStep().getStepOrder(); - } + StackObject stackObject = game.getStack().getFirstOrNull(); + if (stackObject instanceof Spell) { + currentStepOrder = ((Spell) stackObject).getCurrentActivatingManaAbilitiesStep().getStepOrder(); } if (currentStepOrder > manaAbility.useOnActivationManaAbilityStep().getStepOrder()) { return ActivationStatus.getFalse(); diff --git a/Mage/src/main/java/mage/abilities/common/AttachableToRestrictedAbility.java b/Mage/src/main/java/mage/abilities/common/AttachableToRestrictedAbility.java index fc11c56c0e7..1ab07eda011 100644 --- a/Mage/src/main/java/mage/abilities/common/AttachableToRestrictedAbility.java +++ b/Mage/src/main/java/mage/abilities/common/AttachableToRestrictedAbility.java @@ -1,33 +1,43 @@ package mage.abilities.common; -import mage.abilities.Ability; import mage.abilities.effects.common.InfoEffect; +import mage.constants.TargetController; import mage.constants.Zone; +import mage.filter.predicate.Predicate; +import mage.filter.predicate.Predicates; import mage.game.Game; import mage.target.Target; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; /** - * * @author LevelX2 */ public class AttachableToRestrictedAbility extends SimpleStaticAbility { + private final Target attachable; + public AttachableToRestrictedAbility(Target target) { super(Zone.BATTLEFIELD, new InfoEffect("{this} can be attached only to a " + target.getTargetName())); this.attachable = target.copy(); + + // verify check: make sure filter don't have controller predicate cause it used in code without controller info + List list = new ArrayList<>(); + Predicates.collectAllComponents(target.getFilter().getPredicates(), target.getFilter().getExtraPredicates(), list); + if (list.stream().anyMatch(TargetController.ControllerPredicate.class::isInstance)) { + throw new IllegalArgumentException("Wrong code usage: attachable restriction filter must not contain controller predicate"); + } } private AttachableToRestrictedAbility(AttachableToRestrictedAbility ability) { super(ability); - this.attachable = ability.attachable; // Since we never modify the target, we don't need to re-copy it + this.attachable = ability.attachable.copy(); } - public boolean canEquip(UUID toEquip, Ability source, Game game) { - if (source == null) { - return attachable.canTarget(toEquip, game); - } else return attachable.canTarget(toEquip, source, game); + public boolean canEquip(UUID toEquip, Game game) { + return attachable.canTarget(toEquip, null, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/common/CantPayLifeOrSacrificeAbility.java b/Mage/src/main/java/mage/abilities/common/CantPayLifeOrSacrificeAbility.java new file mode 100644 index 00000000000..23e4db0cb93 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/CantPayLifeOrSacrificeAbility.java @@ -0,0 +1,151 @@ +package mage.abilities.common; + +import mage.abilities.Ability; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; + +import java.util.Optional; +import java.util.UUID; + +/** + * Effect used to prevent paying life and, optionally, sacrificing permanents as a cost for activated abilities and casting spells. + * @author Jmlundeen + */ +public class CantPayLifeOrSacrificeAbility extends SimpleStaticAbility { + + private final String rule; + + public CantPayLifeOrSacrificeAbility(FilterPermanent sacrificeFilter) { + this(false, sacrificeFilter); + } + + /** + * @param onlyNonManaAbilities boolean to set if the restriction should only apply to non-mana abilities + * @param sacrificeFilter filter for types of permanents that cannot be sacrificed, can be null if sacrifice not needed. + * e.g. Karn's Sylex + */ + public CantPayLifeOrSacrificeAbility(boolean onlyNonManaAbilities, FilterPermanent sacrificeFilter) { + super(new CantPayLifeEffect(onlyNonManaAbilities)); + if (sacrificeFilter != null) { + addEffect(new CantSacrificeEffect(onlyNonManaAbilities, sacrificeFilter)); + } + this.rule = makeRule(onlyNonManaAbilities, sacrificeFilter); + } + + private CantPayLifeOrSacrificeAbility(CantPayLifeOrSacrificeAbility effect) { + super(effect); + this.rule = effect.rule; + } + + public CantPayLifeOrSacrificeAbility copy() { + return new CantPayLifeOrSacrificeAbility(this); + } + + String makeRule(boolean nonManaAbilities, FilterPermanent sacrificeFilter) { + StringBuilder sb = new StringBuilder("Players can't pay life"); + if (sacrificeFilter != null) { + sb.append(" or sacrifice ").append(sacrificeFilter.getMessage()); + } + sb.append(" to cast spells or activate abilities"); + if (nonManaAbilities) { + sb.append(" that aren't mana abilities"); + } + sb.append("."); + return sb.toString(); + } + + @Override + public String getRule() { + return rule; + } +} + +class CantPayLifeEffect extends ContinuousEffectImpl { + + private final boolean onlyNonManaAbilities; + + /** + * @param onlyNonManaAbilities boolean to set if the restriction should only apply to non-mana abilities + */ + CantPayLifeEffect(boolean onlyNonManaAbilities) { + super(Duration.WhileOnBattlefield, Layer.PlayerEffects, SubLayer.NA, Outcome.Detriment); + this.onlyNonManaAbilities = onlyNonManaAbilities; + } + + private CantPayLifeEffect(CantPayLifeEffect effect) { + super(effect); + this.onlyNonManaAbilities = effect.onlyNonManaAbilities; + } + + public CantPayLifeEffect copy() { + return new CantPayLifeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { + Player player = game.getPlayer(playerId); + if (player == null) { + return false; + } + player.addPayLifeCostRestriction(Player.PayLifeCostRestriction.CAST_SPELLS); + if (this.onlyNonManaAbilities) { + player.addPayLifeCostRestriction(Player.PayLifeCostRestriction.ACTIVATE_NON_MANA_ABILITIES); + } else { + player.addPayLifeCostRestriction(Player.PayLifeCostRestriction.ACTIVATE_MANA_ABILITIES); + player.addPayLifeCostRestriction(Player.PayLifeCostRestriction.ACTIVATE_NON_MANA_ABILITIES); + } + } + return true; + } +} + +class CantSacrificeEffect extends ContinuousRuleModifyingEffectImpl { + + private final FilterPermanent sacrificeFilter; + private final boolean onlyNonManaAbilities; + + CantSacrificeEffect(boolean onlyNonManaAbilities, FilterPermanent sacrificeFilter) { + super(Duration.WhileOnBattlefield, Outcome.Detriment); + this.sacrificeFilter = sacrificeFilter; + this.onlyNonManaAbilities = onlyNonManaAbilities; + } + + private CantSacrificeEffect(CantSacrificeEffect effect) { + super(effect); + this.sacrificeFilter = effect.sacrificeFilter.copy(); + this.onlyNonManaAbilities = effect.onlyNonManaAbilities; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.PAY_SACRIFICE_COST; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Permanent permanent = game.getPermanent(event.getTargetId()); + Optional abilityOptional = game.getAbility(UUID.fromString(event.getData()), event.getSourceId()); + if (permanent == null || !abilityOptional.isPresent()) { + return false; + } + Ability abilityWithCost = abilityOptional.get(); + boolean isActivatedAbility = (onlyNonManaAbilities && abilityWithCost.isManaActivatedAbility()) || + (!onlyNonManaAbilities && abilityWithCost.isActivatedAbility()); + if (!isActivatedAbility && abilityWithCost.getAbilityType() != AbilityType.SPELL) { + return false; + } + return this.sacrificeFilter.match(permanent, event.getPlayerId(), source, game); + } + + @Override + public CantSacrificeEffect copy() { + return new CantSacrificeEffect(this); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/CycleAllTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/CycleAllTriggeredAbility.java index 7621d355e6d..51762197c1b 100644 --- a/Mage/src/main/java/mage/abilities/common/CycleAllTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/CycleAllTriggeredAbility.java @@ -31,10 +31,7 @@ public class CycleAllTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - if (game.getState().getStack().isEmpty()) { - return false; - } - StackObject item = game.getState().getStack().getFirst(); + StackObject item = game.getState().getStack().getFirstOrNull(); return item instanceof StackAbility && item.getStackAbility() instanceof CyclingAbility; } diff --git a/Mage/src/main/java/mage/abilities/common/CycleControllerTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/CycleControllerTriggeredAbility.java index ba2fe63e01e..3154476911a 100644 --- a/Mage/src/main/java/mage/abilities/common/CycleControllerTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/CycleControllerTriggeredAbility.java @@ -42,12 +42,11 @@ public class CycleControllerTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - if (game.getState().getStack().isEmpty() - || !event.getPlayerId().equals(this.getControllerId()) + if (!event.getPlayerId().equals(this.getControllerId()) || (event.getSourceId().equals(this.getSourceId()) && excludeSource)) { return false; } - StackObject item = game.getState().getStack().getFirst(); + StackObject item = game.getState().getStack().getFirstOrNull(); return item instanceof StackAbility && item.getStackAbility() instanceof CyclingAbility; } diff --git a/Mage/src/main/java/mage/abilities/common/DrawNthCardTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DrawNthCardTriggeredAbility.java index 35acc7a0165..5f9d03e771a 100644 --- a/Mage/src/main/java/mage/abilities/common/DrawNthCardTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DrawNthCardTriggeredAbility.java @@ -6,12 +6,14 @@ import mage.abilities.effects.Effect; import mage.abilities.hint.Hint; import mage.abilities.hint.ValueHint; import mage.constants.TargetController; +import mage.constants.WatcherScope; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; -import mage.players.Player; import mage.util.CardUtil; -import mage.watchers.common.CardsDrawnThisTurnWatcher; +import mage.watchers.Watcher; + +import java.util.*; /** * @author TheElk801 @@ -48,6 +50,7 @@ public class DrawNthCardTriggeredAbility extends TriggeredAbilityImpl { this.addHint(hint); } setTriggerPhrase(generateTriggerPhrase()); + this.addWatcher(new DrawNthCardWatcher()); } protected DrawNthCardTriggeredAbility(final DrawNthCardTriggeredAbility ability) { @@ -75,8 +78,7 @@ public class DrawNthCardTriggeredAbility extends TriggeredAbilityImpl { } break; case OPPONENT: - Player controller = game.getPlayer(controllerId); - if (controller == null || !controller.hasOpponent(event.getPlayerId(), game)) { + if (!game.getOpponents(getControllerId()).contains(event.getPlayerId())) { return false; } break; @@ -86,8 +88,7 @@ public class DrawNthCardTriggeredAbility extends TriggeredAbilityImpl { default: throw new IllegalArgumentException("TargetController " + targetController + " not supported"); } - CardsDrawnThisTurnWatcher watcher = game.getState().getWatcher(CardsDrawnThisTurnWatcher.class); - return watcher != null && watcher.getCardsDrawnThisTurn(event.getPlayerId()) == cardNumber; + return DrawNthCardWatcher.checkEvent(event.getPlayerId(), event.getId(), game) + 1 == cardNumber; } public String generateTriggerPhrase() { @@ -110,3 +111,36 @@ public class DrawNthCardTriggeredAbility extends TriggeredAbilityImpl { return new DrawNthCardTriggeredAbility(this); } } + +class DrawNthCardWatcher extends Watcher { + + private final Map> playerDrawEventMap = new HashMap<>(); + + DrawNthCardWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.DREW_CARD) { + playerDrawEventMap + .computeIfAbsent(event.getPlayerId(), x -> new ArrayList<>()) + .add(event.getId()); + } + } + + @Override + public void reset() { + super.reset(); + playerDrawEventMap.clear(); + } + + static int checkEvent(UUID playerId, UUID eventId, Game game) { + return game + .getState() + .getWatcher(DrawNthCardWatcher.class) + .playerDrawEventMap + .getOrDefault(playerId, Collections.emptyList()) + .indexOf(eventId); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/PutCounterOnCreatureTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/PutCounterOnCreatureTriggeredAbility.java deleted file mode 100644 index 6d953099ba8..00000000000 --- a/Mage/src/main/java/mage/abilities/common/PutCounterOnCreatureTriggeredAbility.java +++ /dev/null @@ -1,106 +0,0 @@ -package mage.abilities.common; - -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.Effect; -import mage.constants.Zone; -import mage.counters.Counter; -import mage.filter.FilterPermanent; -import mage.filter.common.FilterCreaturePermanent; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.target.targetpointer.FixedTarget; - -/** - * "Whenever you put one or more counters on a creature " triggered ability - * - * @author PurpleCrowbar - */ -public class PutCounterOnCreatureTriggeredAbility extends TriggeredAbilityImpl { - - private final Counter counterType; // when null, any counter type is accepted - private final FilterPermanent filter; - private final boolean setTargetPointer; - - public PutCounterOnCreatureTriggeredAbility(Effect effect) { - this(effect, (Counter) null); - } - - public PutCounterOnCreatureTriggeredAbility(Effect effect, Counter counter) { - this(effect, counter, new FilterCreaturePermanent()); - } - - public PutCounterOnCreatureTriggeredAbility(Effect effect, FilterPermanent filter) { - this(effect, null, filter); - } - - public PutCounterOnCreatureTriggeredAbility(Effect effect, Counter counter, FilterPermanent filter) { - this(effect, counter, filter, false); - } - - public PutCounterOnCreatureTriggeredAbility(Effect effect, Counter counter, FilterPermanent filter, boolean setTargetPointer) { - this(effect, counter, filter, setTargetPointer, false); - } - - - public PutCounterOnCreatureTriggeredAbility(Effect effect, Counter counter, FilterPermanent filter, boolean setTargetPointer, boolean optional) { - super(Zone.BATTLEFIELD, effect, optional); - this.counterType = counter; - this.filter = filter; - this.setTargetPointer = setTargetPointer; - - setFilterMessage(); - } - - protected PutCounterOnCreatureTriggeredAbility(final PutCounterOnCreatureTriggeredAbility ability) { - super(ability); - this.counterType = ability.counterType; - this.filter = ability.filter; - this.setTargetPointer = ability.setTargetPointer; - } - - @Override - public PutCounterOnCreatureTriggeredAbility copy() { - return new PutCounterOnCreatureTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.COUNTERS_ADDED; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (!isControlledBy(event.getPlayerId())) { - return false; - } - Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); - if (permanent == null) { - permanent = game.getPermanentEntering(event.getTargetId()); - } - if (permanent == null || !filter.match(permanent, controllerId, this, game)) { - return false; - } - if (counterType != null && !event.getData().equals(counterType.getName())) { - return false; - } - if (setTargetPointer) { - getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game)); - } - getEffects().setValue("countersAdded", event.getAmount()); - return true; - } - - private void setFilterMessage() { - String filterMessage = filter.getMessage(); - if (!filterMessage.startsWith("another")) { - filterMessage = "a " + filterMessage; - } - - if (this.counterType == null) { - setTriggerPhrase("Whenever you put one or more counters on " + filterMessage + ", "); - } else { - setTriggerPhrase("Whenever you put one or more " + this.counterType.getName() + " counters on " + filterMessage + ", "); - } - } -} diff --git a/Mage/src/main/java/mage/abilities/common/PutCounterOnPermanentTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/PutCounterOnPermanentTriggeredAbility.java new file mode 100644 index 00000000000..c0b32c1a0f9 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/PutCounterOnPermanentTriggeredAbility.java @@ -0,0 +1,79 @@ +package mage.abilities.common; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +/** + * "Whenever you put one or more [] counters on a []" triggered ability + * + * @author PurpleCrowbar + */ +public class PutCounterOnPermanentTriggeredAbility extends TriggeredAbilityImpl { + + private final CounterType counterType; // when null, any counter type is accepted + private final FilterPermanent filter; + private final boolean setTargetPointer; + + public PutCounterOnPermanentTriggeredAbility(Effect effect, CounterType counterType, FilterPermanent filter) { + this(effect, counterType, filter, false, false); + } + + public PutCounterOnPermanentTriggeredAbility(Effect effect, CounterType counterType, FilterPermanent filter, + boolean setTargetPointer, boolean optional) { + super(Zone.BATTLEFIELD, effect, optional); + this.counterType = counterType; + this.filter = filter; + this.setTargetPointer = setTargetPointer; + setTriggerPhrase("Whenever you put one or more " + + (this.counterType == null ? "" : this.counterType.getName() + " ") + + "counters on " + CardUtil.addArticle(filter.getMessage()) + ", "); + } + + protected PutCounterOnPermanentTriggeredAbility(final PutCounterOnPermanentTriggeredAbility ability) { + super(ability); + this.counterType = ability.counterType; + this.filter = ability.filter; + this.setTargetPointer = ability.setTargetPointer; + } + + @Override + public PutCounterOnPermanentTriggeredAbility copy() { + return new PutCounterOnPermanentTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.COUNTERS_ADDED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!isControlledBy(event.getPlayerId())) { + return false; + } + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); + if (permanent == null) { + permanent = game.getPermanentEntering(event.getTargetId()); + } + if (permanent == null || !filter.match(permanent, controllerId, this, game)) { + return false; + } + if (counterType != null && !event.getData().equals(counterType.getName())) { + return false; + } + if (setTargetPointer) { + getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game)); + } + getEffects().setValue("countersAdded", event.getAmount()); + return true; + } + +} diff --git a/Mage/src/main/java/mage/abilities/costs/Cost.java b/Mage/src/main/java/mage/abilities/costs/Cost.java index 9aecb8937f3..8a70a010b2c 100644 --- a/Mage/src/main/java/mage/abilities/costs/Cost.java +++ b/Mage/src/main/java/mage/abilities/costs/Cost.java @@ -19,9 +19,20 @@ public interface Cost extends Serializable, Copyable { /** * Check is it possible to pay * For mana it checks only single color and amount available, not total mana cost + *

+ * Warning, if you want to use canChoose, then don't forget about already selected targets (important for AI sims). */ boolean canPay(Ability ability, Ability source, UUID controllerId, Game game); + /** + * Simple canPay logic implementation with targets - cost has possible targets or already selected it, e.g. by AI sims + *

+ * Do not override + */ + default boolean canChooseOrAlreadyChosen(Ability ability, Ability source, UUID controllerId, Game game) { + return this.getTargets().stream().allMatch(target -> target.isChosen(game) || target.canChoose(controllerId, source, game)); + } + boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana); boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay); diff --git a/Mage/src/main/java/mage/abilities/costs/common/DiscardTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/DiscardTargetCost.java index 83b12475790..1316d2daf35 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/DiscardTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/DiscardTargetCost.java @@ -70,7 +70,7 @@ public class DiscardTargetCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/costs/common/ExileFromGraveCost.java b/Mage/src/main/java/mage/abilities/costs/common/ExileFromGraveCost.java index 552ab89af2d..6565f9e164d 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/ExileFromGraveCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/ExileFromGraveCost.java @@ -120,7 +120,7 @@ public class ExileFromGraveCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/costs/common/ExileFromHandCost.java b/Mage/src/main/java/mage/abilities/costs/common/ExileFromHandCost.java index 9a9aee8454e..4b1466742d2 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/ExileFromHandCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/ExileFromHandCost.java @@ -82,7 +82,7 @@ public class ExileFromHandCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/costs/common/ExileTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/ExileTargetCost.java index c71f98d225b..07cf32a50c9 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/ExileTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/ExileTargetCost.java @@ -68,7 +68,7 @@ public class ExileTargetCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/costs/common/PutCardFromHandOnTopOfLibraryCost.java b/Mage/src/main/java/mage/abilities/costs/common/PutCardFromHandOnTopOfLibraryCost.java index 565bb66c1b0..890ece07a43 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PutCardFromHandOnTopOfLibraryCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PutCardFromHandOnTopOfLibraryCost.java @@ -32,8 +32,7 @@ public class PutCardFromHandOnTopOfLibraryCost extends CostImpl { TargetCardInHand targetCardInHand = new TargetCardInHand(); targetCardInHand.setRequired(false); Card card; - if (targetCardInHand.canChoose(controllerId, source, game) - && controller.choose(Outcome.PreventDamage, targetCardInHand, source, game)) { + if (controller.choose(Outcome.PreventDamage, targetCardInHand, source, game)) { card = game.getCard(targetCardInHand.getFirstTarget()); paid = card != null && controller.moveCardToLibraryWithInfo(card, source, game, Zone.HAND, true, true); } diff --git a/Mage/src/main/java/mage/abilities/costs/common/PutCountersTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/PutCountersTargetCost.java index 1baac83c362..cb3cbdb36be 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PutCountersTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PutCountersTargetCost.java @@ -58,6 +58,6 @@ public class PutCountersTargetCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } } diff --git a/Mage/src/main/java/mage/abilities/costs/common/RemoveCounterCost.java b/Mage/src/main/java/mage/abilities/costs/common/RemoveCounterCost.java index 03c1607a1fb..6264bd10aac 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/RemoveCounterCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/RemoveCounterCost.java @@ -159,7 +159,7 @@ public class RemoveCounterCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return target.canChoose(controllerId, source, game); + return target.canChooseOrAlreadyChosen(controllerId, source, game); } private String setText() { diff --git a/Mage/src/main/java/mage/abilities/costs/common/ReturnToHandChosenControlledPermanentCost.java b/Mage/src/main/java/mage/abilities/costs/common/ReturnToHandChosenControlledPermanentCost.java index 640f9c7b0d3..48489cdbc0c 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/ReturnToHandChosenControlledPermanentCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/ReturnToHandChosenControlledPermanentCost.java @@ -71,7 +71,7 @@ public class ReturnToHandChosenControlledPermanentCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/costs/common/ReturnToHandFromGraveyardCost.java b/Mage/src/main/java/mage/abilities/costs/common/ReturnToHandFromGraveyardCost.java index 3efc8aec28f..bc0df6c3a52 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/ReturnToHandFromGraveyardCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/ReturnToHandFromGraveyardCost.java @@ -52,7 +52,7 @@ public class ReturnToHandFromGraveyardCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/costs/common/RevealTargetFromHandCost.java b/Mage/src/main/java/mage/abilities/costs/common/RevealTargetFromHandCost.java index 6ce286b3e5e..bb1dcffa446 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/RevealTargetFromHandCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/RevealTargetFromHandCost.java @@ -89,7 +89,7 @@ public class RevealTargetFromHandCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return allowNoReveal || this.getTargets().canChoose(controllerId, source, game); + return allowNoReveal || canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/costs/common/TapTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/TapTargetCost.java index ea468535e43..761f22b5dc2 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/TapTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/TapTargetCost.java @@ -69,7 +69,7 @@ public class TapTargetCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return target.canChoose(controllerId, source, game); + return target.canChooseOrAlreadyChosen(controllerId, source, game); } public TargetControlledPermanent getTarget() { diff --git a/Mage/src/main/java/mage/abilities/costs/common/UntapTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/UntapTargetCost.java index 8e0506c2f5c..2ba5fa58762 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/UntapTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/UntapTargetCost.java @@ -63,7 +63,7 @@ public class UntapTargetCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return target.canChoose(controllerId, source, game); + return target.canChooseOrAlreadyChosen(controllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ConvokedSourceCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ConvokedSourceCount.java index 4af0a7d1837..bb091c2cf19 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ConvokedSourceCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ConvokedSourceCount.java @@ -20,7 +20,7 @@ public enum ConvokedSourceCount implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - return CardUtil.getSourceCostsTag(game, sourceAbility, ConvokeAbility.convokingCreaturesKey, new HashSet<>(0)).size(); + return CardUtil.getSourceCostsTag(game, sourceAbility, ConvokeAbility.convokingCreaturesKey, new HashSet<>()).size(); } @Override diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/SunburstCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/SunburstCount.java index db83fe6837e..0303cfe132b 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/SunburstCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/SunburstCount.java @@ -18,25 +18,24 @@ public enum SunburstCount implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { int count = 0; - if (!game.getStack().isEmpty()) { - StackObject spell = game.getStack().getFirst(); - if (spell instanceof Spell && ((Spell) spell).getSourceId().equals(sourceAbility.getSourceId())) { - Mana mana = ((Spell) spell).getSpellAbility().getManaCostsToPay().getUsedManaToPay(); - if (mana.getBlack() > 0) { - count++; - } - if (mana.getBlue() > 0) { - count++; - } - if (mana.getGreen() > 0) { - count++; - } - if (mana.getRed() > 0) { - count++; - } - if (mana.getWhite() > 0) { - count++; - } + + StackObject spell = game.getStack().getFirstOrNull(); + if (spell instanceof Spell && spell.getSourceId().equals(sourceAbility.getSourceId())) { + Mana mana = ((Spell) spell).getSpellAbility().getManaCostsToPay().getUsedManaToPay(); + if (mana.getBlack() > 0) { + count++; + } + if (mana.getBlue() > 0) { + count++; + } + if (mana.getGreen() > 0) { + count++; + } + if (mana.getRed() > 0) { + count++; + } + if (mana.getWhite() > 0) { + count++; } } return count; diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java index 7db5097dcc8..0f9f25acc3a 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java @@ -513,13 +513,10 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu // If the top card of your library changes while you’re casting a spell, playing a land, or activating an ability, // you can’t look at the new top card until you finish doing so. This means that if you cast the top card of // your library, you can’t look at the next one until you’re done paying for that spell. (2019-05-03) - if (!game.getStack().isEmpty()) { - StackObject stackObject = game.getStack().getFirst(); - return !(stackObject instanceof Spell) - || !Zone.LIBRARY.equals(((Spell) stackObject).getFromZone()) - || stackObject.getStackAbility().getManaCostsToPay().isPaid(); // mana payment finished - } - return true; + StackObject stackObject = game.getStack().getFirstOrNull(); + return !(stackObject instanceof Spell) + || !Zone.LIBRARY.equals(((Spell) stackObject).getFromZone()) + || stackObject.getStackAbility().getManaCostsToPay().isPaid(); // mana payment finished } @Override diff --git a/Mage/src/main/java/mage/abilities/effects/common/SacrificeOpponentsUnlessPayEffect.java b/Mage/src/main/java/mage/abilities/effects/common/SacrificeOpponentsUnlessPayEffect.java index 63bca73a3b5..5eecee67a66 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/SacrificeOpponentsUnlessPayEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/SacrificeOpponentsUnlessPayEffect.java @@ -74,8 +74,7 @@ public class SacrificeOpponentsUnlessPayEffect extends OneShotEffect { if (game.getBattlefield().count(TargetSacrifice.makeFilter(filter), player.getId(), source, game) > 0) { TargetSacrifice target = new TargetSacrifice(1, filter); - if (target.canChoose(player.getId(), source, game)) { - player.choose(Outcome.Sacrifice, target, source, game); + if (player.choose(Outcome.Sacrifice, target, source, game)) { permsToSacrifice.addAll(target.getTargets()); } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java b/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java index 7cacad39c33..a6a958121c2 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java @@ -7,6 +7,7 @@ import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.OneShotEffect; import mage.cards.CardsImpl; import mage.constants.Outcome; +import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.StaticFilters; import mage.game.Game; @@ -60,7 +61,7 @@ public class LookTargetHandChooseDiscardEffect extends OneShotEffect { } return true; } - TargetCard target = new TargetCardInHand(upTo ? 0 : num, num, filter); + TargetCard target = new TargetCard(upTo ? 0 : num, num, Zone.HAND, filter); if (controller.choose(Outcome.Discard, player.getHand(), target, source, game)) { // TODO: must fizzle discard effect on not full choice // - tests: affected (allow to choose and discard 1 instead 2) diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/AirbendTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/AirbendTargetEffect.java new file mode 100644 index 00000000000..b98f6b35221 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/keyword/AirbendTargetEffect.java @@ -0,0 +1,122 @@ +package mage.abilities.effects.keyword; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.costs.Cost; +import mage.abilities.costs.Costs; +import mage.abilities.costs.CostsImpl; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.AsThoughEffectType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; + +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author TheElk801 + */ +public class AirbendTargetEffect extends OneShotEffect { + + public AirbendTargetEffect() { + super(Outcome.Benefit); + } + + private AirbendTargetEffect(final AirbendTargetEffect effect) { + super(effect); + } + + @Override + public AirbendTargetEffect copy() { + return new AirbendTargetEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Set permanents = this + .getTargetPointer() + .getTargets(game, source) + .stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (player == null || permanents.isEmpty()) { + return false; + } + player.moveCards(permanents, Zone.EXILED, source, game); + Cards cards = new CardsImpl(permanents); + cards.retainZone(Zone.EXILED, game); + for (Card card : cards.getCards(game)) { + game.addEffect(new AirbendingCastEffect(card, game), source); + } + game.fireEvent(GameEvent.getEvent( + GameEvent.EventType.AIRBENDED, source.getSourceId(), + source, source.getControllerId() + )); + return true; + } + + @Override + public String getText(Mode mode) { + if (staticText != null && !staticText.isEmpty()) { + return staticText; + } + return "airbend " + getTargetPointer().describeTargets(mode.getTargets(), ""); + } +} + +class AirbendingCastEffect extends AsThoughEffectImpl { + + AirbendingCastEffect(Card card, Game game) { + super(AsThoughEffectType.CAST_FROM_NOT_OWN_HAND_ZONE, Duration.Custom, Outcome.AIDontUseIt); + this.setTargetPointer(new FixedTarget(card, game)); + } + + private AirbendingCastEffect(final AirbendingCastEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public AirbendingCastEffect copy() { + return new AirbendingCastEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + Card card = game.getCard(getTargetPointer().getFirst(game, source)); + if (card == null) { + discard(); + return false; + } + if (!card.getId().equals(objectId) || !card.isOwnedBy(affectedControllerId)) { + return false; + } + Player player = game.getPlayer(affectedControllerId); + if (player == null) { + return false; + } + Costs newCosts = new CostsImpl<>(); + newCosts.addAll(card.getSpellAbility().getCosts()); + player.setCastSourceIdWithAlternateMana(card.getId(), new ManaCostsImpl<>("{2}"), newCosts); + return true; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/EarthbendTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/EarthbendTargetEffect.java new file mode 100644 index 00000000000..7ec45c93c3a --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/keyword/EarthbendTargetEffect.java @@ -0,0 +1,112 @@ +package mage.abilities.effects.keyword; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.Mode; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ReturnToBattlefieldUnderOwnerControlTargetEffect; +import mage.abilities.effects.common.continuous.BecomesCreatureTargetEffect; +import mage.abilities.keyword.HasteAbility; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.custom.CreatureToken; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +/** + * @author TheElk801 + */ +public class EarthbendTargetEffect extends OneShotEffect { + + private final int amount; + + public EarthbendTargetEffect(int amount) { + super(Outcome.Benefit); + this.amount = amount; + } + + private EarthbendTargetEffect(final EarthbendTargetEffect effect) { + super(effect); + this.amount = effect.amount; + } + + @Override + public EarthbendTargetEffect copy() { + return new EarthbendTargetEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + game.addEffect(new BecomesCreatureTargetEffect( + new CreatureToken(0, 0) + .withAbility(HasteAbility.getInstance()), + false, true, Duration.Custom + ), source); + game.addDelayedTriggeredAbility(new EarthbendingDelayedTriggeredAbility(permanent, game), source); + game.fireEvent(GameEvent.getEvent( + GameEvent.EventType.EARTHBENDED, permanent.getId(), + source, source.getControllerId(), amount + )); + return true; + } + + @Override + public String getText(Mode mode) { + if (staticText != null && !staticText.isEmpty()) { + return staticText; + } + return "earthbend " + amount + ". (Target land you control becomes a 0/0 creature " + + "with haste that's still a land. Put " + CardUtil.numberToText(amount, "a") + + " +1/+1 counter" + (amount > 1 ? "s" : "") + " on it. " + + "When it dies or is exiled, return it to the battlefield tapped.)"; + } +} + +class EarthbendingDelayedTriggeredAbility extends DelayedTriggeredAbility { + + private final MageObjectReference mor; + + EarthbendingDelayedTriggeredAbility(Permanent permanent, Game game) { + super(new ReturnToBattlefieldUnderOwnerControlTargetEffect(true, false), Duration.Custom, true, false); + this.mor = new MageObjectReference(permanent, game, 1); + this.getAllEffects().setTargetPointer(new FixedTarget(this.mor)); + } + + private EarthbendingDelayedTriggeredAbility(final EarthbendingDelayedTriggeredAbility ability) { + super(ability); + this.mor = ability.mor; + } + + @Override + public EarthbendingDelayedTriggeredAbility copy() { + return new EarthbendingDelayedTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + return zEvent.getFromZone() == Zone.BATTLEFIELD + && (zEvent.getToZone() == Zone.GRAVEYARD || zEvent.getToZone() == Zone.EXILED) + && mor.refersTo(zEvent.getTarget(), game, -1); + } + + @Override + public String getRule() { + return "When it dies or is exiled, return it to the battlefield tapped."; + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java b/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java index d6baf07740f..5ef72d017fc 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java @@ -160,7 +160,7 @@ class ChampionExileCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java b/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java index 377f7743323..3ba9690fb4d 100644 --- a/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/CraftAbility.java @@ -115,7 +115,7 @@ class CraftCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return target.canChoose(controllerId, source, game); + return target.canChooseOrAlreadyChosen(controllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/keyword/FirebendingAbility.java b/Mage/src/main/java/mage/abilities/keyword/FirebendingAbility.java new file mode 100644 index 00000000000..6ac9f6c8b24 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/FirebendingAbility.java @@ -0,0 +1,91 @@ +package mage.abilities.keyword; + +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.OneShotEffect; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; + +import java.util.Collections; + +/** + * @author TheElk801 + */ +public class FirebendingAbility extends AttacksTriggeredAbility { + + private final DynamicValue amount; + + public FirebendingAbility(int amount) { + this(StaticValue.get(amount)); + } + + public FirebendingAbility(DynamicValue amount) { + super(new FirebendingAbilityEffect(amount)); + this.amount = amount; + } + + private FirebendingAbility(final FirebendingAbility ability) { + super(ability); + this.amount = ability.amount; + } + + @Override + public FirebendingAbility copy() { + return new FirebendingAbility(this); + } + + @Override + public String getRule() { + if (amount instanceof StaticValue) { + return "firebending " + amount + + " (Whenever this creature attacks, add " + + String.join("", Collections.nCopies(((StaticValue) amount).getValue(), "{R}")) + + ". This mana lasts until end of combat.)"; + } + return "firebending X, where X is " + amount.getMessage() + + ". (Whenever this creature attacks, add X {R}. This mana lasts until end of combat.)"; + } +} + +class FirebendingAbilityEffect extends OneShotEffect { + + private final DynamicValue amount; + + FirebendingAbilityEffect(DynamicValue amount) { + super(Outcome.Benefit); + this.amount = amount; + } + + private FirebendingAbilityEffect(final FirebendingAbilityEffect effect) { + super(effect); + this.amount = effect.amount; + } + + @Override + public FirebendingAbilityEffect copy() { + return new FirebendingAbilityEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + int amount = Math.max(this.amount.calculate(game, source, this), 0); + if (amount > 0) { + player.getManaPool().addMana(Mana.RedMana(amount), game, source, Duration.EndOfCombat); + } + game.fireEvent(GameEvent.getEvent( + GameEvent.EventType.FIREBENDED, source.getSourceId(), + source, source.getControllerId(), amount + )); + return true; + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/NinjutsuAbility.java b/Mage/src/main/java/mage/abilities/keyword/NinjutsuAbility.java index 32422330d34..7410e49ad31 100644 --- a/Mage/src/main/java/mage/abilities/keyword/NinjutsuAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/NinjutsuAbility.java @@ -171,7 +171,7 @@ class ReturnAttackerToHandTargetCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return this.getTargets().canChoose(controllerId, source, game); + return canChooseOrAlreadyChosen(ability, source, controllerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/keyword/WaterbendAbility.java b/Mage/src/main/java/mage/abilities/keyword/WaterbendAbility.java new file mode 100644 index 00000000000..f955cbcda41 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/WaterbendAbility.java @@ -0,0 +1,37 @@ +package mage.abilities.keyword; + +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.Cost; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.util.CardUtil; + +/** + * TODO: Implement properly + * + * @author TheElk801 + */ +public class WaterbendAbility extends SimpleActivatedAbility { + + public WaterbendAbility(Effect effect, Cost cost) { + this(Zone.BATTLEFIELD, effect, cost); + } + + public WaterbendAbility(Zone zone, Effect effect, Cost cost) { + super(zone, effect, cost); + } + + private WaterbendAbility(final WaterbendAbility ability) { + super(ability); + } + + @Override + public WaterbendAbility copy() { + return new WaterbendAbility(this); + } + + @Override + public String getRule() { + return "Waterbend " + CardUtil.getTextWithFirstCharUpperCase(super.getRule()); + } +} diff --git a/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java b/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java index 9e9b2f9ead3..ce1a111e405 100644 --- a/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/mana/ActivatedManaAbilityImpl.java @@ -44,19 +44,16 @@ public abstract class ActivatedManaAbilityImpl extends ActivatedAbilityImpl impl public ActivationStatus canActivate(UUID playerId, Game game) { // check if player is in the process of playing spell costs and they are no longer allowed to use // activated mana abilities (e.g. because they started to use improvise or convoke) - if (!game.getStack().isEmpty()) { - StackObject stackObject = game.getStack().getFirst(); - if (stackObject instanceof Spell) { - switch (((Spell) stackObject).getCurrentActivatingManaAbilitiesStep()) { - case BEFORE: - case NORMAL: - break; - case AFTER: - return ActivationStatus.getFalse(); - } + StackObject stackObject = game.getStack().getFirstOrNull(); + if (stackObject instanceof Spell) { + switch (((Spell) stackObject).getCurrentActivatingManaAbilitiesStep()) { + case BEFORE: + case NORMAL: + break; + case AFTER: + return ActivationStatus.getFalse(); } } - return super.canActivate(playerId, game); } diff --git a/Mage/src/main/java/mage/cards/decks/importer/CardLookup.java b/Mage/src/main/java/mage/cards/decks/importer/CardLookup.java index f4ae4e9750a..06727a5e7a5 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/CardLookup.java +++ b/Mage/src/main/java/mage/cards/decks/importer/CardLookup.java @@ -3,9 +3,10 @@ package mage.cards.decks.importer; import mage.cards.repository.CardCriteria; import mage.cards.repository.CardInfo; import mage.cards.repository.CardRepository; +import mage.util.CardUtil; import java.util.List; -import java.util.Optional; +import java.util.Objects; /** * Deck import: helper class to mock cards repository @@ -14,49 +15,40 @@ public class CardLookup { public static final CardLookup instance = new CardLookup(); - public Optional lookupCardInfo(String name) { - return Optional.ofNullable(CardRepository.instance.findPreferredCoreExpansionCard(name)); + public CardInfo lookupCardInfo(String name) { + return CardRepository.instance.findPreferredCoreExpansionCard(name); } public List lookupCardInfo(CardCriteria criteria) { + // can be override to make fake lookup, e.g. for tests return CardRepository.instance.findCards(criteria); } - public Optional lookupCardInfo(String name, String set) { - if (set == null) { - return lookupCardInfo(name); + public CardInfo lookupCardInfo(String name, String set, String cardNumber) { + CardCriteria cardCriteria = new CardCriteria(); + cardCriteria.name(name); + if (set != null) { + cardCriteria.setCodes(set); + } + if (cardNumber != null) { + int intCardNumber = CardUtil.parseCardNumberAsInt(cardNumber); + cardCriteria.minCardNumber(intCardNumber); + cardCriteria.maxCardNumber(intCardNumber); + } + List foundCards = lookupCardInfo(cardCriteria); + + CardInfo res = null; + + // if possible then use strict card number + if (cardNumber != null) { + res = foundCards.stream().filter(c -> Objects.equals(c.getCardNumber(), cardNumber)).findAny().orElse(null); } - Optional result = lookupCardInfo(new CardCriteria().name(name).setCodes(set)) - .stream() - .findAny(); - if (result.isPresent()) { - return result; + if (res == null) { + res = foundCards.stream().findAny().orElse(null); } - return lookupCardInfo(name); - } - - public Optional lookupCardInfo(String name, String set, String cardNumber) { - Optional result; - try { - int intCardNumber = Integer.parseInt(cardNumber); - result = lookupCardInfo( - new CardCriteria() - .name(name) - .setCodes(set) - .minCardNumber(intCardNumber) - .maxCardNumber(intCardNumber)) - .stream() - .findAny(); - if (result.isPresent()) { - return result; - } - } catch (NumberFormatException ignore) { - /* ignored */ - } - - return lookupCardInfo(name, set); + return res; } } diff --git a/Mage/src/main/java/mage/cards/decks/importer/CodDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/CodDeckImporter.java index 7d75dc94f7a..19fd2086573 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/CodDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/CodDeckImporter.java @@ -66,14 +66,13 @@ public class CodDeckImporter extends XmlDeckImporter { private static Function> toDeckCardInfo(CardLookup lookup, StringBuilder errors) { return node -> { String name = node.getAttributes().getNamedItem("name").getNodeValue().trim(); - Optional cardInfo = lookup.lookupCardInfo(name); - if (cardInfo.isPresent()) { - CardInfo info = cardInfo.get(); + CardInfo cardInfo = lookup.lookupCardInfo(name); + if (cardInfo != null) { int amount = getQuantityFromNode(node); - DeckCardInfo.makeSureCardAmountFine(amount, info.getName()); + DeckCardInfo.makeSureCardAmountFine(amount, cardInfo.getName()); return Collections.nCopies( amount, - new DeckCardInfo(info.getName(), info.getCardNumber(), info.getSetCode()) + new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode()) ).stream(); } else { errors.append("Could not find card: '").append(name).append("'\n"); diff --git a/Mage/src/main/java/mage/cards/decks/importer/DecDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/DecDeckImporter.java index 0ec494d82b1..173a2ad88f4 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/DecDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/DecDeckImporter.java @@ -33,11 +33,10 @@ public class DecDeckImporter extends PlainTextDeckImporter { String lineName = line.substring(delim).trim(); try { int num = Integer.parseInt(lineNum); - Optional cardLookup = getCardLookup().lookupCardInfo(lineName); - if (!cardLookup.isPresent()) { + CardInfo cardInfo = getCardLookup().lookupCardInfo(lineName); + if (cardInfo == null) { sbMessage.append("Could not find card: '").append(lineName).append("' at line ").append(lineCount).append('\n'); } else { - CardInfo cardInfo = cardLookup.get(); DeckCardInfo.makeSureCardAmountFine(num, cardInfo.getName()); DeckCardInfo deckCardInfo = new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode()); for (int i = 0; i < num; i++) { diff --git a/Mage/src/main/java/mage/cards/decks/importer/DekDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/DekDeckImporter.java index 92c7df9a6cf..1cc478d0913 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/DekDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/DekDeckImporter.java @@ -32,7 +32,7 @@ public class DekDeckImporter extends PlainTextDeckImporter { } boolean isSideboard = "true".equals(extractAttribute(line, "Sideboard")); - CardInfo cardInfo = getCardLookup().lookupCardInfo(cardName).orElse(null); + CardInfo cardInfo = getCardLookup().lookupCardInfo(cardName); if (cardInfo == null) { sbMessage.append("Could not find card: '").append(cardName).append("' at line ").append(lineCount).append('\n'); } else { diff --git a/Mage/src/main/java/mage/cards/decks/importer/DraftLogImporter.java b/Mage/src/main/java/mage/cards/decks/importer/DraftLogImporter.java index aea081ed5c7..8b99bd864e4 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/DraftLogImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/DraftLogImporter.java @@ -29,9 +29,9 @@ public class DraftLogImporter extends PlainTextDeckImporter { Matcher pickMatcher = PICK_PATTERN.matcher(line); if (pickMatcher.matches()) { String name = pickMatcher.group(1); - CardInfo card = getCardLookup().lookupCardInfo(name, currentSet).orElse(null); - if (card != null) { - deckList.getCards().add(new DeckCardInfo(card.getName(), card.getCardNumber(), card.getSetCode())); + CardInfo cardInfo = getCardLookup().lookupCardInfo(name, currentSet, null); + if (cardInfo != null) { + deckList.getCards().add(new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode())); } else { sbMessage.append("couldn't find: \"").append(name).append("\"\n"); } diff --git a/Mage/src/main/java/mage/cards/decks/importer/MWSDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/MWSDeckImporter.java index 94220902879..7458d3a54ce 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/MWSDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/MWSDeckImporter.java @@ -36,9 +36,9 @@ public class MWSDeckImporter extends PlainTextDeckImporter { int num = Integer.parseInt(lineNum); CardInfo cardInfo = null; if (setCode.isEmpty()) { - cardInfo = getCardLookup().lookupCardInfo(lineName, setCode).orElse(null); + cardInfo = getCardLookup().lookupCardInfo(lineName, setCode, null); } else { - cardInfo = getCardLookup().lookupCardInfo(lineName).orElse(null); + cardInfo = getCardLookup().lookupCardInfo(lineName); } if (cardInfo == null) { diff --git a/Mage/src/main/java/mage/cards/decks/importer/MtgaImporter.java b/Mage/src/main/java/mage/cards/decks/importer/MtgaImporter.java index 8e5047ddda0..471af47c595 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/MtgaImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/MtgaImporter.java @@ -8,6 +8,7 @@ import mage.cards.repository.CardInfo; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -18,7 +19,11 @@ import java.util.regex.Pattern; public class MtgaImporter extends PlainTextDeckImporter { private static final Map SET_REMAPPING = ImmutableMap.of("DAR", "DOM"); - private static final Pattern MTGA_PATTERN = Pattern.compile( + + // example: + // 4 Accumulated Knowledge (A25) 40 + // 4 Accumulated Knowledge + public static final Pattern MTGA_PATTERN = Pattern.compile( "(\\p{Digit}+)" + "\\p{javaWhitespace}+" + "(" + CardNameUtil.CARD_NAME_PATTERN.pattern() + ")" + @@ -33,13 +38,19 @@ public class MtgaImporter extends PlainTextDeckImporter { @Override protected void readLine(String line, DeckCardLists deckList, FixedInfo fixedInfo) { - line = line.trim(); + // moxfield support - remove foil status + line = line.replace(" *F*", ""); - if (line.equals("Deck")) { + line = line.trim(); + String lowerLine = line.toLowerCase(Locale.ENGLISH); + + // mainboard to support decks from archidekt.com + if (lowerLine.startsWith("deck") || lowerLine.startsWith("mainboard")) { + sideboard = false; return; } - if (line.equals("Sideboard") || line.equals("")) { + if (lowerLine.startsWith("sideboard") || lowerLine.startsWith("commander") || lowerLine.startsWith("maybeboard") || lowerLine.equals("")) { sideboard = true; return; } @@ -54,12 +65,12 @@ public class MtgaImporter extends PlainTextDeckImporter { CardInfo found; int count = Integer.parseInt(pattern.group(1)); String name = pattern.group(2); - if (pattern.group(3) != null && pattern.group(4) != null) { + if (pattern.group(3) != null) { String set = SET_REMAPPING.getOrDefault(pattern.group(3), pattern.group(3)); - String cardNumber = pattern.group(4); - found = lookup.lookupCardInfo(name, set, cardNumber).orElse(null); + String cardNumber = pattern.groupCount() >= 4 ? pattern.group(4) : null; + found = lookup.lookupCardInfo(name, set, cardNumber); } else { - found = lookup.lookupCardInfo(name).orElse(null); + found = lookup.lookupCardInfo(name, null, null); } if (found == null) { @@ -74,4 +85,33 @@ public class MtgaImporter extends PlainTextDeckImporter { zone.addAll(Collections.nCopies(count, deckCardInfo.copy())); } + public static boolean isMTGA(String data) { + // examples: + // 4 Accumulated Knowledge (A25) 40 + // 4 Accumulated Knowledge + // Deck + // Commander + // Mainboard - extra mark for archidekt.com + // Sideboard - extra mark for archidekt.com + // Maybeboard - extra mark for archidekt.com + + String firstLine = data.split("\\R", 2)[0]; + String firstLineLower = firstLine.toLowerCase(Locale.ENGLISH); + + // by deck marks + if (firstLineLower.startsWith("deck") + || firstLineLower.startsWith("mainboard") + || firstLineLower.startsWith("sideboard") + || firstLineLower.startsWith("commander") + || firstLineLower.startsWith("maybeboard")) { + return true; + } + + // by card marks + Matcher pattern = MtgaImporter.MTGA_PATTERN.matcher(CardNameUtil.normalizeCardName(firstLine)); + return pattern.matches() + && pattern.groupCount() >= 3 + && pattern.group(3) != null; + } + } diff --git a/Mage/src/main/java/mage/cards/decks/importer/MtgjsonDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/MtgjsonDeckImporter.java index fe3c6cacbba..ab89c48abe6 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/MtgjsonDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/MtgjsonDeckImporter.java @@ -63,11 +63,10 @@ public class MtgjsonDeckImporter extends JsonDeckImporter { } int num = JsonUtil.getAsInt(card, "count"); - Optional cardLookup = getCardLookup().lookupCardInfo(name, setCode); - if (!cardLookup.isPresent()) { + CardInfo cardInfo = getCardLookup().lookupCardInfo(name, setCode, null); + if (cardInfo == null) { sbMessage.append("Could not find card: '").append(name).append("'\n"); } else { - CardInfo cardInfo = cardLookup.get(); for (int i = 0; i < num; i++) { list.add(new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode())); } diff --git a/Mage/src/main/java/mage/cards/decks/importer/O8dDeckImporter.java b/Mage/src/main/java/mage/cards/decks/importer/O8dDeckImporter.java index 96fdb210c80..4fd27f3b662 100644 --- a/Mage/src/main/java/mage/cards/decks/importer/O8dDeckImporter.java +++ b/Mage/src/main/java/mage/cards/decks/importer/O8dDeckImporter.java @@ -63,12 +63,11 @@ public class O8dDeckImporter extends XmlDeckImporter { private static Function> toDeckCardInfo(CardLookup lookup, StringBuilder errors) { return node -> { String name = node.getTextContent(); - Optional cardInfo = lookup.lookupCardInfo(name); - if (cardInfo.isPresent()) { - CardInfo info = cardInfo.get(); + CardInfo cardInfo = lookup.lookupCardInfo(name); + if (cardInfo != null) { return Collections.nCopies( getQuantityFromNode(node), - new DeckCardInfo(info.getName(), info.getCardNumber(), info.getSetCode())).stream(); + new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode())).stream(); } else { errors.append("Could not find card: '").append(name).append("'\n"); return Stream.empty(); diff --git a/Mage/src/main/java/mage/collectors/DataCollector.java b/Mage/src/main/java/mage/collectors/DataCollector.java index db3471a4acd..399b304b815 100644 --- a/Mage/src/main/java/mage/collectors/DataCollector.java +++ b/Mage/src/main/java/mage/collectors/DataCollector.java @@ -2,6 +2,7 @@ package mage.collectors; import mage.game.Game; import mage.game.Table; +import mage.players.Player; import java.util.UUID; @@ -11,10 +12,12 @@ import java.util.UUID; * Supported features: * - [x] collect and print game logs in server output, including unit tests * - [x] collect and save full games history and decks - * - [ ] collect and print performance metrics like ApplyEffects calc time or inform players time (pings) - * - [ ] collect and send metrics to third party tools like prometheus + grafana - * - [ ] prepare "attachable" game data for bug reports - * - [ ] record game replays data (GameView history) + * - [ ] TODO: collect and print performance metrics like ApplyEffects calc time or inform players time (pings) + * - [ ] TODO: collect and send metrics to third party tools like prometheus + grafana + * - [x] tests: print used selections (choices, targets, modes, skips) TODO: add yes/no, replacement effect, coins, other choices + * - [ ] TODO: tests: print additional info like current resolve ability? + * - [ ] TODO: prepare "attachable" game data for bug reports + * - [ ] TODO: record game replays data (GameView history) *

* How-to enable or disable: * - use java params like -Dxmage.dataCollectors.saveGameHistory=true @@ -62,7 +65,27 @@ public interface DataCollector { void onChatTable(UUID tableId, String userName, String message); /** - * @param gameId chat sessings don't have full game access, so use onGameStart event to find game's ID before chat + * @param gameId chat session don't have full game access, so use onGameStart event to find game's ID before chat */ void onChatGame(UUID gameId, String userName, String message); + + /** + * Tests only: on any non-target choice like yes/no, mode, etc + */ + void onTestsChoiceUse(Game game, Player player, String usingChoice, String reason); + + /** + * Tests only: on any target choice + */ + void onTestsTargetUse(Game game, Player player, String usingTarget, String reason); + + /** + * Tests only: on push object to stack (calls before activate and make any choice/announce) + */ + void onTestsStackPush(Game game); + + /** + * Tests only: on stack object resolve (calls before starting resolve) + */ + void onTestsStackResolve(Game game); } diff --git a/Mage/src/main/java/mage/collectors/DataCollectorServices.java b/Mage/src/main/java/mage/collectors/DataCollectorServices.java index dc2740f078a..52a18713f37 100644 --- a/Mage/src/main/java/mage/collectors/DataCollectorServices.java +++ b/Mage/src/main/java/mage/collectors/DataCollectorServices.java @@ -4,6 +4,7 @@ import mage.collectors.services.PrintGameLogsDataCollector; import mage.collectors.services.SaveGameHistoryDataCollector; import mage.game.Game; import mage.game.Table; +import mage.players.Player; import org.apache.log4j.Logger; import java.util.LinkedHashSet; @@ -140,4 +141,28 @@ final public class DataCollectorServices implements DataCollector { public void onChatGame(UUID gameId, String userName, String message) { activeServices.forEach(c -> c.onChatGame(gameId, userName, message)); } + + @Override + public void onTestsChoiceUse(Game game, Player player, String usingChoice, String reason) { + if (game.isSimulation()) return; + activeServices.forEach(c -> c.onTestsChoiceUse(game, player, usingChoice, reason)); + } + + @Override + public void onTestsTargetUse(Game game, Player player, String usingTarget, String reason) { + if (game.isSimulation()) return; + activeServices.forEach(c -> c.onTestsTargetUse(game, player, usingTarget, reason)); + } + + @Override + public void onTestsStackPush(Game game) { + if (game.isSimulation()) return; + activeServices.forEach(c -> c.onTestsStackPush(game)); + } + + @Override + public void onTestsStackResolve(Game game) { + if (game.isSimulation()) return; + activeServices.forEach(c -> c.onTestsStackResolve(game)); + } } diff --git a/Mage/src/main/java/mage/collectors/services/EmptyDataCollector.java b/Mage/src/main/java/mage/collectors/services/EmptyDataCollector.java index 2e5f1793e81..c2e59fe7806 100644 --- a/Mage/src/main/java/mage/collectors/services/EmptyDataCollector.java +++ b/Mage/src/main/java/mage/collectors/services/EmptyDataCollector.java @@ -3,6 +3,7 @@ package mage.collectors.services; import mage.collectors.DataCollector; import mage.game.Game; import mage.game.Table; +import mage.players.Player; import java.util.UUID; @@ -67,4 +68,24 @@ public abstract class EmptyDataCollector implements DataCollector { public void onChatGame(UUID gameId, String userName, String message) { // nothing } + + @Override + public void onTestsChoiceUse(Game game, Player player, String usingChoice, String reason) { + // nothing + } + + @Override + public void onTestsTargetUse(Game game, Player player, String usingTarget, String reason) { + // nothing + } + + @Override + public void onTestsStackPush(Game game) { + // nothing + } + + @Override + public void onTestsStackResolve(Game game) { + // nothing + } } diff --git a/Mage/src/main/java/mage/collectors/services/PrintGameLogsDataCollector.java b/Mage/src/main/java/mage/collectors/services/PrintGameLogsDataCollector.java index f8fc9a8966a..c6c8741505c 100644 --- a/Mage/src/main/java/mage/collectors/services/PrintGameLogsDataCollector.java +++ b/Mage/src/main/java/mage/collectors/services/PrintGameLogsDataCollector.java @@ -1,7 +1,9 @@ package mage.collectors.services; import mage.game.Game; +import mage.players.Player; import mage.util.CardUtil; +import mage.util.ConsoleUtil; import org.apache.log4j.Logger; import org.jsoup.Jsoup; @@ -40,9 +42,47 @@ public class PrintGameLogsDataCollector extends EmptyDataCollector { @Override public void onGameLog(Game game, String message) { String needMessage = Jsoup.parse(message).text(); - writeLog("GAME", "LOG", String.format("%s: %s", + writeLog("LOG", "GAME", String.format("%s: %s", CardUtil.getTurnInfo(game), needMessage )); } + + @Override + public void onTestsChoiceUse(Game game, Player player, String choice, String reason) { + String needReason = Jsoup.parse(reason).text(); + writeLog("LOG", "GAME", ConsoleUtil.asYellow(String.format("%s: %s using choice: %s%s", + CardUtil.getTurnInfo(game), + player.getName(), + choice, + reason.isEmpty() ? "" : " (" + needReason + ")" + ))); + } + + @Override + public void onTestsTargetUse(Game game, Player player, String target, String reason) { + String needReason = Jsoup.parse(reason).text(); + writeLog("LOG", "GAME", ConsoleUtil.asYellow(String.format("%s: %s using target: %s%s", + CardUtil.getTurnInfo(game), + player.getName(), + target, + reason.isEmpty() ? "" : " (" + needReason + ")" + ))); + } + + @Override + public void onTestsStackPush(Game game) { + writeLog("LOG", "GAME", String.format("%s: Stack push: %s", + CardUtil.getTurnInfo(game), + game.getStack().toString() + )); + } + + @Override + public void onTestsStackResolve(Game game) { + writeLog("LOG", "GAME", String.format("%s: Stack resolve: %s", + CardUtil.getTurnInfo(game), + game.getStack().toString() + )); + } } diff --git a/Mage/src/main/java/mage/constants/SubType.java b/Mage/src/main/java/mage/constants/SubType.java index b354e9dca7f..53c9ecd2fe3 100644 --- a/Mage/src/main/java/mage/constants/SubType.java +++ b/Mage/src/main/java/mage/constants/SubType.java @@ -109,6 +109,7 @@ public enum SubType { BEHOLDER("Beholder", SubTypeSet.CreatureType), BERSERKER("Berserker", SubTypeSet.CreatureType), BIRD("Bird", SubTypeSet.CreatureType), + BISON("Bison", SubTypeSet.CreatureType), BITH("Bith", SubTypeSet.CreatureType, true), // Star Wars BLINKMOTH("Blinkmoth", SubTypeSet.CreatureType), BOAR("Boar", SubTypeSet.CreatureType), diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/ConvokedSourcePredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/ConvokedSourcePredicate.java index cbb2b853a42..09ead62f82e 100644 --- a/Mage/src/main/java/mage/filter/predicate/permanent/ConvokedSourcePredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/permanent/ConvokedSourcePredicate.java @@ -17,7 +17,7 @@ public enum ConvokedSourcePredicate implements ObjectSourcePlayerPredicate input, Game game) { - HashSet set = CardUtil.getSourceCostsTag(game, input.getSource(), ConvokeAbility.convokingCreaturesKey, new HashSet<>(0)); + HashSet set = CardUtil.getSourceCostsTag(game, input.getSource(), ConvokeAbility.convokingCreaturesKey, new HashSet<>()); return set.contains(new MageObjectReference(input.getObject(), game)); } } diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index ad0cd9232d4..ea21c5102b5 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -162,7 +162,7 @@ public interface Game extends MageItem, Serializable, Copyable { // Result must be checked for null. Possible errors search pattern: (\S*) = game.getPlayer.+\n(?!.+\1 != null) Player getPlayer(UUID playerId); - Player getPlayerOrPlaneswalkerController(UUID playerId); + Player getPlayerOrPlaneswalkerController(UUID targetId); /** * Static players list from start of the game. Use it to find player by ID or in game engine. diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index e3f59becee3..978171210d6 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -412,12 +412,12 @@ public abstract class GameImpl implements Game { } @Override - public Player getPlayerOrPlaneswalkerController(UUID playerId) { - Player player = getPlayer(playerId); + public Player getPlayerOrPlaneswalkerController(UUID targetId) { + Player player = getPlayer(targetId); if (player != null) { return player; } - Permanent permanent = getPermanent(playerId); + Permanent permanent = getPermanent(targetId); if (permanent == null) { return null; } @@ -1841,6 +1841,7 @@ public abstract class GameImpl implements Game { boolean wasError = false; try { top = state.getStack().peek(); + DataCollectorServices.getInstance().onTestsStackResolve(this); top.resolve(this); resetControlAfterSpellResolve(top.getId()); } catch (Throwable e) { @@ -2819,7 +2820,7 @@ public abstract class GameImpl implements Game { if (attachedTo != null) { for (Ability ability : perm.getAbilities(this)) { if (ability instanceof AttachableToRestrictedAbility) { - if (!((AttachableToRestrictedAbility) ability).canEquip(attachedTo.getId(), null, this)) { + if (!((AttachableToRestrictedAbility) ability).canEquip(attachedTo.getId(), this)) { attachedTo = null; break; } diff --git a/Mage/src/main/java/mage/game/ZonesHandler.java b/Mage/src/main/java/mage/game/ZonesHandler.java index 809fc545a32..d6aa18cd37c 100644 --- a/Mage/src/main/java/mage/game/ZonesHandler.java +++ b/Mage/src/main/java/mage/game/ZonesHandler.java @@ -253,9 +253,9 @@ public final class ZonesHandler { spell = new Spell(card, card.getSpellAbility().copy(), card.getOwnerId(), event.getFromZone(), game); } spell.syncZoneChangeCounterOnStack(card, game); - game.getStack().push(spell); game.getState().setZone(spell.getId(), Zone.STACK); game.getState().setZone(card.getId(), Zone.STACK); + game.getStack().push(game, spell); } break; case BATTLEFIELD: diff --git a/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilationDungeon.java b/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilationDungeon.java index af928e4acfb..a9416f76d98 100644 --- a/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilationDungeon.java +++ b/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilationDungeon.java @@ -188,28 +188,22 @@ class OublietteTarget extends TargetSacrifice { return new OublietteTarget(this); } - @Override - public boolean canTarget(UUID playerId, UUID id, Ability ability, Game game) { - if (!super.canTarget(playerId, id, ability, game)) { - return false; - } - Permanent permanent = game.getPermanent(id); - if (permanent == null) { - return false; - } - if (this.getTargets().isEmpty()) { - return true; - } - Cards cards = new CardsImpl(this.getTargets()); - cards.add(permanent); - return cardTypeAssigner.getRoleCount(cards, game) >= cards.size(); - } - - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, source, game)); + + // only valid roles + Cards existingTargets = new CardsImpl(this.getTargets()); + possibleTargets.removeIf(id -> { + Permanent permanent = game.getPermanent(id); + if (permanent == null) { + return true; + } + Cards newTargets = existingTargets.copy(); + newTargets.add(permanent); + return cardTypeAssigner.getRoleCount(newTargets, game) < newTargets.size(); + }); + return possibleTargets; } diff --git a/Mage/src/main/java/mage/game/command/emblems/EmblemOfCard.java b/Mage/src/main/java/mage/game/command/emblems/EmblemOfCard.java index ad9f4f4c560..853a832d931 100644 --- a/Mage/src/main/java/mage/game/command/emblems/EmblemOfCard.java +++ b/Mage/src/main/java/mage/game/command/emblems/EmblemOfCard.java @@ -29,11 +29,11 @@ public final class EmblemOfCard extends Emblem { String setCode, String infoTypeForError ) { - int cardNumberInt = CardUtil.parseCardNumberAsInt(cardNumber); + int intCardNumber = CardUtil.parseCardNumberAsInt(cardNumber); List found = CardRepository.instance.findCards(new CardCriteria() .name(cardName) - .minCardNumber(cardNumberInt) - .maxCardNumber(cardNumberInt) + .minCardNumber(intCardNumber) + .maxCardNumber(intCardNumber) .setCodes(setCode)); return found.stream() .filter(ci -> ci.getCardNumber().equals(cardNumber)) diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 21ad27b5d95..ed46fcc7717 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -688,6 +688,17 @@ public class GameEvent implements Serializable { /* rad counter life loss/gain effect */ RADIATION_GAIN_LIFE, + /* for checking sacrifice as a cost + targetId the permanent to be sacrificed + sourceId of the ability + playerId controller of ability + data id of the ability being paid for + */ + PAY_SACRIFICE_COST, + EARTHBENDED, + AIRBENDED, + FIREBENDED, + WATERBENDED, // custom events - must store some unique data to track CUSTOM_EVENT; diff --git a/Mage/src/main/java/mage/game/events/TargetEvent.java b/Mage/src/main/java/mage/game/events/TargetEvent.java index f2c90ebc87b..f9b9f96484f 100644 --- a/Mage/src/main/java/mage/game/events/TargetEvent.java +++ b/Mage/src/main/java/mage/game/events/TargetEvent.java @@ -12,24 +12,21 @@ import java.util.UUID; public class TargetEvent extends GameEvent { /** - * @param target - * @param sourceId * @param sourceControllerId can be different from real controller (example: ability instructs another player to targeting) */ public TargetEvent(Card target, UUID sourceId, UUID sourceControllerId) { - super(GameEvent.EventType.TARGET, target.getId(), null, sourceControllerId); - this.setSourceId(sourceId); + this(target.getId(), sourceId, sourceControllerId); } public TargetEvent(Player target, UUID sourceId, UUID sourceControllerId) { - super(GameEvent.EventType.TARGET, target.getId(), null, sourceControllerId); + this(target.getId(), sourceId, sourceControllerId); + } + + public TargetEvent(UUID targetId, UUID sourceId, UUID sourceControllerId) { + super(GameEvent.EventType.TARGET, targetId, null, sourceControllerId); this.setSourceId(sourceId); } - /** - * @param targetId - * @param source - */ public TargetEvent(UUID targetId, Ability source) { super(GameEvent.EventType.TARGET, targetId, source, source.getControllerId()); } diff --git a/Mage/src/main/java/mage/game/permanent/token/AllyToken.java b/Mage/src/main/java/mage/game/permanent/token/AllyToken.java new file mode 100644 index 00000000000..d133366f7b0 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/AllyToken.java @@ -0,0 +1,29 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.constants.CardType; +import mage.constants.SubType; + +/** + * @author TheElk801 + */ +public final class AllyToken extends TokenImpl { + + public AllyToken() { + super("Ally Token", "1/1 white Ally creature token"); + cardType.add(CardType.CREATURE); + color.setWhite(true); + subtype.add(SubType.ALLY); + power = new MageInt(1); + toughness = new MageInt(1); + } + + private AllyToken(final AllyToken token) { + super(token); + } + + public AllyToken copy() { + return new AllyToken(this); + } + +} diff --git a/Mage/src/main/java/mage/game/permanent/token/SoldierFirebendingToken.java b/Mage/src/main/java/mage/game/permanent/token/SoldierFirebendingToken.java new file mode 100644 index 00000000000..80dd650652a --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/SoldierFirebendingToken.java @@ -0,0 +1,30 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.keyword.FirebendingAbility; +import mage.constants.CardType; +import mage.constants.SubType; + +/** + * @author TheElk801 + */ +public final class SoldierFirebendingToken extends TokenImpl { + + public SoldierFirebendingToken() { + super("Soldier Token", "2/2 red Soldier creature token with firebending 1"); + cardType.add(CardType.CREATURE); + color.setRed(true); + subtype.add(SubType.SOLDIER); + power = new MageInt(2); + toughness = new MageInt(2); + this.addAbility(new FirebendingAbility(1)); + } + + private SoldierFirebendingToken(final SoldierFirebendingToken token) { + super(token); + } + + public SoldierFirebendingToken copy() { + return new SoldierFirebendingToken(this); + } +} diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 7cc905df6b6..2b1ed1937c6 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -42,7 +42,11 @@ public class Spell extends StackObjectImpl implements Card { private final List spellAbilities = new ArrayList<>(); + // this.card.getSpellAbility() - blueprint ability with zero selected targets + // this.ability - real casting ability with selected targets + // so if you need to check targets on cast then try to use "Ability source" param first (e.g. aura legality on etb) private final Card card; + private ManaCosts manaCost; private final ObjectColor color; private final ObjectColor frameColor; @@ -1157,7 +1161,7 @@ public class Spell extends StackObjectImpl implements Card { applier.modifySpell(spellCopy, game); } spellCopy.setZone(Zone.STACK, game); // required for targeting ex: Nivmagus Elemental - game.getStack().push(spellCopy); + game.getStack().push(game, spellCopy); // new targets if (newTargetFilterPredicate != null) { diff --git a/Mage/src/main/java/mage/game/stack/SpellStack.java b/Mage/src/main/java/mage/game/stack/SpellStack.java index 83458c5980b..149720cc740 100644 --- a/Mage/src/main/java/mage/game/stack/SpellStack.java +++ b/Mage/src/main/java/mage/game/stack/SpellStack.java @@ -3,6 +3,7 @@ package mage.game.stack; import mage.MageObject; import mage.abilities.Ability; +import mage.collectors.DataCollectorServices; import mage.constants.PutCards; import mage.constants.Zone; import mage.game.Game; @@ -50,6 +51,16 @@ public class SpellStack extends ArrayDeque { } } + @Override + @Deprecated // must use getFirstOrNull instead + public StackObject getFirst() { + return super.getFirst(); + } + + public StackObject getFirstOrNull() { + return this.isEmpty() ? null : this.getFirst(); + } + public boolean remove(StackObject object, Game game) { for (StackObject spell : this) { if (spell.getId().equals(object.getId())) { @@ -136,11 +147,18 @@ public class SpellStack extends ArrayDeque { } @Override + @Deprecated // must use only full version with game param public void push(StackObject e) { super.push(e); this.dateLastAdded = new Date(); } + public void push(Game game, StackObject e) { + super.push(e); + this.dateLastAdded = new Date(); + DataCollectorServices.getInstance().onTestsStackPush(game); + } + public Date getDateLastAdded() { return dateLastAdded; } diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index f83a90a5697..a6f80c416c1 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -744,7 +744,7 @@ public class StackAbility extends StackObjectImpl implements Ability { newAbility.setControllerId(newControllerId); StackAbility newStackAbility = new StackAbility(newAbility, newControllerId); - game.getStack().push(newStackAbility); + game.getStack().push(game, newStackAbility); // new targets if (newTargetFilterPredicate != null) { diff --git a/Mage/src/main/java/mage/players/ManaPool.java b/Mage/src/main/java/mage/players/ManaPool.java index 82bca7edcef..1311c7408fa 100644 --- a/Mage/src/main/java/mage/players/ManaPool.java +++ b/Mage/src/main/java/mage/players/ManaPool.java @@ -9,6 +9,7 @@ import mage.abilities.costs.Cost; import mage.abilities.effects.mana.ManaEffect; import mage.constants.Duration; import mage.constants.ManaType; +import mage.constants.PhaseStep; import mage.constants.TurnPhase; import mage.filter.Filter; import mage.filter.FilterMana; @@ -302,9 +303,17 @@ public class ManaPool implements Serializable { } private int emptyItem(ManaPoolItem item, Emptiable toEmpty, Game game, ManaType manaType) { - if (item.getDuration() == Duration.EndOfTurn - && game.getTurnPhaseType() != TurnPhase.END) { - return 0; + switch (item.getDuration()) { + case EndOfTurn: + if (game.getTurnPhaseType() != TurnPhase.END) { + return 0; + } + break; + case EndOfCombat: + if (game.getTurnPhaseType() != TurnPhase.COMBAT + || game.getTurnStepType() != PhaseStep.END_COMBAT) { + return 0; + } } if (manaBecomesBlack) { int amount = toEmpty.get(manaType); @@ -366,6 +375,10 @@ public class ManaPool implements Serializable { } public void addMana(Mana manaToAdd, Game game, Ability source, boolean dontLoseUntilEOT) { + addMana(manaToAdd, game, source, dontLoseUntilEOT ? Duration.EndOfTurn : null); + } + + public void addMana(Mana manaToAdd, Game game, Ability source, Duration duration) { if (manaToAdd != null) { Mana mana = manaToAdd.copy(); if (!game.replaceEvent(new ManaEvent(EventType.ADD_MANA, source.getId(), source, playerId, mana))) { @@ -376,8 +389,8 @@ public class ManaPool implements Serializable { source.getSourceObject(game), conditionalMana.getManaProducerOriginalId() != null ? conditionalMana.getManaProducerOriginalId() : source.getOriginalId() ); - if (dontLoseUntilEOT) { - item.setDuration(Duration.EndOfTurn); + if (duration != null) { + item.setDuration(duration); } this.manaItems.add(item); } else { @@ -392,8 +405,8 @@ public class ManaPool implements Serializable { source.getOriginalId(), mana.getFlag() ); - if (dontLoseUntilEOT) { - item.setDuration(Duration.EndOfTurn); + if (duration != null) { + item.setDuration(duration); } this.manaItems.add(item); } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 5e3e89f8a8d..084919b8f6d 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -20,7 +20,6 @@ import mage.designations.Designation; import mage.designations.DesignationType; import mage.filter.FilterCard; import mage.filter.FilterMana; -import mage.filter.FilterPermanent; import mage.game.*; import mage.game.draft.Draft; import mage.game.events.GameEvent; @@ -37,10 +36,7 @@ import mage.util.Copyable; import mage.util.MultiAmountMessage; import java.io.Serializable; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; /** @@ -57,16 +53,13 @@ import java.util.stream.Collectors; public interface Player extends MageItem, Copyable { /** - * Enum used to indicate what each player is allowed to spend life on. - * By default it is set to `allAbilities`, but can be changed by effects. - * E.g. Angel of Jubilation sets it to `nonSpellnonActivatedAbilities`, - * and Karn's Sylex sets it to `onlyManaAbilities`. - *

- *

- * Default is PayLifeCostLevel.allAbilities. + * Enum used to indicate what each player is not allowed to spend life on. + * By default a player has no restrictions, but can be changed by effects. + * E.g. Angel of Jubilation adds `CAST_SPELLS` and 'ACTIVATE_ABILITIES', + * and Karn's Sylex adds `CAST_SPELLS` and 'ACTIVATE_NON_MANA_ABILITIES'. */ - enum PayLifeCostLevel { - allAbilities, nonSpellnonActivatedAbilities, onlyManaAbilities, none + enum PayLifeCostRestriction { + CAST_SPELLS, ACTIVATE_NON_MANA_ABILITIES, ACTIVATE_MANA_ABILITIES } /** @@ -177,14 +170,14 @@ public interface Player extends MageItem, Copyable { boolean isCanGainLife(); /** - * Is the player allowed to pay life for casting spells or activate activated abilities + * Adds a {@link PayLifeCostRestriction} to the set of restrictions. * - * @param payLifeCostLevel + * @param payLifeCostRestriction */ - void setPayLifeCostLevel(PayLifeCostLevel payLifeCostLevel); + void addPayLifeCostRestriction(PayLifeCostRestriction payLifeCostRestriction); - PayLifeCostLevel getPayLifeCostLevel(); + EnumSet getPayLifeCostRestrictions(); /** * Can the player pay life to cast or activate the given ability @@ -194,10 +187,6 @@ public interface Player extends MageItem, Copyable { */ boolean canPayLifeCost(Ability Ability); - void setCanPaySacrificeCostFilter(FilterPermanent filter); - - FilterPermanent getSacrificeCostFilter(); - boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game); void setLifeTotalCanChange(boolean lifeTotalCanChange); @@ -1231,10 +1220,6 @@ public interface Player extends MageItem, Copyable { /** * Only used for test player for pre-setting targets - * - * @param ability - * @param game - * @return */ boolean addTargets(Ability ability, Game game); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 94d126e11f5..ef0b43ea920 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -30,7 +30,6 @@ import mage.designations.DesignationType; import mage.designations.Speed; import mage.filter.FilterCard; import mage.filter.FilterMana; -import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterCreatureForCombat; @@ -101,7 +100,7 @@ public abstract class PlayerImpl implements Player, Serializable { protected Cards sideboard; protected Cards hand; protected Graveyard graveyard; - protected Set commandersIds = new HashSet<>(0); + protected Set commandersIds = new HashSet<>(); protected Abilities abilities; protected Counters counters; @@ -150,14 +149,13 @@ public abstract class PlayerImpl implements Player, Serializable { protected boolean isFastFailInTestMode = true; protected boolean canGainLife = true; protected boolean canLoseLife = true; - protected PayLifeCostLevel payLifeCostLevel = PayLifeCostLevel.allAbilities; + protected EnumSet payLifeCostRestrictions = EnumSet.noneOf(PayLifeCostRestriction.class); protected boolean loseByZeroOrLessLife = true; protected boolean canPlotFromTopOfLibrary = false; protected boolean drawsFromBottom = false; protected boolean drawsOnOpponentsTurn = false; protected int speed = 0; - protected FilterPermanent sacrificeCostFilter; protected List alternativeSourceCosts = new ArrayList<>(); // TODO: rework turn controller to use single list (see other todos) @@ -264,8 +262,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.userData = player.userData; this.matchPlayer = player.matchPlayer; - this.payLifeCostLevel = player.payLifeCostLevel; - this.sacrificeCostFilter = player.sacrificeCostFilter; + this.payLifeCostRestrictions = player.payLifeCostRestrictions; this.alternativeSourceCosts = CardUtil.deepCopyObject(player.alternativeSourceCosts); this.storedBookmark = player.storedBookmark; @@ -364,9 +361,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.inRange.clear(); this.inRange.addAll(((PlayerImpl) player).inRange); - this.payLifeCostLevel = player.getPayLifeCostLevel(); - this.sacrificeCostFilter = player.getSacrificeCostFilter() != null - ? player.getSacrificeCostFilter().copy() : null; + this.payLifeCostRestrictions = player.getPayLifeCostRestrictions(); this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); this.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary(); this.drawsFromBottom = player.isDrawsFromBottom(); @@ -481,14 +476,13 @@ public abstract class PlayerImpl implements Player, Serializable { //this.isTestMode // must keep this.canGainLife = true; this.canLoseLife = true; - this.payLifeCostLevel = PayLifeCostLevel.allAbilities; + this.payLifeCostRestrictions.clear(); this.loseByZeroOrLessLife = true; this.canPlotFromTopOfLibrary = false; this.drawsFromBottom = false; this.drawsOnOpponentsTurn = false; this.speed = 0; - this.sacrificeCostFilter = null; this.alternativeSourceCosts.clear(); this.isGameUnderControl = true; @@ -524,8 +518,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.maxAttackedBy = Integer.MAX_VALUE; this.canGainLife = true; this.canLoseLife = true; - this.payLifeCostLevel = PayLifeCostLevel.allAbilities; - this.sacrificeCostFilter = null; + this.payLifeCostRestrictions.clear(); this.loseByZeroOrLessLife = true; this.canPlotFromTopOfLibrary = false; this.drawsFromBottom = false; @@ -1546,7 +1539,7 @@ public abstract class PlayerImpl implements Player, Serializable { setStoredBookmark(bookmark); // move global bookmark to current state (if you activated mana before then you can't rollback it) ability.newId(); ability.setControllerId(playerId); - game.getStack().push(new StackAbility(ability, playerId)); + game.getStack().push(game, new StackAbility(ability, playerId)); if (ability.activate(game, false)) { game.fireEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATED_ABILITY, ability.getId(), ability, playerId)); @@ -1711,7 +1704,7 @@ public abstract class PlayerImpl implements Player, Serializable { ability.adjustTargets(game); if (ability.canChooseTarget(game, playerId)) { if (ability.isUsesStack()) { - game.getStack().push(new StackAbility(ability, playerId)); + game.getStack().push(game, new StackAbility(ability, playerId)); } if (ability.activate(game, false)) { if ((ability.isUsesStack() @@ -4524,7 +4517,6 @@ public abstract class PlayerImpl implements Player, Serializable { } else if (ability.getCosts().getTargets().getNextUnchosen(game) != null) { addCostTargetOptions(options, ability, 0, game); } - return options; } @@ -4561,29 +4553,45 @@ public abstract class PlayerImpl implements Player, Serializable { } /** - * AI related code + * AI related code, generate all possible usage use cases for activating ability (all possible targets combination) */ protected void addTargetOptions(List options, Ability option, int targetNum, Game game) { // TODO: target options calculated for triggered ability too, but do not used in real game - // TODO: there are rare errors with wrong targetNum - maybe multiple game sims can change same target object somehow? - // do not hide NullPointError here, research instead - for (Target target : option.getTargets().getNextUnchosen(game, targetNum).getTargetOptions(option, game)) { + if (targetNum >= option.getTargets().size()) { + return; + } + + // already selected for some reason (TODO: is it possible?) + Target currentTarget = option.getTargets().get(targetNum); + if (currentTarget.isChoiceSelected()) { + return; + } + + // analyse all possible use cases + for (Target targetOption : currentTarget.getTargetOptions(option, game)) { + // fill target Ability newOption = option.copy(); - if (target instanceof TargetAmount) { - for (UUID targetId : target.getTargets()) { - int amount = target.getTargetAmount(targetId); + if (targetOption instanceof TargetAmount) { + for (UUID targetId : targetOption.getTargets()) { + int amount = targetOption.getTargetAmount(targetId); newOption.getTargets().get(targetNum).addTarget(targetId, amount, newOption, game, true); } } else { - for (UUID targetId : target.getTargets()) { + for (UUID targetId : targetOption.getTargets()) { newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true); } } - if (targetNum < option.getTargets().size() - 2) { // wtf + // don't forget about target's status (if it zero then must set skip choice too) + newOption.getTargets().get(targetNum).setSkipChoice(targetOption.isSkipChoice()); + + if (targetNum + 1 < option.getTargets().size()) { + // fill more targets addTargetOptions(options, newOption, targetNum + 1, game); } else if (!option.getCosts().getTargets().isEmpty()) { + // fill cost addCostTargetOptions(options, newOption, 0, game); } else { + // all filled, ability ready with all targets and costs options.add(newOption); } } @@ -4668,46 +4676,41 @@ public abstract class PlayerImpl implements Player, Serializable { return false; } - switch (payLifeCostLevel) { - case allAbilities: - return true; - case onlyManaAbilities: - return ability.isManaAbility(); - case nonSpellnonActivatedAbilities: - return !ability.getAbilityType().isActivatedAbility() - && ability.getAbilityType() != AbilityType.SPELL; - case none: - default: - return false; + boolean canPay = true; + for (PayLifeCostRestriction restriction : payLifeCostRestrictions) { + switch (restriction) { + case CAST_SPELLS: + canPay &= ability.getAbilityType() != AbilityType.SPELL; + break; + case ACTIVATE_NON_MANA_ABILITIES: + canPay &= !ability.isNonManaActivatedAbility(); + break; + case ACTIVATE_MANA_ABILITIES: + canPay &= !ability.isManaActivatedAbility(); + break; + } } + return canPay; } @Override - public PayLifeCostLevel getPayLifeCostLevel() { - return payLifeCostLevel; + public EnumSet getPayLifeCostRestrictions() { + return payLifeCostRestrictions; } @Override - public void setPayLifeCostLevel(PayLifeCostLevel payLifeCostLevel) { - this.payLifeCostLevel = payLifeCostLevel; + public void addPayLifeCostRestriction(PayLifeCostRestriction payLifeCostRestriction) { + this.payLifeCostRestrictions.add(payLifeCostRestriction); } @Override public boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game) { - return permanent.canBeSacrificed() && - (sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, controllerId, source, game)); - } - - @Override - public void setCanPaySacrificeCostFilter(FilterPermanent filter - ) { - this.sacrificeCostFilter = filter; - } - - @Override - public FilterPermanent getSacrificeCostFilter() { - return sacrificeCostFilter; + if (!permanent.canBeSacrificed()) { + return false; + } + String sourceIdString = source.getId().toString(); + return !(game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PAY_SACRIFICE_COST, permanent.getId(), source, controllerId, sourceIdString, 1))); } @Override @@ -4938,6 +4941,7 @@ public abstract class PlayerImpl implements Player, Serializable { List infoList = new ArrayList<>(); for (Card card : cards) { fromZone = game.getState().getZone(card.getId()); + // 712.14a. If a spell or ability puts a transforming double-faced card onto the battlefield "transformed" // or "converted," it enters the battlefield with its back face up. If a player is instructed to put a card // that isn't a transforming double-faced card onto the battlefield transformed or converted, that card stays in @@ -4946,15 +4950,29 @@ public abstract class PlayerImpl implements Player, Serializable { if (enterTransformed != null && enterTransformed && !card.isTransformable()) { continue; } + // 303.4g. If an Aura is entering the battlefield and there is no legal object or player for it to enchant, // the Aura remains in its current zone, unless that zone is the stack. In that case, the Aura is put into // its owner's graveyard instead of entering the battlefield. If the Aura is a token, it isn't created. - if (card.hasSubtype(SubType.AURA, game) - && card.getSpellAbility() != null - && !card.getSpellAbility().getTargets().isEmpty() - && !card.getSpellAbility().getTargets().get(0).copy().withNotTarget(true).canChoose(byOwner ? card.getOwnerId() : getId(), game)) { - continue; + if (card.hasSubtype(SubType.AURA, game) && !(source instanceof BestowAbility)) { + SpellAbility auraSpellAbility; + if (source instanceof SpellAbility && card.getAbilities(game).contains(source)) { + // cast aura - use source ability + auraSpellAbility = (SpellAbility) source; + } else { + // put to battlefield by another effect - use default spell + auraSpellAbility = card.getSpellAbility(); + } + if (auraSpellAbility != null) { + if (auraSpellAbility.getTargets().isEmpty()) { + throw new IllegalArgumentException("Something wrong, found etb aura with empty spell ability or without any targets: " + card + ", source: " + source); + } + if (!auraSpellAbility.getTargets().get(0).copy().withNotTarget(true).canChooseOrAlreadyChosen(byOwner ? card.getOwnerId() : getId(), source, game)) { + continue; + } + } } + ZoneChangeEvent event = new ZoneChangeEvent(card.getId(), source, byOwner ? card.getOwnerId() : getId(), fromZone, Zone.BATTLEFIELD, appliedEffects); infoList.add(new ZoneChangeInfo.Battlefield(event, faceDown, tapped, source)); diff --git a/Mage/src/main/java/mage/players/StubPlayer.java b/Mage/src/main/java/mage/players/StubPlayer.java index d39f65a1444..b5efae370e3 100644 --- a/Mage/src/main/java/mage/players/StubPlayer.java +++ b/Mage/src/main/java/mage/players/StubPlayer.java @@ -43,7 +43,7 @@ public class StubPlayer extends PlayerImpl { if (target instanceof TargetPlayer) { for (Player player : game.getPlayers().values()) { if (player.getId().equals(getId()) - && target.canTarget(getId(), game) + && target.canTarget(getId(), source, game) && !target.contains(getId())) { target.add(player.getId(), game); return true; diff --git a/Mage/src/main/java/mage/target/Target.java b/Mage/src/main/java/mage/target/Target.java index c04395b2cc5..e8c17ff9c36 100644 --- a/Mage/src/main/java/mage/target/Target.java +++ b/Mage/src/main/java/mage/target/Target.java @@ -1,11 +1,13 @@ package mage.target; +import mage.MageObject; import mage.abilities.Ability; import mage.cards.Cards; import mage.constants.Outcome; import mage.constants.Zone; import mage.filter.Filter; import mage.game.Game; +import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.Copyable; @@ -27,10 +29,24 @@ public interface Target extends Copyable, Serializable { * Warning, for "up to" targets it will return true all the time, so make sure your dialog * use do-while logic and call "choose" one time min or use isChoiceCompleted */ - @Deprecated // TODO: replace with UUID abilityControllerId, Ability source, Game game + @Deprecated + // TODO: replace with UUID abilityControllerId, Ability source, Game game boolean isChosen(Game game); - boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game); + /** + * Checking target complete and nothing to choose (X=0, all selected, all possible selected, etc) + * + * @param fromCards can be null for non cards selection + */ + boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game, Cards fromCards); + + /** + * Tests and AI related for "up to" targets (mark target that it was skipped on selection, so new choose dialog will be called) + * Example: AI sim possible target options + */ + boolean isSkipChoice(); + + void setSkipChoice(boolean isSkipChoice); void clearChosen(); @@ -51,16 +67,50 @@ public interface Target extends Copyable, Serializable { */ Target withNotTarget(boolean notTarget); - // methods for targets + /** + * Checks if there are enough targets the player can choose from among them + * or if they are autochosen since there are fewer than the minimum number. + *

+ * Implement as return canChooseFromPossibleTargets(sourceControllerId, source, game); + * TODO: remove after all canChoose replaced with default + * + * @param sourceControllerId - controller of the target event source + * @param source - can be null + * @param game + * @return - true if enough valid choices exist + */ boolean canChoose(UUID sourceControllerId, Ability source, Game game); + /** + * Make sure target can be fully selected or already selected, e.g. by AI sims + *

+ * Do not override + */ + default boolean canChooseOrAlreadyChosen(UUID sourceControllerId, Ability source, Game game) { + return this.isChosen(game) || this.canChoose(sourceControllerId, source, game); + } + + default boolean canChooseFromPossibleTargets(UUID sourceControllerId, Ability source, Game game) { + // TODO: replace all canChoose override methods by that code call + if (getMinNumberOfTargets() == 0) { + return true; + } + + int selectedCount = getSize(); + int moreSelectCount = possibleTargets(sourceControllerId, source, game).size(); + + if (selectedCount >= getMaxNumberOfTargets()) { + return false; + } + + return moreSelectCount > 0 && selectedCount + moreSelectCount >= getMinNumberOfTargets(); + } + /** * Returns a set of all possible targets that match the criteria of the implemented Target class. + * WARNING, it must filter already selected targets by keepValidPossibleTargets call at the end * - * @param sourceControllerId UUID of the ability's controller - * @param source Ability which requires the targets - * @param game Current game - * @return Set of the UUIDs of possible targets + * @param source - can be null */ Set possibleTargets(UUID sourceControllerId, Ability source, Game game); @@ -71,6 +121,42 @@ public interface Target extends Copyable, Serializable { .collect(Collectors.toSet()); } + /** + * Keep only valid and not selected targets - must be used inside any possibleTargets implementation + */ + default Set keepValidPossibleTargets(Set possibleTargets, UUID sourceControllerId, Ability source, Game game) { + // TODO: check target amount in human dialogs - is it allow to select it again + // do not override + // keep only valid and not selected targets list + return possibleTargets.stream() + .filter(this::notContains) + .filter(targetId -> { + // non-target allow any + if (source == null || source.getSourceId() == null || isNotTarget()) { + return true; + } + MageObject sourceObject = game.getObject(source); + if (sourceObject == null) { + return true; + } + + // target allow non-protected + Player targetPlayer = game.getPlayer(targetId); + if (targetPlayer != null) { + return !targetPlayer.hasLeft() + && canTarget(sourceControllerId, targetId, source, game) + && targetPlayer.canBeTargetedBy(sourceObject, sourceControllerId, source, game); + } + Permanent targetPermanent = game.getPermanent(targetId); + if (targetPermanent != null) { + return canTarget(sourceControllerId, targetId, source, game) + && targetPermanent.canBeTargetedBy(sourceObject, sourceControllerId, source, game); + } + return true; + }) + .collect(Collectors.toSet()); + } + /** * Priority method to make a choice from cards and other places, not a player.chooseXXX */ @@ -87,8 +173,6 @@ public interface Target extends Copyable, Serializable { void addTarget(UUID id, int amount, Ability source, Game game, boolean skipEvent); - boolean canTarget(UUID id, Game game); - /** * @param id * @param source WARNING, it can be null for AI or other calls from events @@ -108,11 +192,12 @@ public interface Target extends Copyable, Serializable { */ List getTargetOptions(Ability source, Game game); - boolean canChoose(UUID sourceControllerId, Game game); + default boolean canChoose(UUID sourceControllerId, Game game) { + return canChoose(sourceControllerId, null, game); + } - Set possibleTargets(UUID sourceControllerId, Game game); - - @Deprecated // TODO: need replace to source only version? + @Deprecated + // TODO: need replace to source only version? boolean choose(Outcome outcome, UUID playerId, UUID sourceId, Ability source, Game game); /** @@ -236,6 +321,11 @@ public interface Target extends Copyable, Serializable { boolean contains(UUID targetId); + default boolean notContains(UUID targetId) { + // for better usage in streams + return !contains(targetId); + } + /** * This function tries to auto-choose the next target. *

diff --git a/Mage/src/main/java/mage/target/TargetAmount.java b/Mage/src/main/java/mage/target/TargetAmount.java index d28b1852459..9fcba18dee9 100644 --- a/Mage/src/main/java/mage/target/TargetAmount.java +++ b/Mage/src/main/java/mage/target/TargetAmount.java @@ -1,16 +1,13 @@ package mage.target; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.StaticValue; -import mage.cards.Card; +import mage.cards.Cards; import mage.constants.Outcome; import mage.game.Game; -import mage.game.permanent.Permanent; import mage.players.Player; -import mage.util.DebugUtil; -import mage.util.RandomUtil; +import mage.util.CardUtil; import java.util.*; import java.util.stream.Collectors; @@ -18,7 +15,7 @@ import java.util.stream.Collectors; /** * Distribute value between targets list (damage, counters, etc) * - * @author BetaSteward_at_googlemail.com + * @author BetaSteward_at_googlemail.com, JayDi85 */ public abstract class TargetAmount extends TargetImpl { @@ -68,7 +65,7 @@ public abstract class TargetAmount extends TargetImpl { } @Override - public boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game) { + public boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game, Cards fromCards) { // make sure target request called one time minimum (for "up to" targets) // choice is selected after any addTarget call (by test, AI or human players) if (!isChoiceSelected()) { @@ -80,6 +77,11 @@ public abstract class TargetAmount extends TargetImpl { return false; } + // already selected + if (this.getSize() >= getMaxNumberOfTargets()) { + return true; + } + // TODO: need auto-choose here? See super // all other use cases are fine @@ -178,7 +180,7 @@ public abstract class TargetAmount extends TargetImpl { chosen = isChosen(game); // stop by full complete - if (isChoiceCompleted(targetController.getId(), source, game)) { + if (isChoiceCompleted(targetController.getId(), source, game, null)) { break; } @@ -202,217 +204,21 @@ public abstract class TargetAmount extends TargetImpl { Set possibleTargets = possibleTargets(source.getControllerId(), source, game); // optimizations for less memory/cpu consumptions - printTargetsTableAndVariations("before optimize", game, possibleTargets, options, false); - optimizePossibleTargets(source, game, possibleTargets); - printTargetsTableAndVariations("after optimize", game, possibleTargets, options, false); - - // calc possible amount variations - addTargets(this, possibleTargets, options, source, game); - printTargetsTableAndVariations("after calc", game, possibleTargets, options, true); - - return options; - } - - /** - * AI related, trying to reduce targets for simulations - */ - private void optimizePossibleTargets(Ability source, Game game, Set possibleTargets) { - // remove duplicated/same creatures (example: distribute 3 damage between 10+ same tokens) - + TargetOptimization.printTargetsVariationsForTargetAmount("target amount - before optimize", game, possibleTargets, options, false); // it must have additional threshold to keep more variations for analyse - // // bad example: // - Blessings of Nature // - Distribute four +1/+1 counters among any number of target creatures. // on low targets threshold AI can put 1/1 to opponent's creature instead own, see TargetAmountAITest.test_AI_SimulateTargets + int maxPossibleTargetsToSimulate = CardUtil.overflowMultiply(this.remainingAmount, 2); + TargetOptimization.optimizePossibleTargets(source, game, possibleTargets, maxPossibleTargetsToSimulate); + TargetOptimization.printTargetsVariationsForTargetAmount("target amount - after optimize", game, possibleTargets, options, false); - int maxPossibleTargetsToSimulate = this.remainingAmount * 2; - if (possibleTargets.size() < maxPossibleTargetsToSimulate) { - return; - } + // calc possible amount variations + addTargets(this, possibleTargets, options, source, game); + TargetOptimization.printTargetsVariationsForTargetAmount("target amount - after calc", game, possibleTargets, options, true); - // split targets by groups - Map targetGroups = new HashMap<>(); - possibleTargets.forEach(id -> { - String groupKey = ""; - - // player - Player player = game.getPlayer(id); - if (player != null) { - groupKey = getTargetGroupKeyAsPlayer(player); - } - - // game object - MageObject object = game.getObject(id); - if (object != null) { - groupKey = object.getName(); - if (object instanceof Permanent) { - groupKey += getTargetGroupKeyAsPermanent(game, (Permanent) object); - } else if (object instanceof Card) { - groupKey += getTargetGroupKeyAsCard(game, (Card) object); - } else { - groupKey += getTargetGroupKeyAsOther(game, object); - } - } - - // unknown - use all - if (groupKey.isEmpty()) { - groupKey = id.toString(); - } - - targetGroups.put(id, groupKey); - }); - - Map> groups = new HashMap<>(); - targetGroups.forEach((id, groupKey) -> { - groups.computeIfAbsent(groupKey, k -> new ArrayList<>()); - groups.get(groupKey).add(id); - }); - - // optimize logic: - // - use one target from each target group all the time - // - add random target from random group until fill all remainingAmount condition - - // use one target per group - Set newPossibleTargets = new HashSet<>(); - groups.forEach((groupKey, groupTargets) -> { - UUID targetId = RandomUtil.randomFromCollection(groupTargets); - if (targetId != null) { - newPossibleTargets.add(targetId); - groupTargets.remove(targetId); - } - }); - - // use random target until fill condition - while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) { - String groupKey = RandomUtil.randomFromCollection(groups.keySet()); - if (groupKey == null) { - break; - } - List groupTargets = groups.getOrDefault(groupKey, null); - if (groupTargets == null || groupTargets.isEmpty()) { - groups.remove(groupKey); - continue; - } - UUID targetId = RandomUtil.randomFromCollection(groupTargets); - if (targetId != null) { - newPossibleTargets.add(targetId); - groupTargets.remove(targetId); - } - } - - // keep final result - possibleTargets.clear(); - possibleTargets.addAll(newPossibleTargets); - } - - private String getTargetGroupKeyAsPlayer(Player player) { - // use all - return String.join(";", Arrays.asList( - player.getName(), - String.valueOf(player.getId().hashCode()) - )); - } - - private String getTargetGroupKeyAsPermanent(Game game, Permanent permanent) { - // split by name and stats - // TODO: rework and combine with PermanentEvaluator (to use battlefield score) - - // try to use short text/hash for lesser data on debug - return String.join(";", Arrays.asList( - permanent.getName(), - String.valueOf(permanent.getControllerId().hashCode()), - String.valueOf(permanent.getOwnerId().hashCode()), - String.valueOf(permanent.isTapped()), - String.valueOf(permanent.getPower().getValue()), - String.valueOf(permanent.getToughness().getValue()), - String.valueOf(permanent.getDamage()), - String.valueOf(permanent.getCardType(game).toString().hashCode()), - String.valueOf(permanent.getSubtype(game).toString().hashCode()), - String.valueOf(permanent.getCounters(game).getTotalCount()), - String.valueOf(permanent.getAbilities(game).size()), - String.valueOf(permanent.getRules(game).toString().hashCode()) - )); - } - - private String getTargetGroupKeyAsCard(Game game, Card card) { - // split by name and stats - return String.join(";", Arrays.asList( - card.getName(), - String.valueOf(card.getOwnerId().hashCode()), - String.valueOf(card.getCardType(game).toString().hashCode()), - String.valueOf(card.getSubtype(game).toString().hashCode()), - String.valueOf(card.getCounters(game).getTotalCount()), - String.valueOf(card.getAbilities(game).size()), - String.valueOf(card.getRules(game).toString().hashCode()) - )); - } - - private String getTargetGroupKeyAsOther(Game game, MageObject item) { - // use all - return String.join(";", Arrays.asList( - item.getName(), - String.valueOf(item.getId().hashCode()) - )); - } - - /** - * Debug only. Print targets table and variations. - */ - private void printTargetsTableAndVariations(String info, Game game, Set possibleTargets, List options, boolean isPrintOptions) { - if (!DebugUtil.AI_SHOW_TARGET_OPTIMIZATION_LOGS) return; - - // output example: - // - // Targets (after optimize): 5 - // 0. Balduvian Bears [ac8], C, BalduvianBears, DKM:22::0, 2/2 - // 1. PlayerA (SimulatedPlayer2) - // - // Target variations (info): 126 - // 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 1; 4 -> 1 - // 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 2 - // 0 -> 1; 1 -> 1; 2 -> 1; 4 -> 2 - - // print table - List list = new ArrayList<>(possibleTargets); - Collections.sort(list); - HashMap targetNumbers = new HashMap<>(); - System.out.println(); - System.out.println(String.format("Targets (%s): %d", info, list.size())); - for (int i = 0; i < list.size(); i++) { - targetNumbers.put(list.get(i), i); - String targetName; - Player player = game.getPlayer(list.get(i)); - if (player != null) { - targetName = player.toString(); - } else { - MageObject object = game.getObject(list.get(i)); - if (object != null) { - targetName = object.toString(); - } else { - targetName = "unknown"; - } - } - System.out.println(String.format("%d. %s", i, targetName)); - } - System.out.println(); - - if (!isPrintOptions) { - return; - } - - // print amount variations - List res = options - .stream() - .map(t -> t.getTargets() - .stream() - .map(id -> targetNumbers.get(id) + " -> " + t.getTargetAmount(id)) - .sorted() - .collect(Collectors.joining("; "))).sorted().collect(Collectors.toList()); - System.out.println(); - System.out.println(String.format("Target variations (info): %d", options.size())); - System.out.println(String.join("\n", res)); - System.out.println(); + return options; } final protected void addTargets(TargetAmount target, Set possibleTargets, List options, Ability source, Game game) { diff --git a/Mage/src/main/java/mage/target/TargetCard.java b/Mage/src/main/java/mage/target/TargetCard.java index 3abc11591dc..af7b05fc114 100644 --- a/Mage/src/main/java/mage/target/TargetCard.java +++ b/Mage/src/main/java/mage/target/TargetCard.java @@ -1,6 +1,5 @@ package mage.target; -import mage.MageItem; import mage.abilities.Ability; import mage.cards.Card; import mage.cards.Cards; @@ -56,196 +55,9 @@ public class TargetCard extends TargetObject { return this; } - /** - * Checks if there are enough {@link Card} that can be selected. - * - * @param sourceControllerId - controller of the select event - * @param game - * @return - true if enough valid {@link Card} exist - */ - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - return canChoose(sourceControllerId, null, game); - } - - /** - * Checks if there are enough {@link Card cards} in the appropriate zone that the player can choose from among them - * or if they are autochosen since there are fewer than the minimum number. - * - * @param sourceControllerId - controller of the target event source - * @param source - * @param game - * @return - true if enough valid {@link Card} exist - */ @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int possibleTargets = 0; - if (getMinNumberOfTargets() == 0) { // if 0 target is valid, the canChoose is always true - return true; - } - for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - Player player = game.getPlayer(playerId); - if (player != null) { - if (this.minNumberOfTargets == 0) { - return true; - } - switch (zone) { - case HAND: - possibleTargets += countPossibleTargetInHand(game, player, sourceControllerId, source, - filter, isNotTarget(), this.minNumberOfTargets - possibleTargets); - break; - case GRAVEYARD: - possibleTargets += countPossibleTargetInGraveyard(game, player, sourceControllerId, source, - filter, isNotTarget(), this.minNumberOfTargets - possibleTargets); - break; - case LIBRARY: - possibleTargets += countPossibleTargetInLibrary(game, player, sourceControllerId, source, - filter, isNotTarget(), this.minNumberOfTargets - possibleTargets); - break; - case EXILED: - possibleTargets += countPossibleTargetInExile(game, player, sourceControllerId, source, - filter, isNotTarget(), this.minNumberOfTargets - possibleTargets); - break; - case COMMAND: - possibleTargets += countPossibleTargetInCommandZone(game, player, sourceControllerId, source, - filter, isNotTarget(), this.minNumberOfTargets - possibleTargets); - break; - case ALL: - possibleTargets += countPossibleTargetInAnyZone(game, player, sourceControllerId, source, - filter, isNotTarget(), this.minNumberOfTargets - possibleTargets); - break; - default: - throw new IllegalArgumentException("Unsupported TargetCard zone: " + zone); - } - if (possibleTargets >= this.minNumberOfTargets) { - return true; - } - } - } - return false; - } - - /** - * count up to N possible target cards in a player's hand matching the filter - */ - protected static int countPossibleTargetInHand(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget, int countUpTo) { - UUID sourceId = source != null ? source.getSourceId() : null; - int possibleTargets = 0; - for (Card card : player.getHand().getCards(filter, sourceControllerId, source, game)) { - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - possibleTargets++; - if (possibleTargets >= countUpTo) { - return possibleTargets; // early return for faster computation. - } - } - } - return possibleTargets; - } - - /** - * count up to N possible target cards in a player's graveyard matching the filter - */ - protected static int countPossibleTargetInGraveyard(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget, int countUpTo) { - UUID sourceId = source != null ? source.getSourceId() : null; - int possibleTargets = 0; - for (Card card : player.getGraveyard().getCards(filter, sourceControllerId, source, game)) { - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - possibleTargets++; - if (possibleTargets >= countUpTo) { - return possibleTargets; // early return for faster computation. - } - } - } - return possibleTargets; - } - - /** - * count up to N possible target cards in a player's library matching the filter - */ - protected static int countPossibleTargetInLibrary(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget, int countUpTo) { - UUID sourceId = source != null ? source.getSourceId() : null; - int possibleTargets = 0; - for (Card card : player.getLibrary().getUniqueCards(game)) { - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - if (filter.match(card, game)) { - possibleTargets++; - if (possibleTargets >= countUpTo) { - return possibleTargets; // early return for faster computation. - } - } - } - } - return possibleTargets; - } - - /** - * count up to N possible target cards in a player's exile matching the filter - */ - protected static int countPossibleTargetInExile(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget, int countUpTo) { - UUID sourceId = source != null ? source.getSourceId() : null; - int possibleTargets = 0; - for (Card card : game.getExile().getPermanentExile().getCards(game)) { - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - if (filter.match(card, player.getId(), source, game)) { - possibleTargets++; - if (possibleTargets >= countUpTo) { - return possibleTargets; // early return for faster computation. - } - } - } - } - return possibleTargets; - } - - /** - * count up to N possible target cards in a player's command zone matching the filter - */ - protected static int countPossibleTargetInCommandZone(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget, int countUpTo) { - UUID sourceId = source != null ? source.getSourceId() : null; - int possibleTargets = 0; - List possibleCards = game.getCommandersIds(player, CommanderCardType.ANY, false).stream() - .map(game::getCard) - .filter(Objects::nonNull) - .filter(card -> game.getState().getZone(card.getId()).equals(Zone.COMMAND)) - .filter(card -> filter.match(card, sourceControllerId, source, game)) - .collect(Collectors.toList()); - for (Card card : possibleCards) { - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - possibleTargets++; - if (possibleTargets >= countUpTo) { - return possibleTargets; // early return for faster computation. - } - } - } - return possibleTargets; - } - - /** - * count up to N possible target cards in ANY zone - */ - protected static int countPossibleTargetInAnyZone(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget, int countUpTo) { - UUID sourceId = source != null ? source.getSourceId() : null; - int possibleTargets = 0; - for (Card card : game.getCards()) { - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - if (filter.match(card, game)) { - possibleTargets++; - if (possibleTargets >= countUpTo) { - return possibleTargets; // early return for faster computation. - } - } - } - } - return possibleTargets; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - return possibleTargets(sourceControllerId, null, game); - } - - public Set possibleTargets(UUID sourceControllerId, Cards cards, Ability source, Game game) { - return cards.getCards(filter, sourceControllerId, source, game).stream().map(MageItem::getId).collect(Collectors.toSet()); + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override @@ -278,7 +90,7 @@ public class TargetCard extends TargetObject { } } } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } /** @@ -286,12 +98,8 @@ public class TargetCard extends TargetObject { */ protected static Set getAllPossibleTargetInHand(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget) { Set possibleTargets = new HashSet<>(); - UUID sourceId = source != null ? source.getSourceId() : null; for (Card card : player.getHand().getCards(filter, sourceControllerId, source, game)) { - // TODO: Why for sourceId == null? - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - possibleTargets.add(card.getId()); - } + possibleTargets.add(card.getId()); } return possibleTargets; } @@ -301,11 +109,8 @@ public class TargetCard extends TargetObject { */ protected static Set getAllPossibleTargetInGraveyard(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget) { Set possibleTargets = new HashSet<>(); - UUID sourceId = source != null ? source.getSourceId() : null; for (Card card : player.getGraveyard().getCards(filter, sourceControllerId, source, game)) { - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - possibleTargets.add(card.getId()); - } + possibleTargets.add(card.getId()); } return possibleTargets; } @@ -315,12 +120,9 @@ public class TargetCard extends TargetObject { */ protected static Set getAllPossibleTargetInLibrary(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget) { Set possibleTargets = new HashSet<>(); - UUID sourceId = source != null ? source.getSourceId() : null; for (Card card : player.getLibrary().getUniqueCards(game)) { - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - if (filter.match(card, sourceControllerId, source, game)) { - possibleTargets.add(card.getId()); - } + if (filter.match(card, sourceControllerId, source, game)) { + possibleTargets.add(card.getId()); } } return possibleTargets; @@ -332,11 +134,9 @@ public class TargetCard extends TargetObject { protected static Set getAllPossibleTargetInExile(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget) { Set possibleTargets = new HashSet<>(); UUID sourceId = source != null ? source.getSourceId() : null; - for (Card card : game.getExile().getPermanentExile().getCards(game)) { - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - if (filter.match(card, sourceControllerId, source, game)) { - possibleTargets.add(card.getId()); - } + for (Card card : game.getExile().getAllCardsByRange(game, sourceControllerId)) { + if (filter.match(card, sourceControllerId, source, game)) { + possibleTargets.add(card.getId()); } } return possibleTargets; @@ -367,21 +167,16 @@ public class TargetCard extends TargetObject { */ protected static Set getAllPossibleTargetInAnyZone(Game game, Player player, UUID sourceControllerId, Ability source, FilterCard filter, boolean isNotTarget) { Set possibleTargets = new HashSet<>(); - UUID sourceId = source != null ? source.getSourceId() : null; for (Card card : game.getCards()) { - if (sourceId == null || isNotTarget || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - if (filter.match(card, sourceControllerId, source, game)) { - possibleTargets.add(card.getId()); - } + if (filter.match(card, sourceControllerId, source, game)) { + possibleTargets.add(card.getId()); } } return possibleTargets; } - // TODO: check all class targets, if it override canTarget then make sure it override ALL 3 METHODS with canTarget and possibleTargets (method with cards doesn't need) - @Override - public boolean canTarget(UUID id, Game game) { + public boolean canTarget(UUID id, Ability source, Game game) { // copy-pasted from super but with card instead object Card card = game.getCard(id); return card != null @@ -389,11 +184,6 @@ public class TargetCard extends TargetObject { && getFilter() != null && getFilter().match(card, game); } - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - return canTarget(id, game); - } - @Override public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { Card card = game.getCard(id); diff --git a/Mage/src/main/java/mage/target/TargetImpl.java b/Mage/src/main/java/mage/target/TargetImpl.java index 48f6f4bf73f..e0a00773cc2 100644 --- a/Mage/src/main/java/mage/target/TargetImpl.java +++ b/Mage/src/main/java/mage/target/TargetImpl.java @@ -3,6 +3,7 @@ package mage.target; import mage.MageObject; import mage.abilities.Ability; import mage.cards.Card; +import mage.cards.Cards; import mage.constants.AbilityType; import mage.constants.Outcome; import mage.constants.Zone; @@ -18,7 +19,7 @@ import mage.util.RandomUtil; import java.util.*; /** - * @author BetaSteward_at_googlemail.com + * @author BetaSteward_at_googlemail.com, JayDi85 */ public abstract class TargetImpl implements Target { @@ -41,6 +42,8 @@ public abstract class TargetImpl implements Target { */ protected boolean chosen = false; + protected boolean isSkipChoice = false; + // is the target handled as targeted spell/ability (notTarget = true is used for not targeted effects like e.g. sacrifice) protected boolean notTarget = false; protected boolean atRandom = false; // for inner choose logic @@ -70,6 +73,7 @@ public abstract class TargetImpl implements Target { this.required = target.required; this.requiredExplicitlySet = target.requiredExplicitlySet; this.chosen = target.chosen; + this.isSkipChoice = target.isSkipChoice; this.targets.putAll(target.targets); this.zoneChangeCounters.putAll(target.zoneChangeCounters); this.atRandom = target.atRandom; @@ -163,6 +167,16 @@ public abstract class TargetImpl implements Target { return min == 0 && max == Integer.MAX_VALUE; } + @Override + public boolean isSkipChoice() { + return this.isSkipChoice; + } + + @Override + public void setSkipChoice(boolean isSkipChoice) { + this.isSkipChoice = isSkipChoice; + } + @Override public String getMessage(Game game) { // UI choose message @@ -261,7 +275,7 @@ public abstract class TargetImpl implements Target { } @Override - public boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game) { + public boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game, Cards fromCards) { // make sure target request called one time minimum (for "up to" targets) // choice is selected after any addTarget call (by test, AI or human players) if (!isChoiceSelected()) { @@ -273,22 +287,23 @@ public abstract class TargetImpl implements Target { return false; } + // already selected + if (this.getSize() >= getMaxNumberOfTargets()) { + return true; + } + // make sure to auto-finish on all targets selection // - human player can select and deselect targets until fill all targets amount or press done button // - AI player can select all new targets as much as possible if (getMaxNumberOfTargets() > 0) { - if (getMaxNumberOfTargets() == Integer.MAX_VALUE) { - if (abilityControllerId != null && source != null) { - // any amount - nothing to choose - return this.getSize() >= this.possibleTargets(abilityControllerId, source, game).size(); - } else { - // any amount - any selected - return this.getSize() > 0; - } - } else { - // check selected limit - return this.getSize() >= getMaxNumberOfTargets(); + // full selection + if (this.getSize() >= getMaxNumberOfTargets()) { + return true; } + + // partly selection + int moreSelectCount = this.possibleTargets(abilityControllerId, source, game, fromCards).size(); + return moreSelectCount == 0 || isSkipChoice(); } // all other use cases are fine @@ -300,12 +315,13 @@ public abstract class TargetImpl implements Target { targets.clear(); zoneChangeCounters.clear(); chosen = false; + isSkipChoice = false; } @Override public boolean isChoiceSelected() { // min = max = 0 - for abilities with X=0, e.g. nothing to choose - return chosen || getMaxNumberOfTargets() == 0 && getMinNumberOfTargets() == 0; + return chosen || getMaxNumberOfTargets() == 0 && getMinNumberOfTargets() == 0 || isSkipChoice(); } @Override @@ -423,7 +439,7 @@ public abstract class TargetImpl implements Target { chosen = isChosen(game); // stop by full complete - if (isChoiceCompleted(abilityControllerId, source, game)) { + if (isChoiceCompleted(abilityControllerId, source, game, null)) { break; } @@ -503,7 +519,7 @@ public abstract class TargetImpl implements Target { chosen = isChosen(game); // stop by full complete - if (isChoiceCompleted(abilityControllerId, source, game)) { + if (isChoiceCompleted(abilityControllerId, source, game, null)) { break; } @@ -570,13 +586,24 @@ public abstract class TargetImpl implements Target { @Override public List getTargetOptions(Ability source, Game game) { List options = new ArrayList<>(); - List possibleTargets = new ArrayList<>(); - possibleTargets.addAll(possibleTargets(source.getControllerId(), source, game)); - possibleTargets.removeAll(getTargets()); + Set possibleTargets = possibleTargets(source.getControllerId(), source, game); + + // optimizations for less memory/cpu consumptions + int maxPossibleTargetsToSimulate = Math.min(TargetOptimization.AI_MAX_POSSIBLE_TARGETS_TO_CHOOSE, possibleTargets.size()); // see TargetAmount + if (getMinNumberOfTargets() > 0) { + maxPossibleTargetsToSimulate = Math.max(maxPossibleTargetsToSimulate, getMinNumberOfTargets()); + } + TargetOptimization.printTargetsVariationsForTarget("target - before optimize", game, possibleTargets, options, false); + TargetOptimization.optimizePossibleTargets(source, game, possibleTargets, maxPossibleTargetsToSimulate); + TargetOptimization.printTargetsVariationsForTarget("target - after optimize", game, possibleTargets, options, false); + + // calc all optimized combinations + // TODO: replace by google/apache lib to generate all combinations + List needPossibleTargets = new ArrayList<>(possibleTargets); // get the length of the array // e.g. for {'A','B','C','D'} => N = 4 - int N = possibleTargets.size(); + int N = needPossibleTargets.size(); // not enough targets, return no option if (N < getMinNumberOfTargets()) { return options; @@ -584,6 +611,7 @@ public abstract class TargetImpl implements Target { // not target but that's allowed, return one empty option if (N == 0) { TargetImpl target = this.copy(); + target.setSkipChoice(true); options.add(target); return options; } @@ -603,6 +631,7 @@ public abstract class TargetImpl implements Target { int minK = getMinNumberOfTargets(); if (getMinNumberOfTargets() == 0) { // add option without targets if possible TargetImpl target = this.copy(); + target.setSkipChoice(true); options.add(target); minK = 1; } @@ -631,7 +660,7 @@ public abstract class TargetImpl implements Target { //add the new target option TargetImpl target = this.copy(); for (int i = 0; i < combination.length; i++) { - target.addTarget(possibleTargets.get(combination[i]), source, game, true); + target.addTarget(needPossibleTargets.get(combination[i]), source, game, true); } options.add(target); index++; @@ -650,6 +679,9 @@ public abstract class TargetImpl implements Target { } } } + + TargetOptimization.printTargetsVariationsForTarget("target - after calc", game, possibleTargets, options, true); + return options; } diff --git a/Mage/src/main/java/mage/target/TargetObject.java b/Mage/src/main/java/mage/target/TargetObject.java index 1cce9447696..46918cfd479 100644 --- a/Mage/src/main/java/mage/target/TargetObject.java +++ b/Mage/src/main/java/mage/target/TargetObject.java @@ -50,27 +50,17 @@ public abstract class TargetObject extends TargetImpl { /** * Warning, don't use with non card objects here like commanders/emblems/etc. If you want it then * override canTarget in your own target. - * - * @param id - * @param game - * @return */ @Override - public boolean canTarget(UUID id, Game game) { + public boolean canTarget(UUID id, Ability source, Game game) { MageObject object = game.getObject(id); return object != null && zone != null && zone.match(game.getState().getZone(id)) && getFilter() != null && getFilter().match(object, game); } - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - return canTarget(id, game); - } - @Override public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { return canTarget(id, source, game); } - } diff --git a/Mage/src/main/java/mage/target/TargetOptimization.java b/Mage/src/main/java/mage/target/TargetOptimization.java new file mode 100644 index 00000000000..879b32e5f57 --- /dev/null +++ b/Mage/src/main/java/mage/target/TargetOptimization.java @@ -0,0 +1,237 @@ +package mage.target; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.cards.Card; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.util.DebugUtil; +import mage.util.RandomUtil; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Helper class to optimize possible targets list for AI and playable calcs + *

+ * Features: + * - less possible targets, less combinations, less CPU/memory usage for sims + * - group all possible targets by same characteristics; + * - fill target one by one from each group + * + * @author JayDi85 + */ +public class TargetOptimization { + + // for up to or any amount - limit max game sims to analyse + // (it's useless to calc all possible combinations on too much targets) + static public int AI_MAX_POSSIBLE_TARGETS_TO_CHOOSE = 7; + + public static void optimizePossibleTargets(Ability source, Game game, Set possibleTargets, int maxPossibleTargetsToSimulate) { + // remove duplicated/same creatures + // example: distribute 3 damage between 10+ same tokens + // example: target x1 from x10 forests - it's useless to recalc each forest + + if (possibleTargets.size() < maxPossibleTargetsToSimulate) { + return; + } + + // split targets by groups + Map targetGroups = new HashMap<>(); + possibleTargets.forEach(id -> { + String groupKey = ""; + + // player + Player player = game.getPlayer(id); + if (player != null) { + groupKey = getTargetGroupKeyAsPlayer(player); + } + + // game object + MageObject object = game.getObject(id); + if (object != null) { + groupKey = object.getName(); + if (object instanceof Permanent) { + groupKey += getTargetGroupKeyAsPermanent(game, (Permanent) object); + } else if (object instanceof Card) { + groupKey += getTargetGroupKeyAsCard(game, (Card) object); + } else { + groupKey += getTargetGroupKeyAsOther(game, object); + } + } + + // unknown - use all + if (groupKey.isEmpty()) { + groupKey = id.toString(); + } + + targetGroups.put(id, groupKey); + }); + + Map> groups = new HashMap<>(); + targetGroups.forEach((id, groupKey) -> { + groups.computeIfAbsent(groupKey, k -> new ArrayList<>()); + groups.get(groupKey).add(id); + }); + + // optimize logic: + // - use one target from each target group all the time + // - add random target from random group until fill all remainingAmount condition + + // use one target per group + Set newPossibleTargets = new HashSet<>(); + groups.forEach((groupKey, groupTargets) -> { + UUID targetId = RandomUtil.randomFromCollection(groupTargets); + if (targetId != null) { + newPossibleTargets.add(targetId); + groupTargets.remove(targetId); + } + }); + + // use random target until fill condition + while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) { + String groupKey = RandomUtil.randomFromCollection(groups.keySet()); + if (groupKey == null) { + break; + } + List groupTargets = groups.getOrDefault(groupKey, null); + if (groupTargets == null || groupTargets.isEmpty()) { + groups.remove(groupKey); + continue; + } + UUID targetId = RandomUtil.randomFromCollection(groupTargets); + if (targetId != null) { + newPossibleTargets.add(targetId); + groupTargets.remove(targetId); + } + } + + // keep final result + possibleTargets.clear(); + possibleTargets.addAll(newPossibleTargets); + } + + private static String getTargetGroupKeyAsPlayer(Player player) { + // use all + return String.join(";", Arrays.asList( + player.getName(), + String.valueOf(player.getId().hashCode()) + )); + } + + private static String getTargetGroupKeyAsPermanent(Game game, Permanent permanent) { + // split by name and stats + // TODO: rework and combine with PermanentEvaluator (to use battlefield score) + + // try to use short text/hash for lesser data on debug + return String.join(";", Arrays.asList( + permanent.getName(), + String.valueOf(permanent.getControllerId().hashCode()), + String.valueOf(permanent.getOwnerId().hashCode()), + String.valueOf(permanent.isTapped()), + String.valueOf(permanent.getPower().getValue()), + String.valueOf(permanent.getToughness().getValue()), + String.valueOf(permanent.getDamage()), + String.valueOf(permanent.getCardType(game).toString().hashCode()), + String.valueOf(permanent.getSubtype(game).toString().hashCode()), + String.valueOf(permanent.getCounters(game).getTotalCount()), + String.valueOf(permanent.getAbilities(game).size()), + String.valueOf(permanent.getRules(game).toString().hashCode()) + )); + } + + private static String getTargetGroupKeyAsCard(Game game, Card card) { + // split by name and stats + return String.join(";", Arrays.asList( + card.getName(), + String.valueOf(card.getOwnerId().hashCode()), + String.valueOf(card.getCardType(game).toString().hashCode()), + String.valueOf(card.getSubtype(game).toString().hashCode()), + String.valueOf(card.getCounters(game).getTotalCount()), + String.valueOf(card.getAbilities(game).size()), + String.valueOf(card.getRules(game).toString().hashCode()) + )); + } + + private static String getTargetGroupKeyAsOther(Game game, MageObject item) { + // use all + return String.join(";", Arrays.asList( + item.getName(), + String.valueOf(item.getId().hashCode()) + )); + } + + public static void printTargetsVariationsForTarget(String info, Game game, Set possibleTargets, List options, boolean isPrintOptions) { + List usedOptions = options.stream() + .filter(Objects::nonNull) + .map(TargetImpl.class::cast) + .collect(Collectors.toList()); + printTargetsTableAndVariationsInner(info, game, possibleTargets, usedOptions, isPrintOptions); + } + + public static void printTargetsVariationsForTargetAmount(String info, Game game, Set possibleTargets, List options, boolean isPrintOptions) { + List usedOptions = options.stream() + .filter(Objects::nonNull) + .map(TargetImpl.class::cast) + .collect(Collectors.toList()); + printTargetsTableAndVariationsInner(info, game, possibleTargets, usedOptions, isPrintOptions); + } + + private static void printTargetsTableAndVariationsInner(String info, Game game, Set possibleTargets, List options, boolean isPrintOptions) { + if (!DebugUtil.AI_SHOW_TARGET_OPTIMIZATION_LOGS) return; + + // output example: + // + // Targets (after optimize): 5 + // 0. Balduvian Bears [ac8], C, BalduvianBears, DKM:22::0, 2/2 + // 1. PlayerA (SimulatedPlayer2) + // + // Target variations (info): 126 + // 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 1; 4 -> 1 + // 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 2 + // 0 -> 1; 1 -> 1; 2 -> 1; 4 -> 2 + + // print table + List list = new ArrayList<>(possibleTargets); + Collections.sort(list); + HashMap targetNumbers = new HashMap<>(); + System.out.println(); + System.out.println(String.format("Targets (%s): %d", info, list.size())); + for (int i = 0; i < list.size(); i++) { + targetNumbers.put(list.get(i), i); + String targetName; + Player player = game.getPlayer(list.get(i)); + if (player != null) { + targetName = player.toString(); + } else { + MageObject object = game.getObject(list.get(i)); + if (object != null) { + targetName = object.toString(); + } else { + targetName = "unknown"; + } + } + System.out.println(String.format("%d. %s", i, targetName)); + } + System.out.println(); + + if (!isPrintOptions) { + return; + } + + // print amount variations + List res = options + .stream() + .map(t -> t.getTargets() + .stream() + .map(id -> targetNumbers.get(id) + (t instanceof TargetAmount ? " -> " + t.getTargetAmount(id) : "")) + .sorted() + .collect(Collectors.joining("; "))).sorted().collect(Collectors.toList()); + System.out.println(); + System.out.println(String.format("Target variations (info): %d", options.size())); + System.out.println(String.join("\n", res)); + System.out.println(); + } + +} diff --git a/Mage/src/main/java/mage/target/TargetPermanent.java b/Mage/src/main/java/mage/target/TargetPermanent.java index c97ea339e01..16cf4a9c1fb 100644 --- a/Mage/src/main/java/mage/target/TargetPermanent.java +++ b/Mage/src/main/java/mage/target/TargetPermanent.java @@ -51,11 +51,11 @@ public class TargetPermanent extends TargetObject { @Override public boolean canTarget(UUID id, Ability source, Game game) { - return canTarget(source.getControllerId(), id, source, game); + return canTarget(source == null ? null : source.getControllerId(), id, source, game); } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { Permanent permanent = game.getPermanent(id); if (permanent == null) { return false; @@ -67,19 +67,14 @@ public class TargetPermanent extends TargetObject { // first for protection from spells or abilities (e.g. protection from colored spells, r1753) // second for protection from sources (e.g. protection from artifacts + equip ability) if (!isNotTarget()) { - if (!permanent.canBeTargetedBy(game.getObject(source.getId()), controllerId, source, game) - || !permanent.canBeTargetedBy(game.getObject(source), controllerId, source, game)) { + if (!permanent.canBeTargetedBy(game.getObject(source.getId()), playerId, source, game) + || !permanent.canBeTargetedBy(game.getObject(source), playerId, source, game)) { return false; } } } - return filter.match(permanent, controllerId, source, game); - } - - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game, boolean flag) { - Permanent permanent = game.getPermanent(id); - return filter.match(permanent, controllerId, source, game); + return filter.match(permanent, playerId, source, game); } @Override @@ -87,91 +82,19 @@ public class TargetPermanent extends TargetObject { return this.filter; } - /** - * Checks if there are enough {@link Permanent} that can be chosen. - *

- * Takes into account notTarget parameter, in case it's true doesn't check - * for protection, shroud etc. - * - * @param sourceControllerId controller of the target event source - * @param source - * @param game - * @return true if enough valid {@link Permanent} exist - */ @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int remainingTargets = this.minNumberOfTargets - targets.size(); - if (remainingTargets <= 0) { - return true; - } - int count = 0; - MageObject targetSource = game.getObject(source); - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId())) { - if (notTarget || permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - count++; - if (count >= remainingTargets) { - return true; - } - } - } - } - return false; - } - - /** - * Checks if there are enough {@link Permanent} that can be selected. Should - * not be used for Ability targets since this does not check for protection, - * shroud etc. - * - * @param sourceControllerId - controller of the select event - * @param game - * @return - true if enough valid {@link Permanent} exist - */ - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - int remainingTargets = this.minNumberOfTargets - targets.size(); - if (remainingTargets == 0) { - // if we return true, then AnowonTheRuinSage will hang for AI when no targets in play - // TODO: retest Anowon the Ruin Sage - return true; - } - int count = 0; - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, game)) { - if (!targets.containsKey(permanent.getId())) { - count++; - if (count >= remainingTargets) { - return true; - } - } - } - return false; + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { // TODO: check if possible targets works with setTargetController from some cards like Nicol Bolas, Dragon-God Set possibleTargets = new HashSet<>(); - MageObject targetSource = game.getObject(source); for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game)) { - if (!targets.containsKey(permanent.getId())) { - if (notTarget || permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - possibleTargets.add(permanent.getId()); - } - } + possibleTargets.add(permanent.getId()); } - return possibleTargets; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - Set possibleTargets = new HashSet<>(); - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, sourceControllerId, game)) { - if (!targets.containsKey(permanent.getId())) { - possibleTargets.add(permanent.getId()); - } - } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/TargetPlayer.java b/Mage/src/main/java/mage/target/TargetPlayer.java index 9c1c13ad722..ef0696597b7 100644 --- a/Mage/src/main/java/mage/target/TargetPlayer.java +++ b/Mage/src/main/java/mage/target/TargetPlayer.java @@ -51,82 +51,21 @@ public class TargetPlayer extends TargetImpl { return filter; } - /** - * Checks if there are enough {@link Player} that can be chosen. Should only - * be used for Ability targets since this checks for protection, shroud etc. - * - * @param sourceControllerId - controller of the target event source - * @param source - * @param game - * @return - true if enough valid {@link Player} exist - */ @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int count = 0; - MageObject targetSource = game.getObject(source); - for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - Player player = game.getPlayer(playerId); - if (player != null && !player.hasLeft() && filter.match(player, sourceControllerId, source, game)) { - if (player.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - } - return false; - } - - /** - * Checks if there are enough {@link Player} that can be selected. Should - * not be used for Ability targets since this does not check for protection, - * shroud etc. - * - * @param sourceControllerId - controller of the select event - * @param game - * @return - true if enough valid {@link Player} exist - */ - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - int count = 0; - for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - Player player = game.getPlayer(playerId); - if (player != null && !player.hasLeft() && filter.match(player, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - return false; + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); - MageObject targetSource = game.getObject(source); for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { Player player = game.getPlayer(playerId); - if (player != null && !player.hasLeft() && filter.match(player, sourceControllerId, source, game)) { - if (isNotTarget() || player.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - possibleTargets.add(playerId); - } - } - } - return possibleTargets; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - Set possibleTargets = new HashSet<>(); - for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - Player player = game.getPlayer(playerId); - if (player != null && !player.hasLeft() && filter.match(player, game)) { + if (player != null && filter.match(player, sourceControllerId, source, game)) { possibleTargets.add(playerId); } } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override @@ -138,12 +77,6 @@ public class TargetPlayer extends TargetImpl { return targets.keySet().stream().anyMatch(playerId -> canTarget(playerId, source, game)); } - @Override - public boolean canTarget(UUID id, Game game) { - Player player = game.getPlayer(id); - return filter.match(player, game); - } - @Override public boolean canTarget(UUID id, Ability source, Game game) { Player player = game.getPlayer(id); diff --git a/Mage/src/main/java/mage/target/TargetSource.java b/Mage/src/main/java/mage/target/TargetSource.java index 171e6b88236..5ffbc7639f2 100644 --- a/Mage/src/main/java/mage/target/TargetSource.java +++ b/Mage/src/main/java/mage/target/TargetSource.java @@ -89,63 +89,9 @@ public class TargetSource extends TargetObject { return true; } - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - return canChoose(sourceControllerId, (Ability) null, game); - } - @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int count = 0; - for (StackObject stackObject : game.getStack()) { - if (game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) - && filter.match(stackObject, sourceControllerId, source, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - for (Permanent permanent : game.getBattlefield().getActivePermanents(sourceControllerId, game)) { - if (filter.match(permanent, sourceControllerId, source, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - for (Player player : game.getPlayers().values()) { - for (Card card : player.getGraveyard().getCards(game)) { - if (filter.match(card, sourceControllerId, source, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - } - for (Card card : game.getExile().getAllCards(game)) { - if (filter.match(card, sourceControllerId, source, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - for (CommandObject commandObject : game.getState().getCommand()) { - if (filter.match(commandObject, sourceControllerId, source, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - return false; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - return possibleTargets(sourceControllerId, (Ability) null, game); + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override @@ -179,7 +125,8 @@ public class TargetSource extends TargetObject { possibleTargets.add(commandObject.getId()); } } - return possibleTargets; + + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/TargetSpell.java b/Mage/src/main/java/mage/target/TargetSpell.java index 4ded8d97a8d..45d11a6a6c9 100644 --- a/Mage/src/main/java/mage/target/TargetSpell.java +++ b/Mage/src/main/java/mage/target/TargetSpell.java @@ -64,41 +64,16 @@ public class TargetSpell extends TargetObject { @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - if (this.minNumberOfTargets == 0) { - return true; - } - int count = 0; - for (StackObject stackObject : game.getStack()) { - // rule 114.4. A spell or ability on the stack is an illegal target for itself. - if (source.getSourceId() != null && source.getSourceId().equals(stackObject.getSourceId())) { - continue; - } - if (canBeChosen(stackObject, sourceControllerId, source, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - return false; - } - - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - return canChoose(sourceControllerId, null, game); + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - return game.getStack().stream() + Set possibleTargets = game.getStack().stream() .filter(stackObject -> canBeChosen(stackObject, sourceControllerId, source, game)) .map(StackObject::getId) .collect(Collectors.toSet()); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - return this.possibleTargets(sourceControllerId, null, game); + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override @@ -107,7 +82,11 @@ public class TargetSpell extends TargetObject { } private boolean canBeChosen(StackObject stackObject, UUID sourceControllerId, Ability source, Game game) { + // rule 114.4. A spell or ability on the stack is an illegal target for itself. + boolean isSelfTarget = source.getSourceId() != null && source.getSourceId().equals(stackObject.getSourceId()); + return stackObject instanceof Spell + && !isSelfTarget && game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) && canTarget(sourceControllerId, stackObject.getId(), source, game); } diff --git a/Mage/src/main/java/mage/target/TargetStackObject.java b/Mage/src/main/java/mage/target/TargetStackObject.java index 2b7840436c2..42df9dc7ec3 100644 --- a/Mage/src/main/java/mage/target/TargetStackObject.java +++ b/Mage/src/main/java/mage/target/TargetStackObject.java @@ -56,22 +56,7 @@ public class TargetStackObject extends TargetObject { @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int count = 0; - for (StackObject stackObject : game.getStack()) { - if (game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) - && filter.match(stackObject, sourceControllerId, source, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - return false; - } - - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - return canChoose(sourceControllerId, null, game); + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override @@ -83,12 +68,7 @@ public class TargetStackObject extends TargetObject { possibleTargets.add(stackObject.getId()); } } - return possibleTargets; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - return this.possibleTargets(sourceControllerId, null, game); + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/Targets.java b/Mage/src/main/java/mage/target/Targets.java index cba1f867cef..10e6cba05ab 100644 --- a/Mage/src/main/java/mage/target/Targets.java +++ b/Mage/src/main/java/mage/target/Targets.java @@ -2,6 +2,7 @@ package mage.target; import mage.MageObject; import mage.abilities.Ability; +import mage.cards.Cards; import mage.constants.Outcome; import mage.game.Game; import mage.game.events.GameEvent; @@ -63,8 +64,8 @@ public class Targets extends ArrayList implements Copyable { return unchosenIndex < res.size() ? res.get(unchosenIndex) : null; } - public boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game) { - return stream().allMatch(t -> t.isChoiceCompleted(abilityControllerId, source, game)); + public boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game, Cards fromCards) { + return stream().allMatch(t -> t.isChoiceCompleted(abilityControllerId, source, game, fromCards)); } public void clearChosen() { @@ -78,99 +79,64 @@ public class Targets extends ArrayList implements Copyable { } public boolean choose(Outcome outcome, UUID playerId, UUID sourceId, Ability source, Game game) { - Player player = game.getPlayer(playerId); - if (player == null) { - return false; - } - - // in test mode some targets can be predefined already, e.g. by cast/activate command - // so do not clear chosen status here - - if (this.size() > 0) { - do { - // stop on disconnect or nothing to choose - if (!player.canRespond() || !canChoose(playerId, source, game)) { - break; - } - - // stop on complete - Target target = this.getNextUnchosen(game); - if (target == null) { - break; - } - - // stop on cancel/done - if (!target.choose(outcome, playerId, sourceId, source, game)) { - break; - } - - // target done, can take next one - } while (true); - } - - if (DebugUtil.GAME_SHOW_CHOOSE_TARGET_LOGS && !game.isSimulation()) { - printDebugTargets("choose finish", this, source, game); - } - - return isChosen(game); + return makeChoice(false, outcome, playerId, source, false, game, false); } public boolean chooseTargets(Outcome outcome, UUID playerId, Ability source, boolean noMana, Game game, boolean canCancel) { - Player player = game.getPlayer(playerId); - if (player == null) { - return false; - } + return makeChoice(true, outcome, playerId, source, noMana, game, canCancel); + } + private boolean makeChoice(boolean isTargetChoice, Outcome outcome, UUID playerId, Ability source, boolean noMana, Game game, boolean canCancel) { // in test mode some targets can be predefined already, e.g. by cast/activate command // so do not clear chosen status here - if (this.size() > 0) { - do { - // stop on disconnect or nothing to choose - if (!player.canRespond() || !canChoose(playerId, source, game)) { - break; - } + // there are possible multiple targets, so must check per target, not whole list + // good example: cast Scatter to the Winds with awaken + for (Target target : this) { + UUID abilityControllerId = target.getAffectedAbilityControllerId(playerId); - // stop on complete - Target target = this.getNextUnchosen(game); - if (target == null) { - break; - } + // stop on disconnect + Player player = game.getPlayer(abilityControllerId); + if (player == null || !player.canRespond()) { + return false; + } - // some targets can have controller different than ability controller - UUID targetController = playerId; - if (target.getTargetController() != null) { - targetController = target.getTargetController(); - } + // continue on nothing to choose or complete + if (target.isChoiceSelected() || !target.canChoose(abilityControllerId, source, game)) { + continue; + } - // disable cancel button - if cast without mana (e.g. by suspend you may not be able to cancel the casting if you are able to cast it - if (noMana) { - target.setRequired(true); - } - // enable cancel button - if (canCancel) { - target.setRequired(false); - } + // TODO: need research and remove or re-implement for other choices + // disable cancel button - if cast without mana (e.g. by Suspend) you may not be able to cancel the casting if you are able to cast it + if (noMana) { + target.setRequired(true); + } + // enable cancel button + if (canCancel) { + target.setRequired(false); + } - // stop on cancel/done - if (!target.chooseTarget(outcome, targetController, source, game)) { - if (!target.isChosen(game)) { - break; - } - } + // continue on cancel/skip one of the target + boolean choiceRes; + if (isTargetChoice) { + choiceRes = target.chooseTarget(outcome, abilityControllerId, source, game); + } else { + choiceRes = target.choose(outcome, abilityControllerId, source, game); + } + if (!choiceRes) { + //break; // do not stop targeting, example: two "or" targets from Finale of Promise + } + } - // reset on wrong restrictions and start from scratch - if (this.getNextUnchosen(game) == null - && game.replaceEvent(new GameEvent(GameEvent.EventType.TARGETS_VALID, source.getSourceId(), source, source.getControllerId()), source)) { - clearChosen(); - } - - // target done, can take next one - } while (true); + // TODO: need research or wait bug reports - old version was able to continue selection from scratch, + // current version just clear the chosen, but do not start selection again + // reset on wrong restrictions and start from scratch + if (isTargetChoice && isChosen(game) && game.replaceEvent(new GameEvent(GameEvent.EventType.TARGETS_VALID, source.getSourceId(), source, source.getControllerId()), source)) { + clearChosen(); } if (DebugUtil.GAME_SHOW_CHOOSE_TARGET_LOGS && !game.isSimulation()) { - printDebugTargets("chooseTargets finish", this, source, game); + printDebugTargets(isTargetChoice ? "target finish" : "choose finish", this, source, game); } return isChosen(game); diff --git a/Mage/src/main/java/mage/target/common/TargetActivatedAbility.java b/Mage/src/main/java/mage/target/common/TargetActivatedAbility.java index 17e7067d386..87ab3a0454d 100644 --- a/Mage/src/main/java/mage/target/common/TargetActivatedAbility.java +++ b/Mage/src/main/java/mage/target/common/TargetActivatedAbility.java @@ -54,38 +54,21 @@ public class TargetActivatedAbility extends TargetObject { @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - return canChoose(sourceControllerId, game); - } - - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - for (StackObject stackObject : game.getStack()) { - if (stackObject.getStackAbility() != null - && stackObject.getStackAbility().isActivatedAbility() - && game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getStackAbility().getControllerId()) - ) { - return true; - } - } - return false; + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - return possibleTargets(sourceControllerId, game); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { Set possibleTargets = new HashSet<>(); for (StackObject stackObject : game.getStack()) { if (stackObject.getStackAbility().isActivatedAbility() && game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getStackAbility().getControllerId()) - && filter.match(stackObject, game)) { + && filter.match(stackObject,sourceControllerId, source, game) + && this.notContains(stackObject.getId())) { possibleTargets.add(stackObject.getStackAbility().getId()); } } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetCardAndOrCard.java b/Mage/src/main/java/mage/target/common/TargetCardAndOrCard.java index d5c01e5f4b7..4d542757de9 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardAndOrCard.java +++ b/Mage/src/main/java/mage/target/common/TargetCardAndOrCard.java @@ -12,6 +12,7 @@ import mage.filter.predicate.Predicate; import mage.filter.predicate.Predicates; import mage.filter.predicate.mageobject.NamePredicate; import mage.game.Game; +import mage.game.permanent.Permanent; import mage.target.TargetCard; import mage.util.CardUtil; @@ -70,48 +71,23 @@ public class TargetCardAndOrCard extends TargetCard { return new TargetCardAndOrCard(this); } - @Override - public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { - if (!super.canTarget(playerId, id, source, game)) { - return false; - } - Card card = game.getCard(id); - if (card == null) { - return false; - } - if (this.getTargets().isEmpty()) { - return true; - } - Cards cards = new CardsImpl(this.getTargets()); - cards.add(card); - return assignment.getRoleCount(cards, game) >= cards.size(); - } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - // assuming max targets = 2, need to expand this code if not - Card card = game.getCard(this.getFirstTarget()); - if (card == null) { - return possibleTargets; // no further restriction if no target yet chosen - } - Cards cards = new CardsImpl(card); - if (assignment.getRoleCount(cards, game) == 2) { - // if the first chosen target is both types, no further restriction - return possibleTargets; - } - Set leftPossibleTargets = new HashSet<>(); - for (UUID possibleId : possibleTargets) { - Card possibleCard = game.getCard(possibleId); - Cards checkCards = cards.copy(); - checkCards.add(possibleCard); - if (assignment.getRoleCount(checkCards, game) == 2) { - // if the possible target and the existing target have both types, it's legal - // but this prevents the case of both targets with the same type - leftPossibleTargets.add(possibleId); + + // only valid roles + Cards existingTargets = new CardsImpl(this.getTargets()); + possibleTargets.removeIf(id -> { + Card card = game.getCard(id); + if (card == null) { + return true; } - } - return leftPossibleTargets; + Cards newTargets = existingTargets.copy(); + newTargets.add(card); + return assignment.getRoleCount(newTargets, game) < newTargets.size(); + }); + + return possibleTargets; } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInLibrary.java b/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInLibrary.java index ff4235ce33f..3b5b3461715 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInLibrary.java +++ b/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInLibrary.java @@ -14,7 +14,6 @@ import mage.filter.predicate.mageobject.NamePredicate; import mage.game.Game; import mage.util.CardUtil; -import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -71,48 +70,23 @@ public class TargetCardAndOrCardInLibrary extends TargetCardInLibrary { return new TargetCardAndOrCardInLibrary(this); } - @Override - public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { - if (!super.canTarget(playerId, id, source, game)) { - return false; - } - Card card = game.getCard(id); - if (card == null) { - return false; - } - if (this.getTargets().isEmpty()) { - return true; - } - Cards cards = new CardsImpl(this.getTargets()); - cards.add(card); - return assignment.getRoleCount(cards, game) >= cards.size(); - } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); - // assuming max targets = 2, need to expand this code if not - Card card = game.getCard(this.getFirstTarget()); - if (card == null) { - return possibleTargets; // no further restriction if no target yet chosen - } - Cards cards = new CardsImpl(card); - if (assignment.getRoleCount(cards, game) == 2) { - // if the first chosen target is both types, no further restriction - return possibleTargets; - } - Set leftPossibleTargets = new HashSet<>(); - for (UUID possibleId : possibleTargets) { - Card possibleCard = game.getCard(possibleId); - Cards checkCards = cards.copy(); - checkCards.add(possibleCard); - if (assignment.getRoleCount(checkCards, game) == 2) { - // if the possible target and the existing target have both types, it's legal - // but this prevents the case of both targets with the same type - leftPossibleTargets.add(possibleId); + + // only valid roles + Cards existingTargets = new CardsImpl(this.getTargets()); + possibleTargets.removeIf(id -> { + Card card = game.getCard(id); + if (card == null) { + return true; } - } - return leftPossibleTargets; + Cards newTargets = existingTargets.copy(); + newTargets.add(card); + return assignment.getRoleCount(newTargets, game) < newTargets.size(); + }); + + return possibleTargets; } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetCardInASingleGraveyard.java b/Mage/src/main/java/mage/target/common/TargetCardInASingleGraveyard.java index 38176140196..b0e98188d5a 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInASingleGraveyard.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInASingleGraveyard.java @@ -5,7 +5,6 @@ import mage.cards.Card; import mage.constants.Zone; import mage.filter.FilterCard; import mage.game.Game; -import mage.game.events.TargetEvent; import mage.players.Player; import mage.target.TargetCard; @@ -28,71 +27,35 @@ public class TargetCardInASingleGraveyard extends TargetCard { super(target); } - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - UUID firstTarget = this.getFirstTarget(); - - // If a card is already targeted, ensure that this new target has the same owner as currently chosen target - if (firstTarget != null) { - Card card = game.getCard(firstTarget); - Card targetCard = game.getCard(id); - if (card == null || targetCard == null || !card.isOwnedBy(targetCard.getOwnerId())) { - return false; - } - } - - // If it has the same owner (or no target picked) check that it's a valid target with super - return super.canTarget(id, source, game); - } - - /** - * Set of UUIDs of all possible targets - * - * @param sourceControllerId UUID of the ability's controller - * @param source Ability which requires the targets - * @param game Current game - * @return Set of the UUIDs of possible targets - */ @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); - UUID sourceId = source != null ? source.getSourceId() : null; - UUID controllerOfFirstTarget = null; - - // If any targets have been chosen, get the UUID of the owner in order to limit the targets to that owner's graveyard - if (!targets.isEmpty()) { - for (UUID cardInGraveyardId : targets.keySet()) { - Card targetCard = game.getCard(cardInGraveyardId); - if (targetCard == null) { - continue; - } - - controllerOfFirstTarget = targetCard.getOwnerId(); - break; // Only need the first UUID since they will all be the same - } - } + Card firstTarget = game.getCard(source.getFirstTarget()); for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - // If the playerId of this iteration is not the same as that of any existing target, then continue - // All cards must be from the same player's graveyard. - if (controllerOfFirstTarget != null && !playerId.equals(controllerOfFirstTarget)) { - continue; - } - Player player = game.getPlayer(playerId); if (player == null) { continue; } - for (Card card : player.getGraveyard().getCards(filter, sourceControllerId, source, game)) { - // TODO: Why for sourceId == null? - if (sourceId == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) { - possibleTargets.add(card.getId()); + if (firstTarget == null) { + // playable or not selected + // use any player + } else { + // already selected + // use from same player + if (!playerId.equals(firstTarget.getOwnerId())) { + continue; } } + + for (Card card : player.getGraveyard().getCards(filter, sourceControllerId, source, game)) { + possibleTargets.add(card.getId()); + } } - return possibleTargets; + + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetCardInCommandZone.java b/Mage/src/main/java/mage/target/common/TargetCardInCommandZone.java index 46a8988d0e1..a06ea169ab5 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInCommandZone.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInCommandZone.java @@ -46,7 +46,7 @@ public class TargetCardInCommandZone extends TargetCard { @Override public boolean canTarget(UUID id, Ability source, Game game) { - return this.canTarget(source.getControllerId(), id, source, game); + return this.canTarget(source == null ? null : source.getControllerId(), id, source, game); } @Override @@ -59,30 +59,13 @@ public class TargetCardInCommandZone extends TargetCard { Cards cards = new CardsImpl(game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY)); for (Card card : cards.getCards(filter, sourceControllerId, source, game)) { - if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { - possibleTargets.add(card.getId()); - } + possibleTargets.add(card.getId()); } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - Player player = game.getPlayer(sourceControllerId); - if (player == null) { - return false; - } - - int possibletargets = 0; - Cards cards = new CardsImpl(game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY)); - for (Card card : cards.getCards(filter, sourceControllerId, source, game)) { - if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { - possibletargets++; - if (possibletargets >= this.minNumberOfTargets) { - return true; - } - } - } - return false; + return canChooseFromPossibleTargets(sourceControllerId, source, game); } } \ No newline at end of file diff --git a/Mage/src/main/java/mage/target/common/TargetCardInExile.java b/Mage/src/main/java/mage/target/common/TargetCardInExile.java index 2c8cba1d09a..292e958cb1c 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInExile.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInExile.java @@ -12,34 +12,22 @@ import java.util.HashSet; import java.util.Set; import java.util.UUID; - /** * @author BetaSteward_at_googlemail.com */ public class TargetCardInExile extends TargetCard { - // If null, can target any card in exile matching [filter] - // If non-null, can only target - private final UUID zoneId; + private final UUID zoneId; // use null to target any exile zone or only specific - /** - * @param filter filter for the card to be a target - */ public TargetCardInExile(FilterCard filter) { this(1, 1, filter); } - /** - * @param minNumTargets minimum number of targets - * @param maxNumTargets maximum number of targets - * @param filter filter for the card to be a target - */ public TargetCardInExile(int minNumTargets, int maxNumTargets, FilterCard filter) { this(minNumTargets, maxNumTargets, filter, null); } /** - * @param filter filter for the card to be a target * @param zoneId if non-null can only target cards in that exileZone. if null card can be in ever exile zone. */ public TargetCardInExile(FilterCard filter, UUID zoneId) { @@ -47,10 +35,7 @@ public class TargetCardInExile extends TargetCard { } /** - * @param minNumTargets minimum number of targets - * @param maxNumTargets maximum number of targets - * @param filter filter for the card to be a target - * @param zoneId if non-null can only target cards in that exileZone. if null card can be in ever exile zone. + * @param zoneId if non-null can only target cards in that exileZone. if null card can be in ever exile zone. */ public TargetCardInExile(int minNumTargets, int maxNumTargets, FilterCard filter, UUID zoneId) { super(minNumTargets, maxNumTargets, Zone.EXILED, filter); @@ -65,6 +50,7 @@ public class TargetCardInExile extends TargetCard { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); + if (zoneId == null) { // no specific exile zone for (Card card : game.getExile().getAllCardsByRange(game, sourceControllerId)) { if (filter.match(card, sourceControllerId, source, game)) { @@ -81,40 +67,8 @@ public class TargetCardInExile extends TargetCard { } } } - return possibleTargets; - } - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - if (zoneId == null) { // no specific exile zone - int numberTargets = 0; - for (ExileZone exileZone : game.getExile().getExileZones()) { - numberTargets += exileZone.count(filter, sourceControllerId, source, game); - if (numberTargets >= this.minNumberOfTargets) { - return true; - } - } - } else { - ExileZone exileZone = game.getExile().getExileZone(zoneId); - return exileZone != null && exileZone.count(filter, sourceControllerId, source, game) >= this.minNumberOfTargets; - - } - return false; - } - - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - Card card = game.getCard(id); - if (card != null && game.getState().getZone(card.getId()) == Zone.EXILED) { - if (zoneId == null) { // no specific exile zone - return filter.match(card, source.getControllerId(), source, game); - } - ExileZone exile = game.getExile().getExileZone(zoneId); - if (exile != null && exile.contains(id)) { - return filter.match(card, source.getControllerId(), source, game); - } - } - return false; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java b/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java index b86014f5cac..368978950c8 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInGraveyardBattlefieldOrStack.java @@ -1,6 +1,5 @@ package mage.target.common; -import mage.MageObject; import mage.abilities.Ability; import mage.constants.ComparisonType; import mage.constants.Zone; @@ -18,7 +17,6 @@ import java.util.Set; import java.util.UUID; /** - * * @author LevelX2 */ public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard { @@ -59,28 +57,12 @@ public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard { @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - if (super.canChoose(sourceControllerId, source, game)) { - return true; - } - MageObject targetSource = game.getObject(source); - for (Permanent permanent : game.getBattlefield().getActivePermanents(filterPermanent, sourceControllerId, source, game)) { - if (notTarget || permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - return true; - } - } - for (StackObject stackObject : game.getStack()) { - if (stackObject instanceof Spell - && game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) - && filterSpell.match(stackObject, sourceControllerId, source, game)) { - return true; - } - } - return false; + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override public boolean canTarget(UUID id, Ability source, Game game) { - return this.canTarget(source.getControllerId(), id, source, game); + return this.canTarget(source == null ? null : source.getControllerId(), id, source, game); } @Override @@ -90,31 +72,22 @@ public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard { } Permanent permanent = game.getPermanent(id); if (permanent != null) { - return filterPermanent.match(permanent, playerId, source, game); + return playerId == null ? filterPermanent.match(permanent, game) : filterPermanent.match(permanent, playerId, source, game); } Spell spell = game.getSpell(id); - return spell != null && filterSpell.match(spell, playerId, source, game); - } - - @Override - public boolean canTarget(UUID id, Game game) { - return this.canTarget(null, id, null, game); // wtf - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - return this.possibleTargets(sourceControllerId, (Ability) null, game); + return spell != null && (playerId == null ? filter.match(spell, game) : filterSpell.match(spell, playerId, source, game)); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); // in graveyard first - MageObject targetSource = game.getObject(source); + + // from battlefield for (Permanent permanent : game.getBattlefield().getActivePermanents(filterPermanent, sourceControllerId, source, game)) { - if (notTarget || permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - possibleTargets.add(permanent.getId()); - } + possibleTargets.add(permanent.getId()); } + + // from stack for (StackObject stackObject : game.getStack()) { if (stackObject instanceof Spell && game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) @@ -122,7 +95,8 @@ public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard { possibleTargets.add(stackObject.getId()); } } - return possibleTargets; + + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetCardInHand.java b/Mage/src/main/java/mage/target/common/TargetCardInHand.java index 25dd47d6bb1..2057ca9126e 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInHand.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInHand.java @@ -1,7 +1,9 @@ package mage.target.common; +import mage.MageObject; import mage.abilities.Ability; import mage.cards.Card; +import mage.constants.TargetController; import mage.constants.Zone; import mage.filter.FilterCard; import mage.game.Game; @@ -53,7 +55,7 @@ public class TargetCardInHand extends TargetCard { @Override public boolean canTarget(UUID id, Ability source, Game game) { - return this.canTarget(source.getControllerId(), id, source, game); + return this.canTarget(source == null ? null : source.getControllerId(), id, source, game); } @Override @@ -61,30 +63,16 @@ public class TargetCardInHand extends TargetCard { Set possibleTargets = new HashSet<>(); Player player = game.getPlayer(sourceControllerId); if (player != null) { - for (Card card : player.getHand().getCards(filter, sourceControllerId, source, game)) { - if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { - possibleTargets.add(card.getId()); - } - } - } - return possibleTargets; + player.getHand().getCards(filter, sourceControllerId, source, game).stream() + .map(MageObject::getId) + .forEach(possibleTargets::add); + }; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int possibleTargets = 0; - Player player = game.getPlayer(sourceControllerId); - if (player != null) { - for (Card card : player.getHand().getCards(filter, sourceControllerId, source, game)) { - if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { - possibleTargets++; - if (possibleTargets >= this.minNumberOfTargets) { - return true; - } - } - } - } - return false; + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java b/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java index a6c111bca01..30475de61c2 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java @@ -97,7 +97,7 @@ public class TargetCardInLibrary extends TargetCard { chosen = isChosen(game); // stop by full complete - if (isChoiceCompleted(abilityControllerId, source, game)) { + if (isChoiceCompleted(abilityControllerId, source, game, null)) { break; } diff --git a/Mage/src/main/java/mage/target/common/TargetCardInOpponentsGraveyard.java b/Mage/src/main/java/mage/target/common/TargetCardInOpponentsGraveyard.java index e7f81c810ef..9968cd74126 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInOpponentsGraveyard.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInOpponentsGraveyard.java @@ -70,50 +70,6 @@ public class TargetCardInOpponentsGraveyard extends TargetCard { return false; } - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - return canChoose(sourceControllerId, null, game); - } - - /** - * Checks if there are enough {@link Card} that can be chosen. - * - * @param sourceControllerId - controller of the target event source - * @param source - * @param game - * @return - true if enough valid {@link Card} exist - */ - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - int possibleTargets = 0; - if (getMinNumberOfTargets() == 0) { // if 0 target is valid, the canChoose is always true - return true; - } - Player sourceController = game.getPlayer(sourceControllerId); - for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - if (!sourceController.hasOpponent(playerId, game)) { - continue; - } - if (this.allFromOneOpponent) { - possibleTargets = 0; - } - if (!playerId.equals(sourceControllerId)) { - Player player = game.getPlayer(playerId); - if (player != null) { - for (Card card : player.getGraveyard().getCards(filter, sourceControllerId, source, game)) { - if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { - possibleTargets++; - if (possibleTargets >= this.minNumberOfTargets) { - return true; - } - } - } - } - } - } - return false; - } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); @@ -126,9 +82,7 @@ public class TargetCardInOpponentsGraveyard extends TargetCard { if (player != null) { Set targetsInThisGraveyeard = new HashSet<>(); for (Card card : player.getGraveyard().getCards(filter, sourceControllerId, source, game)) { - if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { - targetsInThisGraveyeard.add(card.getId()); - } + targetsInThisGraveyeard.add(card.getId()); } // if there is not enough possible targets, the can't be any if (this.allFromOneOpponent && targetsInThisGraveyeard.size() < this.minNumberOfTargets) { @@ -137,7 +91,7 @@ public class TargetCardInOpponentsGraveyard extends TargetCard { possibleTargets.addAll(targetsInThisGraveyeard); } } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetCardInYourGraveyard.java b/Mage/src/main/java/mage/target/common/TargetCardInYourGraveyard.java index 5920ba3717d..53264f40a7e 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInYourGraveyard.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInYourGraveyard.java @@ -80,59 +80,9 @@ public class TargetCardInYourGraveyard extends TargetCard { Set possibleTargets = new HashSet<>(); Player player = game.getPlayer(sourceControllerId); for (Card card : player.getGraveyard().getCards(filter, sourceControllerId, source, game)) { - if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { - possibleTargets.add(card.getId()); - } + possibleTargets.add(card.getId()); } - return possibleTargets; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Cards cards, Ability source, Game game) { - Set possibleTargets = new HashSet<>(); - Player player = game.getPlayer(sourceControllerId); - if (player == null) { - return possibleTargets; - } - - for (Card card : cards.getCards(filter, sourceControllerId, source, game)) { - if (player.getGraveyard().getCards(game).contains(card)) { - possibleTargets.add(card.getId()); - } - } - return possibleTargets; - } - - /** - * Checks if there are enough {@link Card} that can be selected. - * - * @param sourceControllerId - controller of the select event - * @param game - * @return - true if enough valid {@link Card} exist - */ - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - return game.getPlayer(sourceControllerId).getGraveyard().count(filter, game) >= this.minNumberOfTargets; - } - - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - Player player = game.getPlayer(sourceControllerId); - if (player != null) { - if (this.minNumberOfTargets == 0) { - return true; - } - int possibleTargets = 0; - for (Card card : player.getGraveyard().getCards(filter, sourceControllerId, source, game)) { - if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { - possibleTargets++; - if (possibleTargets >= this.minNumberOfTargets) { - return true; - } - } - } - } - return false; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetCardInYourGraveyardOrExile.java b/Mage/src/main/java/mage/target/common/TargetCardInYourGraveyardOrExile.java index fd5b6816627..c36eb9e0a13 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardInYourGraveyardOrExile.java +++ b/Mage/src/main/java/mage/target/common/TargetCardInYourGraveyardOrExile.java @@ -33,47 +33,26 @@ public class TargetCardInYourGraveyardOrExile extends TargetCard { this.filterExile = target.filterExile; } - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - Player player = game.getPlayer(sourceControllerId); - if (player == null) { - return false; - } - int possibleTargets = 0; - // in your graveyard: - possibleTargets += countPossibleTargetInGraveyard(game, player, sourceControllerId, source, - filterGraveyard, isNotTarget(), this.minNumberOfTargets - possibleTargets); - if (possibleTargets >= this.minNumberOfTargets) { - return true; - } - // in exile: - possibleTargets += countPossibleTargetInExile(game, player, sourceControllerId, source, - filterExile, isNotTarget(), this.minNumberOfTargets - possibleTargets); - return possibleTargets >= this.minNumberOfTargets; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - return this.possibleTargets(sourceControllerId, (Ability) null, game); - } - @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); + Player player = game.getPlayer(sourceControllerId); if (player == null) { return possibleTargets; } - // in your graveyard: + + // in your graveyard possibleTargets.addAll(getAllPossibleTargetInGraveyard(game, player, sourceControllerId, source, filterGraveyard, isNotTarget())); - // in exile: + // in exile possibleTargets.addAll(getAllPossibleTargetInExile(game, player, sourceControllerId, source, filterExile, isNotTarget())); - return possibleTargets; + + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override public boolean canTarget(UUID id, Ability source, Game game) { - return this.canTarget(source.getControllerId(), id, source, game); + return this.canTarget(source == null ? null : source.getControllerId(), id, source, game); } @Override @@ -84,19 +63,14 @@ public class TargetCardInYourGraveyardOrExile extends TargetCard { } switch (game.getState().getZone(id)) { case GRAVEYARD: - return filterGraveyard.match(card, playerId, source, game); + return playerId == null ? filterGraveyard.match(card, game) : filterGraveyard.match(card, playerId, source, game); case EXILED: - return filterExile.match(card, playerId, source, game); + return playerId == null ? filterExile.match(card, game) : filterExile.match(card, playerId, source, game); default: return false; } } - @Override - public boolean canTarget(UUID id, Game game) { - return this.canTarget(null, id, null, game); - } - @Override public TargetCardInYourGraveyardOrExile copy() { return new TargetCardInYourGraveyardOrExile(this); diff --git a/Mage/src/main/java/mage/target/common/TargetCreaturesWithDifferentPowers.java b/Mage/src/main/java/mage/target/common/TargetCreaturesWithDifferentPowers.java index 121b2e8ca52..64689ae5288 100644 --- a/Mage/src/main/java/mage/target/common/TargetCreaturesWithDifferentPowers.java +++ b/Mage/src/main/java/mage/target/common/TargetCreaturesWithDifferentPowers.java @@ -34,8 +34,8 @@ public class TargetCreaturesWithDifferentPowers extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } Permanent creature = game.getPermanent(id); diff --git a/Mage/src/main/java/mage/target/common/TargetOpponentsChoicePermanent.java b/Mage/src/main/java/mage/target/common/TargetOpponentsChoicePermanent.java index a3ddb8a237d..10a9db39355 100644 --- a/Mage/src/main/java/mage/target/common/TargetOpponentsChoicePermanent.java +++ b/Mage/src/main/java/mage/target/common/TargetOpponentsChoicePermanent.java @@ -12,6 +12,8 @@ import mage.target.TargetPermanent; import java.util.UUID; /** + * TODO: rework to support possible targets + * * @author Mael */ public class TargetOpponentsChoicePermanent extends TargetPermanent { @@ -28,20 +30,15 @@ public class TargetOpponentsChoicePermanent extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game, boolean flag) { - return opponentId != null && super.canTarget(opponentId, id, source, game, flag); - } - - @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { Permanent permanent = game.getPermanent(id); if (opponentId != null) { if (permanent != null) { if (source != null) { boolean canSourceControllerTarget = true; if (!isNotTarget()) { - if (!permanent.canBeTargetedBy(game.getObject(source.getId()), controllerId, source, game) - || !permanent.canBeTargetedBy(game.getObject(source), controllerId, source, game)) { + if (!permanent.canBeTargetedBy(game.getObject(source.getId()), playerId, source, game) + || !permanent.canBeTargetedBy(game.getObject(source), playerId, source, game)) { canSourceControllerTarget = false; } } diff --git a/Mage/src/main/java/mage/target/common/TargetPermanentAmount.java b/Mage/src/main/java/mage/target/common/TargetPermanentAmount.java index 9525f7b11cc..2b963c75377 100644 --- a/Mage/src/main/java/mage/target/common/TargetPermanentAmount.java +++ b/Mage/src/main/java/mage/target/common/TargetPermanentAmount.java @@ -77,17 +77,8 @@ public class TargetPermanentAmount extends TargetAmount { return this.filter; } - @Override - public boolean canTarget(UUID objectId, Game game) { - Permanent permanent = game.getPermanent(objectId); - return filter.match(permanent, game); - } - @Override public boolean canTarget(UUID objectId, Ability source, Game game) { - if (getMaxNumberOfTargets() > 0 && getTargets().size() >= getMaxNumberOfTargets()) { - return getTargets().contains(objectId); - } Permanent permanent = game.getPermanent(objectId); if (permanent == null) { return false; @@ -95,8 +86,7 @@ public class TargetPermanentAmount extends TargetAmount { if (source == null) { return filter.match(permanent, game); } - MageObject targetSource = source.getSourceObject(game); - return (notTarget || permanent.canBeTargetedBy(targetSource, source.getControllerId(), source, game)) + return (isNotTarget() || permanent.canBeTargetedBy(game.getObject(source), source.getControllerId(), source, game)) && filter.match(permanent, source.getControllerId(), source, game); } @@ -107,47 +97,18 @@ public class TargetPermanentAmount extends TargetAmount { @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - return game.getBattlefield().getActivePermanents(filter, sourceControllerId, source, game).size() - >= this.minNumberOfTargets; - } - - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - return game.getBattlefield().getActivePermanents(filter, sourceControllerId, game).size() - >= this.minNumberOfTargets; + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - if (getMaxNumberOfTargets() > 0 && getTargets().size() >= getMaxNumberOfTargets()) { - return getTargets() - .stream() - .collect(Collectors.toSet()); - } - MageObject targetSource = game.getObject(source); - return game + Set possibleTargets = game .getBattlefield() .getActivePermanents(filter, sourceControllerId, source, game) .stream() - .filter(Objects::nonNull) - .filter(permanent -> notTarget || permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) - .map(Permanent::getId) - .collect(Collectors.toSet()); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - if (getMaxNumberOfTargets() > 0 && getTargets().size() >= getMaxNumberOfTargets()) { - return getTargets() - .stream() - .collect(Collectors.toSet()); - } - return game - .getBattlefield() - .getActivePermanents(filter, sourceControllerId, game) - .stream() .map(Permanent::getId) .collect(Collectors.toSet()); + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayer.java b/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayer.java index 6cc048b56ff..af75f3eda94 100644 --- a/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayer.java +++ b/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayer.java @@ -61,16 +61,6 @@ public class TargetPermanentOrPlayer extends TargetImpl { return filter; } - @Override - public boolean canTarget(UUID id, Game game) { - Permanent permanent = game.getPermanent(id); - if (permanent != null) { - return filter.match(permanent, game); - } - Player player = game.getPlayer(id); - return filter.match(player, game); - } - @Override public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { return canTarget(id, source, game); @@ -183,33 +173,16 @@ public class TargetPermanentOrPlayer extends TargetImpl { MageObject targetSource = game.getObject(source); for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { Player player = game.getPlayer(playerId); - if (player != null && (notTarget || player.canBeTargetedBy(targetSource, sourceControllerId, source, game)) && filter.match(player, sourceControllerId, source, game)) { + if (player != null && filter.match(player, sourceControllerId, source, game)) { possibleTargets.add(playerId); } } for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { - if ((notTarget || permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) && filter.match(permanent, sourceControllerId, source, game)) { + if (filter.match(permanent, sourceControllerId, source, game)) { possibleTargets.add(permanent.getId()); } } - return possibleTargets; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - Set possibleTargets = new HashSet<>(); - for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - Player player = game.getPlayer(playerId); - if (filter.match(player, game)) { - possibleTargets.add(playerId); - } - } - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { - if (filter.match(permanent, sourceControllerId, null, game)) { - possibleTargets.add(permanent.getId()); - } - } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayerAmount.java b/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayerAmount.java index a1e91b817cc..eee75f2dd54 100644 --- a/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayerAmount.java +++ b/Mage/src/main/java/mage/target/common/TargetPermanentOrPlayerAmount.java @@ -36,49 +36,30 @@ public abstract class TargetPermanentOrPlayerAmount extends TargetAmount { return this.filter; } - @Override - public boolean canTarget(UUID objectId, Game game) { - - // max targets limit reached (only selected can be chosen again) - if (getMaxNumberOfTargets() > 0 && getTargets().size() >= getMaxNumberOfTargets()) { - return getTargets().contains(objectId); - } - - Permanent permanent = game.getPermanent(objectId); - if (permanent != null) { - return filter.match(permanent, game); - } - Player player = game.getPlayer(objectId); - return filter.match(player, game); - } - @Override public boolean canTarget(UUID objectId, Ability source, Game game) { - - // max targets limit reached (only selected can be chosen again) - if (getMaxNumberOfTargets() > 0 && getTargets().size() >= getMaxNumberOfTargets()) { - return getTargets().contains(objectId); - } - Permanent permanent = game.getPermanent(objectId); Player player = game.getPlayer(objectId); if (source != null) { - MageObject targetSource = source.getSourceObject(game); if (permanent != null) { - return permanent.canBeTargetedBy(targetSource, source.getControllerId(), source, game) + return (isNotTarget() || permanent.canBeTargetedBy(game.getObject(source), source.getControllerId(), source, game)) && filter.match(permanent, source.getControllerId(), source, game); } if (player != null) { - return player.canBeTargetedBy(targetSource, source.getControllerId(), source, game) + return (isNotTarget() || player.canBeTargetedBy(game.getObject(source), source.getControllerId(), source, game)) && filter.match(player, game); } + } else { + if (permanent != null) { + return filter.match(permanent, game); + } + if (player != null) { + return filter.match(player, game); + } } - if (permanent != null) { - return filter.match(permanent, game); - } - return filter.match(player, game); + return false; } @Override @@ -88,100 +69,13 @@ public abstract class TargetPermanentOrPlayerAmount extends TargetAmount { @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - // no max targets limit here - int count = 0; - MageObject targetSource = game.getObject(source); - for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - Player player = game.getPlayer(playerId); - if (player == null - || !player.canBeTargetedBy(targetSource, sourceControllerId, source, game) - || !filter.match(player, game)) { - continue; - } - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { - if (!permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - continue; - } - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - return false; - } - - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - // no max targets limit here - int count = 0; - for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) { - Player player = game.getPlayer(playerId); - if (player == null || !filter.match(player, game)) { - continue; - } - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - return false; + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { Set possibleTargets = new HashSet<>(); - // max targets limit reached (only selected can be chosen again) - if (getMaxNumberOfTargets() > 0 && getTargets().size() >= getMaxNumberOfTargets()) { - possibleTargets.addAll(getTargets()); - return possibleTargets; - } - - MageObject targetSource = game.getObject(source); - - game.getState() - .getPlayersInRange(sourceControllerId, game) - .stream() - .map(game::getPlayer) - .filter(Objects::nonNull) - .filter(player -> player.canBeTargetedBy(targetSource, sourceControllerId, source, game) - && filter.match(player, game) - ) - .map(Player::getId) - .forEach(possibleTargets::add); - - game.getBattlefield() - .getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game) - .stream() - .filter(Objects::nonNull) - .filter(permanent -> permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)) - .map(Permanent::getId) - .forEach(possibleTargets::add); - - return possibleTargets; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - Set possibleTargets = new HashSet<>(); - - // max targets limit reached (only selected can be chosen again) - if (getMaxNumberOfTargets() > 0 && getTargets().size() >= getMaxNumberOfTargets()) { - possibleTargets.addAll(getTargets()); - return possibleTargets; - } - game.getState() .getPlayersInRange(sourceControllerId, game) .stream() @@ -194,10 +88,11 @@ public abstract class TargetPermanentOrPlayerAmount extends TargetAmount { game.getBattlefield() .getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game) .stream() + .filter(Objects::nonNull) .map(Permanent::getId) .forEach(possibleTargets::add); - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetPermanentOrSuspendedCard.java b/Mage/src/main/java/mage/target/common/TargetPermanentOrSuspendedCard.java index 0f00929e453..144b3aa29b3 100644 --- a/Mage/src/main/java/mage/target/common/TargetPermanentOrSuspendedCard.java +++ b/Mage/src/main/java/mage/target/common/TargetPermanentOrSuspendedCard.java @@ -55,26 +55,14 @@ public class TargetPermanentOrSuspendedCard extends TargetImpl { @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - MageObject sourceObject = game.getObject(source); - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { - if (permanent.canBeTargetedBy(sourceObject, sourceControllerId, source, game) && filter.match(permanent, sourceControllerId, source, game)) { - return true; - } - } - for (Card card : game.getExile().getAllCards(game)) { - if (filter.match(card, sourceControllerId, source, game)) { - return true; - } - } - return false; + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set possibleTargets = new HashSet<>(20); - MageObject sourceObject = game.getObject(source); + Set possibleTargets = new HashSet<>(); for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { - if (permanent.canBeTargetedBy(sourceObject, sourceControllerId, source, game) && filter.match(permanent, sourceControllerId, source, game)) { + if (filter.match(permanent, sourceControllerId, source, game)) { possibleTargets.add(permanent.getId()); } } @@ -83,17 +71,7 @@ public class TargetPermanentOrSuspendedCard extends TargetImpl { possibleTargets.add(card.getId()); } } - return possibleTargets; - } - - @Override - public boolean canTarget(UUID id, Game game) { - Permanent permanent = game.getPermanent(id); - if (permanent != null) { - return filter.match(permanent, game); - } - Card card = game.getExile().getCard(id, game); - return filter.match(card, game); + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override @@ -101,8 +79,7 @@ public class TargetPermanentOrSuspendedCard extends TargetImpl { Permanent permanent = game.getPermanent(id); if (permanent != null) { if (source != null) { - MageObject targetSource = game.getObject(source); - return permanent.canBeTargetedBy(targetSource, source.getControllerId(), source, game) + return (isNotTarget() || permanent.canBeTargetedBy(game.getObject(source), source.getControllerId(), source, game)) && filter.match(permanent, source.getControllerId(), source, game); } else { return filter.match(permanent, game); @@ -117,16 +94,6 @@ public class TargetPermanentOrSuspendedCard extends TargetImpl { return this.canTarget(id, source, game); } - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - return this.canChoose(sourceControllerId, null, game); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - return this.possibleTargets(sourceControllerId, null, game); - } - @Override public String getTargetedName(Game game) { StringBuilder sb = new StringBuilder(); diff --git a/Mage/src/main/java/mage/target/common/TargetPermanentSameController.java b/Mage/src/main/java/mage/target/common/TargetPermanentSameController.java index 9dd16fff7e8..b6c1898fc72 100644 --- a/Mage/src/main/java/mage/target/common/TargetPermanentSameController.java +++ b/Mage/src/main/java/mage/target/common/TargetPermanentSameController.java @@ -28,8 +28,8 @@ public class TargetPermanentSameController extends TargetPermanent { } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (!super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { return false; } if (this.getTargets().isEmpty()) { diff --git a/Mage/src/main/java/mage/target/common/TargetSpellOrPermanent.java b/Mage/src/main/java/mage/target/common/TargetSpellOrPermanent.java index 5122e3d5763..b8a432dab60 100644 --- a/Mage/src/main/java/mage/target/common/TargetSpellOrPermanent.java +++ b/Mage/src/main/java/mage/target/common/TargetSpellOrPermanent.java @@ -73,23 +73,12 @@ public class TargetSpellOrPermanent extends TargetImpl { this.filter = filter; } - @Override - public boolean canTarget(UUID id, Game game) { - Permanent permanent = game.getPermanent(id); - if (permanent != null) { - return filter.match(permanent, game); - } - Spell spell = game.getStack().getSpell(id); - return filter.match(spell, game); - } - @Override public boolean canTarget(UUID id, Ability source, Game game) { Permanent permanent = game.getPermanent(id); if (permanent != null) { if (source != null) { - MageObject targetSource = game.getObject(source); - return permanent.canBeTargetedBy(targetSource, source.getControllerId(), source, game) + return (isNotTarget() || permanent.canBeTargetedBy(game.getObject(source), source.getControllerId(), source, game)) && filter.match(permanent, source.getControllerId(), source, game); } else { return filter.match(permanent, game); @@ -183,35 +172,18 @@ public class TargetSpellOrPermanent extends TargetImpl { for (StackObject stackObject : game.getStack()) { Spell spell = game.getStack().getSpell(stackObject.getId()); if (spell != null - && !source.getSourceId().equals(spell.getSourceId()) + && targetSource != null + && !targetSource.getId().equals(spell.getSourceId()) && filter.match(spell, sourceControllerId, source, game)) { possibleTargets.add(spell.getId()); } } for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { - if (permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game) && filter.match(permanent, sourceControllerId, source, game)) { + if (filter.match(permanent, sourceControllerId, source, game)) { possibleTargets.add(permanent.getId()); } } - return possibleTargets; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - Set possibleTargets = new HashSet<>(); - for (StackObject stackObject : game.getStack()) { - Spell spell = game.getStack().getSpell(stackObject.getId()); - if (spell != null - && filter.match(spell, sourceControllerId, null, game)) { - possibleTargets.add(spell.getId()); - } - } - for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { - if (filter.match(permanent, sourceControllerId, null, game)) { - possibleTargets.add(permanent.getId()); - } - } - return possibleTargets; + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override diff --git a/Mage/src/main/java/mage/target/common/TargetTappedPermanentAsYouCast.java b/Mage/src/main/java/mage/target/common/TargetTappedPermanentAsYouCast.java index 4fa993cba4a..db7f03551a3 100644 --- a/Mage/src/main/java/mage/target/common/TargetTappedPermanentAsYouCast.java +++ b/Mage/src/main/java/mage/target/common/TargetTappedPermanentAsYouCast.java @@ -30,21 +30,21 @@ public class TargetTappedPermanentAsYouCast extends TargetPermanent { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - return game.getBattlefield().getActivePermanents(getFilter(), source.getControllerId(), source, game).stream() + Set possibleTargets = game.getBattlefield().getActivePermanents(getFilter(), sourceControllerId, source, game).stream() .filter(Permanent::isTapped) .map(Permanent::getId) .collect(Collectors.toSet()); + return keepValidPossibleTargets(possibleTargets, sourceControllerId, source, game); } @Override public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - return game.getBattlefield().getActivePermanents(getFilter(), source.getControllerId(), source, game).stream() - .anyMatch(Permanent::isTapped); + return canChooseFromPossibleTargets(sourceControllerId, source, game); } @Override - public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { - if (super.canTarget(controllerId, id, source, game)) { + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (super.canTarget(playerId, id, source, game)) { Permanent permanent = game.getPermanent(id); return permanent != null && permanent.isTapped(); } @@ -54,6 +54,14 @@ public class TargetTappedPermanentAsYouCast extends TargetPermanent { // See ruling: https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/253345-dream-leash @Override public boolean stillLegalTarget(UUID controllerId, UUID id, Ability source, Game game) { + // on resolve must ignore tapped status + + // The middle ability of Enthralling Hold affects only the choice of target as the spell is cast. + // If the creature becomes untapped before the spell resolves, it still resolves. If a player is allowed + // to change the spell's target while it's on the stack, they may choose an untapped creature. If you put + // Enthralling Hold onto the battlefield without casting it, you may attach it to an untapped creature. + // (2020-06-23) + Permanent permanent = game.getPermanent(id); return permanent != null && getFilter().match(permanent, game) diff --git a/Mage/src/main/java/mage/target/common/TargetTriggeredAbility.java b/Mage/src/main/java/mage/target/common/TargetTriggeredAbility.java index 585af8abdd0..b771bb39943 100644 --- a/Mage/src/main/java/mage/target/common/TargetTriggeredAbility.java +++ b/Mage/src/main/java/mage/target/common/TargetTriggeredAbility.java @@ -65,14 +65,10 @@ public class TargetTriggeredAbility extends TargetObject { @Override public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - return possibleTargets(sourceControllerId, game); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { return game.getStack().stream() .filter(TargetTriggeredAbility::isTriggeredAbility) .map(stackObject -> stackObject.getStackAbility().getId()) + .filter(this::notContains) .collect(Collectors.toSet()); } diff --git a/Mage/src/main/java/mage/util/ConsoleUtil.java b/Mage/src/main/java/mage/util/ConsoleUtil.java new file mode 100644 index 00000000000..cb143692785 --- /dev/null +++ b/Mage/src/main/java/mage/util/ConsoleUtil.java @@ -0,0 +1,19 @@ +package mage.util; + +/** + * Helper class to work with console logs + */ +public class ConsoleUtil { + + public static String asRed(String text) { + return "\u001B[31m" + text + "\u001B[0m"; + } + + public static String asGreen(String text) { + return "\u001B[32m" + text + "\u001B[0m"; + } + + public static String asYellow(String text) { + return "\u001B[33m" + text + "\u001B[0m"; + } +} diff --git a/Mage/src/main/java/mage/util/DebugUtil.java b/Mage/src/main/java/mage/util/DebugUtil.java index fcad86ea034..8706f5f2ffb 100644 --- a/Mage/src/main/java/mage/util/DebugUtil.java +++ b/Mage/src/main/java/mage/util/DebugUtil.java @@ -15,7 +15,7 @@ public class DebugUtil { // game simulations runs in multiple threads, if you stop code to debug then it will be terminated by timeout // so AI debug mode will make single simulation thread without any timeouts public static boolean AI_ENABLE_DEBUG_MODE = false; - public static boolean AI_SHOW_TARGET_OPTIMIZATION_LOGS = false; // works with target amount + public static boolean AI_SHOW_TARGET_OPTIMIZATION_LOGS = false; // works with target and target amount calculations // SERVER // data collectors - enable additional logs and data collection for better AI and human games debugging diff --git a/Mage/src/main/java/mage/watchers/common/RevoltWatcher.java b/Mage/src/main/java/mage/watchers/common/RevoltWatcher.java index b6f0c6583e1..e5d49b85a47 100644 --- a/Mage/src/main/java/mage/watchers/common/RevoltWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/RevoltWatcher.java @@ -17,7 +17,7 @@ import java.util.UUID; */ public class RevoltWatcher extends Watcher { - private final Set revoltActivePlayerIds = new HashSet<>(0); + private final Set revoltActivePlayerIds = new HashSet<>(); public RevoltWatcher() { super(WatcherScope.GAME); diff --git a/Mage/src/main/resources/tokens-database.txt b/Mage/src/main/resources/tokens-database.txt index 8d0dacf120a..b7dab103f95 100644 --- a/Mage/src/main/resources/tokens-database.txt +++ b/Mage/src/main/resources/tokens-database.txt @@ -1535,7 +1535,8 @@ |Generate|TOK:SLD|Hydra|||ZaxaraTheExemplaryHydraToken| |Generate|TOK:SLD|Icingdeath, Frost Tongue|||IcingdeathFrostTongueToken| |Generate|TOK:SLD|Marit Lage|||MaritLageToken| -|Generate|TOK:SLD|Mechtitan|||MechtitanToken| +|Generate|TOK:SLD|Mechtitan|1||MechtitanToken| +|Generate|TOK:SLD|Mechtitan|2||MechtitanToken| |Generate|TOK:SLD|Myr|||MyrToken| |Generate|TOK:SLD|Saproling|||SaprolingToken| |Generate|TOK:SLD|Shapeshifter|1||ShapeshifterBlueToken| diff --git a/Mage/src/test/data/importer/testdeck.mtga b/Mage/src/test/data/importer/testdeck.mtga index fe31c404b5b..887a568c686 100644 --- a/Mage/src/test/data/importer/testdeck.mtga +++ b/Mage/src/test/data/importer/testdeck.mtga @@ -5,6 +5,9 @@ 1 Expansion // Explosion (GRN) 224 1 Forest (XLN) 277 1 Teferi, Hero of Dominaria (DAR) 207 +1 Benalish Marshal (PDOM) 6p +1 Benalish Marshal (PDOM) 6s +1 Benalish Marshal 3 Unmoored Ego (GRN) 212 1 Beacon Bolt (GRN) 154 diff --git a/Mage/src/test/java/mage/cards/decks/importer/FakeCardLookup.java b/Mage/src/test/java/mage/cards/decks/importer/FakeCardLookup.java index c655b8fff16..beab8edea02 100644 --- a/Mage/src/test/java/mage/cards/decks/importer/FakeCardLookup.java +++ b/Mage/src/test/java/mage/cards/decks/importer/FakeCardLookup.java @@ -29,26 +29,29 @@ public class FakeCardLookup extends CardLookup { return this; } - public Optional lookupCardInfo(String cardName) { + public CardInfo lookupCardInfo(String cardName) { CardInfo card = lookup.get(cardName); if (card != null) { - return Optional.of(card); + return card; } if (alwaysMatches) { - return Optional.of(new CardInfo() {{ + return new CardInfo() {{ name = cardName; - }}); + }}; } - return Optional.empty(); + return null; } @Override public List lookupCardInfo(CardCriteria criteria) { - return lookupCardInfo(criteria.getName()) - .map(Collections::singletonList) - .orElse(Collections.emptyList()); + CardInfo found = lookupCardInfo(criteria.getName()); + if (found != null) { + return new ArrayList<>(Collections.singletonList(found)); + } else { + return Collections.emptyList(); + } } } diff --git a/Mage/src/test/java/mage/cards/decks/importer/MtgaImporterTest.java b/Mage/src/test/java/mage/cards/decks/importer/MtgaImporterTest.java index 865dae482f5..46c82f86a4d 100644 --- a/Mage/src/test/java/mage/cards/decks/importer/MtgaImporterTest.java +++ b/Mage/src/test/java/mage/cards/decks/importer/MtgaImporterTest.java @@ -36,11 +36,12 @@ public class MtgaImporterTest { .addMain("Expansion // Explosion", 1) .addMain("Forest", 1) .addMain("Teferi, Hero of Dominaria", 1) + .addMain("Benalish Marshal", 3) .addSide("Unmoored Ego", 3) .addSide("Beacon Bolt", 1) - .verify(deck, 8, 4); + .verify(deck, 11, 4); } } \ No newline at end of file diff --git a/Utils/keywords.txt b/Utils/keywords.txt index fa446453d07..a2e1ccb4ebe 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -52,6 +52,7 @@ Exploit|new| Extort|new| Fabricate|number| Fear|instance| +Firebending|number| First strike|instance| Flanking|new| Flash|instance| diff --git a/Utils/known-sets.txt b/Utils/known-sets.txt index a3483768fea..9ea77b07980 100644 --- a/Utils/known-sets.txt +++ b/Utils/known-sets.txt @@ -19,6 +19,7 @@ Asia Pacific Land Program|AsiaPacificLandProgram| Assassin's Creed|AssassinsCreed| Avacyn Restored|AvacynRestored| Avatar: The Last Airbender|AvatarTheLastAirbender| +Avatar: The Last Airbender Eternal|AvatarTheLastAirbenderEternal| Battlebond|Battlebond| Battle for Zendikar|BattleForZendikar| Betrayers of Kamigawa|BetrayersOfKamigawa| diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index f4216fda947..fc2b028b622 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -59718,16 +59718,57 @@ Temple of Triumph|Edge of Eternities Commander|188|R||Land|||This land enters ta Twilight Mire|Edge of Eternities Commander|189|R||Land|||{T}: Add {C}.${B/G}, {T}: Add {B}{B}, {B}{G}, or {G}{G}.| Viridescent Bog|Edge of Eternities Commander|190|R||Land|||{1}, {T}: Add {B}{G}.| Wastes|Edge of Eternities Commander|191|C||Basic Land|||{T}: Add {C}.| +Aang's Journey|Avatar: The Last Airbender|1|C|{2}|Sorcery - Lesson|||Kicker {2}$Search your library for a basic land card. If this spell was kicked, instead search your library for a basic land card and a Shrine card. Reveal those cards, put them into your hand, then shuffle.$You gain 2 life.| +Aang's Iceberg|Avatar: The Last Airbender|5|R|{2}{W}|Enchantment|||Flash$When this enchantment enters, exile up to one other target nonland permanent until this enchantment leaves the battlefield.$Waterbend {3}: Sacrifice this enchantment. If you do, scry 2.| +Appa, Steadfast Guardian|Avatar: The Last Airbender|10|M|{2}{W}{W}|Legendary Creature - Bison Ally|3|4|Flash$Flying$When Appa enters, airbend any number of other target nonland permanents you control.$Whenever you cast a spell from exile, create a 1/1 white Ally creature token.| +Avatar Enthusiasts|Avatar: The Last Airbender|11|C|{2}{W}|Creature - Human Peasant Ally|2|2|Whenever another Ally you control enters, put a +1/+1 counter on this creature.| +Momo, Friendly Flier|Avatar: The Last Airbender|29|R|{W}|Legendary Creature - Lemur Bat Ally|1|1|Flying$The first non-Lemur creature spell with flying you cast during each of your turns costs {1} less to cast.$Whenever another creature you control with flying enters, Momo gets +1/+1 until end of turn.| +Southern Air Temple|Avatar: The Last Airbender|36|U|{3}{W}|Legendary Enchantment - Shrine|||When Southern Air Temple enters, put X +1/+1 counters on each creature you control, where X is the number of Shrines you control.$Whenever another Shrine you control enters, put a +1/+1 counter on each creature you control.| +Sokka's Haiku|Avatar: The Last Airbender|71|U|{3}{U}{U}|Instant - Lesson|||Counter target spell.$Draw a card, then mill three cards.$Untap target land.| +Yue, the Moon Spirit|Avatar: The Last Airbender|83|R|{3}{U}|Legendary Creature - Spirit Ally|3|3|Flying, vigilance$Waterbend {5}, {T}: You may cast a noncreature spell from your hand without paying its mana cost.| +The Rise of Sozin|Avatar: The Last Airbender|117|M|{4}{B}{B}|Enchantment - Saga|||(As this Saga enters and after your draw step, add a lore counter.)$I -- Destroy all creatures.$II -- Choose a card name. Search target opponent's graveyard, hand, and library for up to four cards with that name and exile them. Then that player shuffles.$III -- Exile this Saga, then return it to the battlefield transformed under your control.| +Fire Lord Sozin|Avatar: The Last Airbender|117|M||Legendary Creature - Human Noble|5|5|Menace, firebending 3$Whenever Fire Lord Sozin deals combat damage to a player, you may pay {X}. When you do, put any number of target creature cards with total mana value X or less from that player's graveyard onto the battlefield under your control.| +Fated Firepower|Avatar: The Last Airbender|132|M|{X}{R}{R}{R}|Enchantment|||Flash$This enchantment enters with X fire counters on it.$If a source you control would deal damage to an opponent or a permanent an opponent controls, it deals that much damage plus an amount of damage equal to the number of fire counters on this enchantment instead.| +Fire Nation Attacks|Avatar: The Last Airbender|133|U|{4}{R}|Instant|||Create two 2/2 red Soldier creature tokens with firebending 1.$Flashback {8}{R}| +Redirect Lightning|Avatar: The Last Airbender|151|R|{R}|Instant - Lesson|||As an additional cost to cast this spell, pay 5 life or pay {2}.$Change the target of target spell or ability with a single target.| +Earthbending Lesson|Avatar: The Last Airbender|176|C|{3}{G}|Sorcery - Lesson|||Earthbend 4.| +Avatar Aang|Avatar: The Last Airbender|207|M|{R}{G}{W}{U}|Legendary Creature - Human Avatar Ally|4|4|Flying, firebending 2$Whenever you waterbend, earthbend, firebend, or airbend, draw a card. Then if you've done all four this turn, transform Avatar Aang.| +Aang, Master of Elements|Avatar: The Last Airbender|207|M||Legendary Creature - Avatar Ally|6|6|Flying$Spells you cast cost {W}{U}{B}{R}{G} less to cast.$At the beginning of each upkeep, you may transform Aang, Master of Elements. If you do, you gain 4 life, draw four cards, put four +1/+1 counters on him, and he deals 4 damage to each opponent.| +Fire Lord Zuko|Avatar: The Last Airbender|221|R|{R}{W}{B}|Legendary Creature - Human Noble Ally|2|4|Firebending X, where X is Fire Lord Zuko's power.$Whenever you cast a spell from exile and whenever a permanent you control enters from exile, put a +1/+1 counter on each creature you control.| +Katara, the Fearless|Avatar: The Last Airbender|230|R|{G}{W}{U}|Legendary Creature - Human Warrior Ally|3|3|If a triggered ability of an Ally you control triggers, that ability triggers an additional time.| +Katara, Water Tribe's Hope|Avatar: The Last Airbender|231|R|{2}{W}{U}{U}|Legendary Creature - Human Warrior Ally|3|3|Vigilance$When Katara enters, create a 1/1 white Ally creature token.$Waterbend {X}: Creatures you control have base power and toughness X/X until end of turn. X can't be 0. Activate only during your turn.| +Sokka, Bold Boomeranger|Avatar: The Last Airbender|240|R|{U}{R}|Legendary Creature - Human Warrior Ally|1|1|When Sokka enters, discard up to two cards, then draw that many cards.$Whenever you cast an artifact or Lesson spell, put a +1/+1 counter on Sokka.| +Toph, the First Metalbender|Avatar: The Last Airbender|247|R|{1}{R}{G}{W}|Legendary Creature - Human Warrior Ally|3|3|Nontoken artifacts you control are lands in addition to their other types.$At the beginning of your end step, earthbend 2.| +Plains|Avatar: The Last Airbender|287|C||Basic Land - Plains|||({T}: Add {W}.)| +Island|Avatar: The Last Airbender|288|C||Basic Land - Island|||({T}: Add {U}.)| +Swamp|Avatar: The Last Airbender|289|C||Basic Land - Swamp|||({T}: Add {B}.)| +Mountain|Avatar: The Last Airbender|290|C||Basic Land - Mountain|||({T}: Add {R}.)| +Forest|Avatar: The Last Airbender|291|C||Basic Land - Forest|||({T}: Add {G}.)| +Appa, Steadfast Guardian|Avatar: The Last Airbender|316|M|{2}{W}{W}|Legendary Creature - Bison Ally|3|4|Flash$Flying$When Appa enters, airbend any number of other target nonland permanents you control.$Whenever you cast a spell from exile, create a 1/1 white Ally creature token.| +Momo, Friendly Flier|Avatar: The Last Airbender|317|R|{W}|Legendary Creature - Lemur Bat Ally|1|1|Flying$The first non-Lemur creature spell with flying you cast during each of your turns costs {1} less to cast.$Whenever another creature you control with flying enters, Momo gets +1/+1 until end of turn.| +Aang's Iceberg|Avatar: The Last Airbender|336|R|{2}{W}|Enchantment|||Flash$When this enchantment enters, exile up to one other target nonland permanent until this enchantment leaves the battlefield.$Waterbend {3}: Sacrifice this enchantment. If you do, scry 2.| +Yue, the Moon Spirit|Avatar: The Last Airbender|338|R|{3}{U}|Legendary Creature - Spirit Ally|3|3|Flying, vigilance$Waterbend {5}, {T}: You may cast a noncreature spell from your hand without paying its mana cost.| +Fated Firepower|Avatar: The Last Airbender|341|M|{X}{R}{R}{R}|Enchantment|||Flash$This enchantment enters with X fire counters on it.$If a source you control would deal damage to an opponent or a permanent an opponent controls, it deals that much damage plus an amount of damage equal to the number of fire counters on this enchantment instead.| +Redirect Lightning|Avatar: The Last Airbender|343|R|{R}|Instant - Lesson|||As an additional cost to cast this spell, pay 5 life or pay {2}.$Change the target of target spell or ability with a single target.| +Katara, the Fearless|Avatar: The Last Airbender|350|R|{G}{W}{U}|Legendary Creature - Human Warrior Ally|3|3|If a triggered ability of an Ally you control triggers, that ability triggers an additional time.| +Katara, Water Tribe's Hope|Avatar: The Last Airbender|351|R|{2}{W}{U}{U}|Legendary Creature - Human Warrior Ally|3|3|Vigilance$When Katara enters, create a 1/1 white Ally creature token.$Waterbend {X}: Creatures you control have base power and toughness X/X until end of turn. X can't be 0. Activate only during your turn.| +Toph, the First Metalbender|Avatar: The Last Airbender|353|R|{1}{R}{G}{W}|Legendary Creature - Human Warrior Ally|3|3|Nontoken artifacts you control are lands in addition to their other types.$At the beginning of your end step, earthbend 2.| +The Rise of Sozin|Avatar: The Last Airbender|356|M|{4}{B}{B}|Enchantment - Saga|||(As this Saga enters and after your draw step, add a lore counter.)$I -- Destroy all creatures.$II -- Choose a card name. Search target opponent's graveyard, hand, and library for up to four cards with that name and exile them. Then that player shuffles.$III -- Exile this Saga, then return it to the battlefield transformed under your control.| +Fire Lord Sozin|Avatar: The Last Airbender|356|M||Legendary Creature - Human Noble|5|5|Menace, firebending 3$Whenever Fire Lord Sozin deals combat damage to a player, you may pay {X}. When you do, put any number of target creature cards with total mana value X or less from that player's graveyard onto the battlefield under your control.| +Fire Lord Zuko|Avatar: The Last Airbender|360|R|{R}{W}{B}|Legendary Creature - Human Noble Ally|2|4|Firebending X, where X is Fire Lord Zuko's power.$Whenever you cast a spell from exile and whenever a permanent you control enters from exile, put a +1/+1 counter on each creature you control.| +Katara, the Fearless|Avatar: The Last Airbender|361|R|{G}{W}{U}|Legendary Creature - Human Warrior Ally|3|3|If a triggered ability of an Ally you control triggers, that ability triggers an additional time.| +Toph, the First Metalbender|Avatar: The Last Airbender|362|R|{1}{R}{G}{W}|Legendary Creature - Human Warrior Ally|3|3|Nontoken artifacts you control are lands in addition to their other types.$At the beginning of your end step, earthbend 2.| Avatar Aang|Avatar: The Last Airbender|363|M|{R}{G}{W}{U}|Legendary Creature - Human Avatar Ally|4|4|Flying, firebending 2$Whenever you waterbend, earthbend, firebend, or airbend, draw a card. Then if you've done all four this turn, transform Avatar Aang.| -Aang, Master of Elements|Avatar: The Last Airbender|363|M||Legendary Creature - Avatar Ally|6|6|Flying$Spelsl you cast cost {W}{U}{B}{R}{G} less to cast.$At the beginning of each upkeep, you may transform Aang, Master of Elements. If you do, you gain 4 life, draw four cards, put four +1/+1 counters on him, and he deals 4 damage to each opponent.| -Anti-Venom, Horrifying Healer|Marvel's Spider-Man|1|M|{W}{W}{W}{W}{W}|Legendary Creature - Symbiote Hero|5|5|When Anti-Venom enters, if he was cast, return target creature card from your graveyard to the battlefield. If damage would be dealt to Anti-Venom, prevent that damage and put that many +1/+1 counters on him.| +Aang, Master of Elements|Avatar: The Last Airbender|363|M||Legendary Creature - Avatar Ally|6|6|Flying$Spells you cast cost {W}{U}{B}{R}{G} less to cast.$At the beginning of each upkeep, you may transform Aang, Master of Elements. If you do, you gain 4 life, draw four cards, put four +1/+1 counters on him, and he deals 4 damage to each opponent.| +Sokka, Bold Boomeranger|Avatar: The Last Airbender|383|R|{U}{R}|Legendary Creature - Human Warrior Ally|1|1|When Sokka enters, discard up to two cards, then draw that many cards.$Whenever you cast an artifact or Lesson spell, put a +1/+1 counter on Sokka.| +Anti-Venom, Horrifying Healer|Marvel's Spider-Man|1|M|{W}{W}{W}{W}{W}|Legendary Creature - Symbiote Hero|5|5|When Anti-Venom enters, if he was cast, return target creature card from your graveyard to the battlefield.$If damage would be dealt to Anti-Venom, prevent that damage and put that many +1/+1 counters on him.| Aunt May|Marvel's Spider-Man|3|U|{W}|Legendary Creature - Human Citizen|0|2|Whenever another creature you control enters, you gain 1 life. If it's a Spider, put a +1/+1 counter on it.| Daily Bugle Reporters|Marvel's Spider-Man|6|C|{3}{W}|Creature - Human Citizen|2|3|When this creature enters, choose one --$* Puff Piece -- Put a +1/+1 counter on each of up to two target creatures.$* Investigative Journalism -- Return target creature card with mana value 2 or less from your graveyard to your hand.| Origin of Spider-Man|Marvel's Spider-Man|9|R|{1}{W}|Enchantment - Saga|||(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)$I -- Create a 2/1 green Spider creature token with reach.$II -- Put a +1/+1 counter on target creature you control. It becomes a legendary Spider Hero in addition to its other types.$III -- Target creature you control gains double strike until end of turn.| Peter Parker|Marvel's Spider-Man|10|M|{1}{W}|Legendary Creature - Human Scientist Hero|0|1|When Peter Parker enters, create a 2/1 green Spider creature token with reach.${1}{G}{W}{U}: Transform Peter Parker. Activate only as a sorcery.| Amazing Spider-Man|Marvel's Spider-Man|10|M|{1}{G}{W}{U}|Legendary Creature - Spider Human Hero|4|4|Vigilance, reach$Each legendary spell you cast that's one or more colors has web-slinging {G}{W}{U}.| Selfless Police Captain|Marvel's Spider-Man|12|C|{1}{W}|Creature - Human Detective|1|1|This creature enters with a +1/+1 counter on it.$When this creature leaves the battlefield, put its +1/+1 counters on target creature you control.| -Spectacular Spider-Man|Marvel's Spider-Man|14|R|{1}{W}|Legendary Creature - Spider Human Hero|3|2|Flash 1: Spectacular Spider-Man gains flying until end of turn. 1, Sacrifice Spectacular Spider-Man: Creatures you control gain hexproof and indestructible until end of turn.| +Spectacular Spider-Man|Marvel's Spider-Man|14|R|{1}{W}|Legendary Creature - Spider Human Hero|3|2|Flash${1}: Spectacular Spider-Man gains flying until end of turn.${1}, Sacrifice Spectacular Spider-Man: Creatures you control gain hexproof and indestructible until end of turn.| Spectacular Tactics|Marvel's Spider-Man|15|C|{1}{W}|Instant|||Choose one --$* Put a +1/+1 counter on target creature you control. It gains hexproof until end of turn.$* Destroy target creature with power 4 or greater.| Spider-Man, Web-Slinger|Marvel's Spider-Man|16|C|{2}{W}|Legendary Creature - Spider Human Hero|3|3|Web-slinging {W}| Starling, Aerial Ally|Marvel's Spider-Man|18|C|{4}{W}|Legendary Creature - Human Hero|3|4|Flying$When Starling enters, another target creature you control gains flying until end of turn.| @@ -59753,7 +59794,7 @@ Venom's Hunger|Marvel's Spider-Man|73|C|{4}{B}|Sorcery|||This spell costs {2} le Angry Rabble|Marvel's Spider-Man|75|C|{1}{R}|Creature - Human Citizen|2|2|Trample$Whenever you cast a spell with mana value 4 or greater, this creature deals 1 damage to each opponent.${5}{R}: Put two +1/+1 counters on this creature. Activate only as a sorcery.| Electro's Bolt|Marvel's Spider-Man|77|C|{2}{R}|Sorcery|||Electro's Bolt deals 4 damage to target creature.$Mayhem {1}{R}| Gwen Stacy|Marvel's Spider-Man|78|M|{1}{R}|Legendary Creature - Human Performer Hero|2|1|When Gwen Stacy enters, exile the top card of your library. You may play that card for as long as you control this creature.${2}{U}{R}{W}: Transform Gwen Stacy. Activate only as a sorcery.| -Ghost-Spider|Marvel's Spider-Man|78|M|{2}{U}{R}{W}|Legendary Creature - Spider Human Hero|2|1 Creature|Flying, vigilance, haste$Whenever you play a land from exile or cast a spell from exile, put a +1/+1 counter on Ghost-Spider.$Remove two counters from Ghost- Spider: Exile the top card of your library. You may play that card this turn.| +Ghost-Spider|Marvel's Spider-Man|78|M|{2}{U}{R}{W}|Legendary Creature - Spider Human Hero|4|4|Flying, vigilance, haste$Whenever you play a land from exile or cast a spell from exile, put a +1/+1 counter on Ghost-Spider.$Remove two counters from Ghost- Spider: Exile the top card of your library. You may play that card this turn.| Masked Meower|Marvel's Spider-Man|82|C|{R}|Creature - Spider Cat Hero|1|1|Haste$Discard a card, Sacrifice this creature: Draw a card.| Romantic Rendezvous|Marvel's Spider-Man|86|C|{1}{R}|Sorcery|||Discard a card, then draw two cards.| Shock|Marvel's Spider-Man|88|C|{R}|Instant|||Shock deals 2 damage to any target.| @@ -59767,6 +59808,7 @@ Grow Extra Arms|Marvel's Spider-Man|101|C|{1}{G}|Instant|||This spell costs {1} Guy in the Chair|Marvel's Spider-Man|102|C|{2}{G}|Creature - Human Advisor|2|3|{T}: Add one mana of any color.$Web Support -- {2}{G}, {T}: Put a +1/+1 counter on target Spider. Activate only as a sorcery.| Kapow!|Marvel's Spider-Man|103|C|{2}{G}|Sorcery|||Put a +1/+1 counter on target creature you control. It fights target creature an opponent controls.| Kraven's Cats|Marvel's Spider-Man|104|C|{1}{G}|Creature - Cat Villain|2|2|{2}{G}: This creature gets +2/+2 until end of turn. Activate only once each turn.| +Kraven's Last Hunt|Marvel's Spider-Man|105|R|{3}{G}|Enchantment - Saga|||(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)$I -- Mill five cards. When you do, this Saga deals damage equal to the greatest power among creature cards in your graveyard to target creature.$II -- Target creature you control gets +2/+2 until end of turn.$III -- Return target creature card from your graveyard to your hand.| Lurking Lizards|Marvel's Spider-Man|107|C|{1}{G}|Creature - Lizard Villain|1|3|Trample$Whenever you cast a spell with mana value 4 or greater, put a +1/+1 counter on this creature.| Miles Morales|Marvel's Spider-Man|108|M|{1}{G}|Legendary Creature - Human Citizen Hero|1|2|When Miles Morales enters, put a +1/+1 counter on each of up to two target creatures.${3}{R}{G}{W}: Transform Miles Morales. Activate only as a sorcery.| Ultimate Spider-Man|Marvel's Spider-Man|108|M|{3}{R}{G}{W}|Legendary Creature - Spider Human Hero|4|3|First strike, haste$Camouflage -- {2}: Put a +1/+1 counter on Ultimate Spider-Man. He gains hexproof and becomes colorless until end of turn.$Whenever you attack, double the number of each kind of counter on each Spider and legendary creature you control.| @@ -59799,7 +59841,7 @@ Miles Morales|Marvel's Spider-Man|200|M|{1}{G}|Legendary Creature - Human Citize Ultimate Spider-Man|Marvel's Spider-Man|200|M|{3}{R}{G}{W}|Legendary Creature - Spider Human Hero|4|3|First strike, haste$Camouflage -- {2}: Put a +1/+1 counter on Ultimate Spider-Man. He gains hexproof and becomes colorless until end of turn.$Whenever you attack, double the number of each kind of counter on each Spider and legendary creature you control.| Spider-Ham, Peter Porker|Marvel's Spider-Man|201|R|{1}{G}|Legendary Creature - Spider Boar Hero|2|2|When Spider-Ham enters, create a Food token.$Animal May-Ham -- Other Spiders, Boars, Bats, Bears, Birds, Cats, Dogs, Frogs, Jackals, Lizards, Mice, Otters, Rabbits, Raccoons, Rats, Squirrels, Turtles, and Wolves you control get +1/+1.| Gwen Stacy|Marvel's Spider-Man|202|M|{1}{R}|Legendary Creature - Human Performer Hero|2|1|When Gwen Stacy enters, exile the top card of your library. You may play that card for as long as you control this creature.${2}{U}{R}{W}: Transform Gwen Stacy. Activate only as a sorcery.| -Ghost-Spider|Marvel's Spider-Man|202|M|{2}{U}{R}{W}|Legendary Creature - Spider Human Hero|2|1 Creature|Flying, vigilance, haste$Whenever you play a land from exile or cast a spell from exile, put a +1/+1 counter on Ghost-Spider.$Remove two counters from Ghost- Spider: Exile the top card of your library. You may play that card this turn.| +Ghost-Spider|Marvel's Spider-Man|202|M|{2}{U}{R}{W}|Legendary Creature - Spider Human Hero|4|4|Flying, vigilance, haste$Whenever you play a land from exile or cast a spell from exile, put a +1/+1 counter on Ghost-Spider.$Remove two counters from Ghost- Spider: Exile the top card of your library. You may play that card this turn.| Web-Warriors|Marvel's Spider-Man|203|U|{4}{G/W}|Creature - Spider Hero|4|3|When this creature enters, put a +1/+1 counter on each other creature you control.| Spider-Man Noir|Marvel's Spider-Man|204|U|{4}{B}|Legendary Creature - Spider Human Hero|4|4|Menace$Whenever a creature you control attacks alone, put a +1/+1 counter on it. Then surveil X, where X is the number of counters on it.| Spider-Man 2099|Marvel's Spider-Man|205|R|{U}{R}|Legendary Creature - Spider Human Hero|2|3|From the Future -- You can't cast Spider-Man 2099 during your first, second, or third turns of the game.$Double strike, vigilance$At the beginning of your end step, if you've played a land or cast a spell this turn from anywhere other than your hand, Spider-Man 2099 deals damage equal to his power to any target.| @@ -59808,17 +59850,22 @@ Spider-Punk|Marvel's Spider-Man|207|R|{1}{R}|Legendary Creature - Spider Human H Peter Parker|Marvel's Spider-Man|208|M|{1}{W}|Legendary Creature - Human Scientist Hero|0|1|When Peter Parker enters, create a 2/1 green Spider creature token with reach.${1}{G}{W}{U}: Transform Peter Parker. Activate only as a sorcery.| Amazing Spider-Man|Marvel's Spider-Man|208|M|{1}{G}{W}{U}|Legendary Creature - Spider Human Hero|4|4|Vigilance, reach$Each legendary spell you cast that's one or more colors has web-slinging {G}{W}{U}.| Gwen Stacy|Marvel's Spider-Man|209|M|{1}{R}|Legendary Creature - Human Performer Hero|2|1|When Gwen Stacy enters, exile the top card of your library. You may play that card for as long as you control this creature.${2}{U}{R}{W}: Transform Gwen Stacy. Activate only as a sorcery.| -Ghost-Spider|Marvel's Spider-Man|209|M|{2}{U}{R}{W}|Legendary Creature - Spider Human Hero|2|1 Creature|Flying, vigilance, haste$Whenever you play a land from exile or cast a spell from exile, put a +1/+1 counter on Ghost-Spider.$Remove two counters from Ghost- Spider: Exile the top card of your library. You may play that card this turn.| +Ghost-Spider|Marvel's Spider-Man|209|M|{2}{U}{R}{W}|Legendary Creature - Spider Human Hero|4|4|Flying, vigilance, haste$Whenever you play a land from exile or cast a spell from exile, put a +1/+1 counter on Ghost-Spider.$Remove two counters from Ghost- Spider: Exile the top card of your library. You may play that card this turn.| Spider-Punk|Marvel's Spider-Man|210|R|{1}{R}|Legendary Creature - Spider Human Hero|2|1|Riot$Other Spiders you control have riot.$Spells and abilities can't be countered.$Damage can't be prevented.| Miles Morales|Marvel's Spider-Man|211|M|{1}{G}|Legendary Creature - Human Citizen Hero|1|2|When Miles Morales enters, put a +1/+1 counter on each of up to two target creatures.${3}{R}{G}{W}: Transform Miles Morales. Activate only as a sorcery.| Ultimate Spider-Man|Marvel's Spider-Man|211|M|{3}{R}{G}{W}|Legendary Creature - Spider Human Hero|4|3|First strike, haste$Camouflage -- {2}: Put a +1/+1 counter on Ultimate Spider-Man. He gains hexproof and becomes colorless until end of turn.$Whenever you attack, double the number of each kind of counter on each Spider and legendary creature you control.| Spider-Man 2099|Marvel's Spider-Man|216|R|{U}{R}|Legendary Creature - Spider Human Hero|2|3|From the Future -- You can't cast Spider-Man 2099 during your first, second, or third turns of the game.$Double strike, vigilance$At the beginning of your end step, if you've played a land or cast a spell this turn from anywhere other than your hand, Spider-Man 2099 deals damage equal to his power to any target.| Symbiote Spider-Man|Marvel's Spider-Man|217|R|{2}{U/B}|Legendary Creature - Symbiote Spider Hero|2|4|Whenever this creature deals combat damage to a player, look at that many cards from the top of your library. Put one of them into your hand and the rest into your graveyard.$Find New Host -- {2}{U/B}, Exile this card from your graveyard: Put a +1/+1 counter on target creature you control. It gains this card's other abilities. Activate only as a sorcery.| Origin of Spider-Man|Marvel's Spider-Man|218|R|{1}{W}|Enchantment - Saga|||(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)$I -- Create a 2/1 green Spider creature token with reach.$II -- Put a +1/+1 counter on target creature you control. It becomes a legendary Spider Hero in addition to its other types.$III -- Target creature you control gains double strike until end of turn.| +Kraven's Last Hunt|Marvel's Spider-Man|226|R|{3}{G}|Enchantment - Saga|||(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)$I -- Mill five cards. When you do, this Saga deals damage equal to the greatest power among creature cards in your graveyard to target creature.$II -- Target creature you control gets +2/+2 until end of turn.$III -- Return target creature card from your graveyard to your hand.| Doctor Octopus, Master Planner|Marvel's Spider-Man|228|M|{5}{U}{B}|Legendary Creature - Human Scientist Villain|4|8|Other Villains you control get +2/+2.$Your maximum hand size is eight.$At the beginning of your end step, if you have fewer than eight cards in hand, draw cards equal to the difference.| Peter Parker|Marvel's Spider-Man|232|M|{1}{W}|Legendary Creature - Human Scientist Hero|0|1|When Peter Parker enters, create a 2/1 green Spider creature token with reach.${1}{G}{W}{U}: Transform Peter Parker. Activate only as a sorcery.| Amazing Spider-Man|Marvel's Spider-Man|232|M|{1}{G}{W}{U}|Legendary Creature - Spider Human Hero|4|4|Vigilance, reach$Each legendary spell you cast that's one or more colors has web-slinging {G}{W}{U}.| Miles Morales|Marvel's Spider-Man|234|M|{1}{G}|Legendary Creature - Human Citizen Hero|1|2|When Miles Morales enters, put a +1/+1 counter on each of up to two target creatures.${3}{R}{G}{W}: Transform Miles Morales. Activate only as a sorcery.| Ultimate Spider-Man|Marvel's Spider-Man|234|M|{3}{R}{G}{W}|Legendary Creature - Spider Human Hero|4|3|First strike, haste$Camouflage -- {2}: Put a +1/+1 counter on Ultimate Spider-Man. He gains hexproof and becomes colorless until end of turn.$Whenever you attack, double the number of each kind of counter on each Spider and legendary creature you control.| -Anti-Venom, Horrifying Healer|Marvel's Spider-Man|244|M|{W}{W}{W}{W}{W}|Legendary Creature - Symbiote Hero|5|5|When Anti-Venom enters, if he was cast, return target creature card from your graveyard to the battlefield. If damage would be dealt to Anti-Venom, prevent that damage and put that many +1/+1 counters on him.| +Anti-Venom, Horrifying Healer|Marvel's Spider-Man|244|M|{W}{W}{W}{W}{W}|Legendary Creature - Symbiote Hero|5|5|When Anti-Venom enters, if he was cast, return target creature card from your graveyard to the battlefield.$If damage would be dealt to Anti-Venom, prevent that damage and put that many +1/+1 counters on him.| Iron Spider, Stark Upgrade|Marvel's Spider-Man|279|R|{3}|Legendary Artifact Creature - Spider Hero|2|3|Vigilance${T}: Put a +1/+1 counter on each artifact creature and/or Vehicle you control.${2}, Remove two +1/+1 counters from among artifacts you control: Draw a card.| +Aang, Airbending Master|Avatar: The Last Airbender Eternal|74|M|{4}{W}|Legendary Creature - Human Avatar Ally|4|4|When Aang enters, airbend another target creature.$Whenever one or more creatures you control leave the battlefield without dying, you get an experience counter.$At the beginning of your upkeep, create a 1/1 white Ally creature token for each experience counter you have.| +Katara, Waterbending Master|Avatar: The Last Airbender Eternal|93|M|{1}{U}|Legendary Creature - Human Warrior Ally|1|3|Whenever you cast a spell during an opponent's turn, you get an experience counter.$Whenever Katara attacks, you may draw a card for each experience counter you have. If you do, discard a card.| +Fire Lord Ozai|Avatar: The Last Airbender Eternal|104|M|{3}{B}|Legendary Creature - Human Noble|4|4|Whenever Fire Lord Ozai attacks, you may sacrifice another creature. If you do, add an amount of {R} equal to the sacrificed creature's power. Until end of combat, you don't lose this mana as steps end.${6}: Exile the top card of each opponent's library. Until end of turn, you may play one of those cards without paying its mana cost.| +The Cabbage Merchant|Avatar: The Last Airbender Eternal|134|R|{2}{G}|Legendary Creature - Human Citizen|2|2|Whenever an opponent casts a noncreature spell, create a Food token.$Whenever a creature deals combat damage to you, sacrifice a Food token.$Tap two untapped Foods you control: Add one mana of any color.| diff --git a/Utils/mtg-sets-data.txt b/Utils/mtg-sets-data.txt index 49164ecb554..df42f6ac90c 100644 --- a/Utils/mtg-sets-data.txt +++ b/Utils/mtg-sets-data.txt @@ -30,6 +30,7 @@ Antiquities|ATQ| Assassin's Creed|ACR| Avacyn Restored|AVR| Avatar: The Last Airbender|TLA| +Avatar: The Last Airbender Eternal|TLE| Battle for Zendikar|BFZ| Battlebond|BBD| Born of the Gods|BNG|