diff --git a/Mage.Client/pom.xml b/Mage.Client/pom.xml index cc198add2a8..c573fb21f68 100644 --- a/Mage.Client/pom.xml +++ b/Mage.Client/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage-client diff --git a/Mage.Client/src/main/java/mage/client/MageFrame.java b/Mage.Client/src/main/java/mage/client/MageFrame.java index cfcf25b24f2..16c710e2ac8 100644 --- a/Mage.Client/src/main/java/mage/client/MageFrame.java +++ b/Mage.Client/src/main/java/mage/client/MageFrame.java @@ -949,11 +949,15 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { } catch (SocketException ex) { } currentConnection.setUserIdStr(System.getProperty("user.name") + ":" + System.getProperty("os.name") + ":" + MagePreferences.getUserNames() + ":" + allMAC); - currentConnection.setProxyType(proxyType); - currentConnection.setProxyHost(proxyServer); - currentConnection.setProxyPort(proxyPort); - currentConnection.setProxyUsername(proxyUsername); - currentConnection.setProxyPassword(proxyPassword); + if (PreferencesDialog.NETWORK_ENABLE_PROXY_SUPPORT) { + currentConnection.setProxyType(proxyType); + currentConnection.setProxyHost(proxyServer); + currentConnection.setProxyPort(proxyPort); + currentConnection.setProxyUsername(proxyUsername); + currentConnection.setProxyPassword(proxyPassword); + } else { + currentConnection.setProxyType(ProxyType.NONE); + } setUserPrefsToConnection(currentConnection); } diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form index aeb23125b61..2fb5afdb761 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form @@ -3170,7 +3170,7 @@ - + diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java index 0b3da3c3277..94238a49468 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java @@ -55,6 +55,8 @@ public class PreferencesDialog extends javax.swing.JDialog { private static PreferencesDialog instance; // shared dialog instance + public static final boolean NETWORK_ENABLE_PROXY_SUPPORT = false; // TODO: delete proxy at all after few releases, 2025-02-09 + // WARNING, do not change const values - it must be same for compatibility with user's saved settings public static final String KEY_SHOW_TOOLTIPS_DELAY = "showTooltipsDelay"; public static final String KEY_SHOW_CARD_NAMES = "showCardNames"; @@ -2795,7 +2797,7 @@ public class PreferencesDialog extends javax.swing.JDialog { tabsPanel.addTab("Sounds", tabSounds); - connection_Proxy.setBorder(javax.swing.BorderFactory.createTitledBorder(javax.swing.BorderFactory.createEtchedBorder(), "Proxy for server connection and images download")); + connection_Proxy.setBorder(javax.swing.BorderFactory.createTitledBorder(javax.swing.BorderFactory.createEtchedBorder(), "Proxy for server connection and images download (DO NOT SUPPORTED)")); cbProxyType.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { @@ -3317,6 +3319,11 @@ public class PreferencesDialog extends javax.swing.JDialog { return; } + if (!NETWORK_ENABLE_PROXY_SUPPORT) { + connection.setProxyType(ProxyType.NONE); + return; + } + connection.setProxyType(configProxyType); if (configProxyType != ProxyType.NONE) { String host = getCachedValue(KEY_PROXY_ADDRESS, ""); 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 01a1baf1f32..f7f4f6ca7d1 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 @@ -479,6 +479,7 @@ public class ScryfallImageSupportCards { add("KLR"); // Kaladesh Remastered add("CMR"); // Commander Legends add("CC1"); // Commander Collection: Green + add("PJ21"); // Judge Gift Cards 2021 add("PL21"); // Year of the Ox 2021 add("KHM"); // Kaldheim add("KHC"); // Kaldheim Commander @@ -490,6 +491,7 @@ public class ScryfallImageSupportCards { add("C21"); // Commander 2021 add("MH2"); // Modern Horizons 2 add("H1R"); // Modern Horizons 1 Timeshifts + add("PW21"); // Wizards Play Network 2021 add("PLG21"); // Love Your LGS 2021 add("AFR"); // Adventures in the Forgotten Realms add("AFC"); // Forgotten Realms Commander @@ -499,12 +501,15 @@ public class ScryfallImageSupportCards { add("VOW"); // Innistrad: Crimson Vow add("VOC"); // Crimson Vow Commander add("YMID"); // Alchemy: Innistrad + add("P22"); // Judge Gift Cards 2022 add("DBL"); // Innistrad: Double Feature add("CC2"); // Commander Collection: Black add("NEO"); // Kamigawa: Neon Dynasty add("YNEO"); // Alchemy: Kamigawa add("NEC"); // Neon Dynasty Commander add("PL22"); // Year of the Tiger 2022 + add("PW22"); // Wizards Play Network 2022 + add("GDY"); // Game Day Promos add("SNC"); // Streets of New Capenna add("NCC"); // New Capenna Commander add("SLX"); // Universes Within @@ -522,6 +527,8 @@ public class ScryfallImageSupportCards { add("BOT"); // Transformers add("J22"); // Jumpstart 2022 add("SCD"); // Starter Commander Decks + add("PW23"); // Wizards Play Network 2023 + add("P23"); // Judge Gift Cards 2023 add("SLC"); // Secret Lair 30th Anniversary Countdown Kit add("DMR"); // Dominaria Remastered add("ONE"); // Phyrexia: All Will Be One @@ -547,6 +554,7 @@ public class ScryfallImageSupportCards { add("LCC"); // The The Lost Caverns of Ixalan Commander add("REX"); // Jurassic World Collection add("SPG"); // Special Guests + add("PW24"); // Wizards Play Network 2024 add("RVR"); // Ravnica Remastered add("PIP"); // Fallout add("MKM"); // Murders at Karlov Manor @@ -567,6 +575,7 @@ public class ScryfallImageSupportCards { add("FDN"); // Foundations add("J25"); // Foundations Jumpstart add("PIO"); // Pioneer Masters + add("PW25"); // Wizards Play Network 2025 add("INR"); // Innistrad Remastered add("DFT"); // Aetherdrift add("DRC"); // Aetherdrift Commander 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 b0851db1cd7..a5fe3be7c12 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 @@ -2555,6 +2555,38 @@ public class ScryfallImageSupportTokens { // H17 put("H17/Dragon", "https://api.scryfall.com/cards/h17/4/en?format=image"); + // INR + put("INR/Emblem Arlinn", "https://api.scryfall.com/cards/tinr/23/en?format=image"); + put("INR/Blood", "https://api.scryfall.com/cards/tinr/21/en?format=image"); + put("INR/Emblem Chandra", "https://api.scryfall.com/cards/tinr/24/en?format=image"); + put("INR/Clue", "https://api.scryfall.com/cards/tinr/22/en?format=image"); + put("INR/Demon", "https://api.scryfall.com/cards/tinr/6/en?format=image"); + put("INR/Eldrazi Horror", "https://api.scryfall.com/cards/tinr/1/en?format=image"); + put("INR/Elemental", "https://api.scryfall.com/cards/tinr/13/en?format=image"); + put("INR/Human/1", "https://api.scryfall.com/cards/tinr/14/en?format=image"); + put("INR/Human/2", "https://api.scryfall.com/cards/tinr/2/en?format=image"); + put("INR/Human Cleric", "https://api.scryfall.com/cards/tinr/19/en?format=image"); + put("INR/Human Soldier/1", "https://api.scryfall.com/cards/tinr/3/en?format=image"); + put("INR/Human Soldier/2", "https://api.scryfall.com/cards/tinr/20/en?format=image"); + put("INR/Human Wizard", "https://api.scryfall.com/cards/tinr/5/en?format=image"); + put("INR/Insect", "https://api.scryfall.com/cards/tinr/15/en?format=image"); + put("INR/Emblem Jace", "https://api.scryfall.com/cards/tinr/25/en?format=image"); + put("INR/Spider", "https://api.scryfall.com/cards/tinr/16/en?format=image"); + put("INR/Spirit", "https://api.scryfall.com/cards/tinr/4/en?format=image"); + put("INR/Emblem Tamiyo", "https://api.scryfall.com/cards/tinr/26/en?format=image"); + put("INR/Treefolk", "https://api.scryfall.com/cards/tinr/17/en?format=image"); + put("INR/Vampire/1", "https://api.scryfall.com/cards/tinr/7/en?format=image"); + put("INR/Vampire/2", "https://api.scryfall.com/cards/tinr/8/en?format=image"); + put("INR/Wolf/1", "https://api.scryfall.com/cards/tinr/9/en?format=image"); + put("INR/Wolf/2", "https://api.scryfall.com/cards/tinr/18/en?format=image"); + put("INR/Emblem Wrenn", "https://api.scryfall.com/cards/tinr/27/en?format=image"); + put("INR/Zombie/1", "https://api.scryfall.com/cards/tinr/12/en?format=image"); + put("INR/Zombie/2", "https://api.scryfall.com/cards/tinr/10/en?format=image"); + put("INR/Zombie/3", "https://api.scryfall.com/cards/tinr/11/en?format=image"); + + // DFT + put("DFT/Emblem Chandra", "https://api.scryfall.com/cards/tdft/13/en?format=image"); + // generate supported sets supportedSets.clear(); for (String cardName : this.keySet()) { 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 69bb5e382d2..f90f7e8df0a 100644 --- a/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java +++ b/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java @@ -13,6 +13,7 @@ import java.io.InputStream; /** * @author JayDi85 */ +@Ignore // TODO: too many fails due third party servers downtime, migrate to more stable resources or just run it manually public class DownloaderTest { @Test diff --git a/Mage.Common/pom.xml b/Mage.Common/pom.xml index e9248c50e18..2a0c7e1bf6a 100644 --- a/Mage.Common/pom.xml +++ b/Mage.Common/pom.xml @@ -7,7 +7,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage-common diff --git a/Mage.Common/src/main/java/mage/utils/MageVersion.java b/Mage.Common/src/main/java/mage/utils/MageVersion.java index 0cd7917d144..79dfcb49f43 100644 --- a/Mage.Common/src/main/java/mage/utils/MageVersion.java +++ b/Mage.Common/src/main/java/mage/utils/MageVersion.java @@ -17,7 +17,7 @@ public class MageVersion implements Serializable, Comparable { // * launcher gives priority to 1.4.48 instead 1.4.48-any-text, so don't use empty release info public static final int MAGE_VERSION_MAJOR = 1; public static final int MAGE_VERSION_MINOR = 4; - public static final int MAGE_VERSION_RELEASE = 55; + public static final int MAGE_VERSION_RELEASE = 56; public static final String MAGE_VERSION_RELEASE_INFO = "V3"; // V1, V1a, V1b for releases; V1-beta3, V1-beta4 for betas // strict mode diff --git a/Mage.Common/src/main/java/mage/view/CardView.java b/Mage.Common/src/main/java/mage/view/CardView.java index bc03a3d3633..c13f283c768 100644 --- a/Mage.Common/src/main/java/mage/view/CardView.java +++ b/Mage.Common/src/main/java/mage/view/CardView.java @@ -944,7 +944,7 @@ public class CardView extends SimpleCardView { this(true); this.gameObject = true; this.id = designation.getId(); - this.mageObjectType = MageObjectType.NULL; + this.mageObjectType = MageObjectType.DESIGNATION; this.name = designation.getName(); this.displayName = name; this.displayFullName = name; @@ -955,9 +955,8 @@ public class CardView extends SimpleCardView { this.frameStyle = FrameStyle.M15_NORMAL; this.cardNumber = designation.getCardNumber(); this.expansionSetCode = designation.getExpansionSetCode(); - this.cardNumber = ""; - this.imageFileName = ""; - this.imageNumber = 0; + this.imageFileName = designation.getImageFileName(); + this.imageNumber = designation.getImageNumber(); this.rarity = Rarity.SPECIAL; // no playable/chooseable marks for designations diff --git a/Mage.Common/src/main/java/mage/view/GameView.java b/Mage.Common/src/main/java/mage/view/GameView.java index b55f4ea8c37..59d47955831 100644 --- a/Mage.Common/src/main/java/mage/view/GameView.java +++ b/Mage.Common/src/main/java/mage/view/GameView.java @@ -62,7 +62,11 @@ public class GameView implements Serializable { private final int turn; private boolean special = false; private final boolean rollbackTurnsAllowed; + + // for debug only + // TODO: implement and support in admin tools private int totalErrorsCount; + private int totalEffectsCount; public GameView(GameState state, Game game, UUID createdForPlayerId, UUID watcherUserId) { Player createdForPlayer = null; @@ -209,6 +213,7 @@ public class GameView implements Serializable { } this.rollbackTurnsAllowed = game.getOptions().rollbackTurnsAllowed; this.totalErrorsCount = game.getTotalErrorsCount(); + this.totalEffectsCount = game.getTotalEffectsCount(); } private void checkPaid(UUID uuid, StackAbility stackAbility) { @@ -349,4 +354,8 @@ public class GameView implements Serializable { public int getTotalErrorsCount() { return this.totalErrorsCount; } + + public int getTotalEffectsCount() { + return this.totalEffectsCount; + } } diff --git a/Mage.Common/src/main/java/mage/view/RevealedView.java b/Mage.Common/src/main/java/mage/view/RevealedView.java index 46c34b216d3..2a009dc7fa0 100644 --- a/Mage.Common/src/main/java/mage/view/RevealedView.java +++ b/Mage.Common/src/main/java/mage/view/RevealedView.java @@ -7,6 +7,7 @@ import java.io.Serializable; import mage.cards.Card; import mage.cards.Cards; import mage.game.Game; +import mage.game.permanent.PermanentCard; /** * @author BetaSteward_at_googlemail.com @@ -19,7 +20,11 @@ public class RevealedView implements Serializable { public RevealedView(String name, Cards cards, Game game) { this.name = name; for (Card card : cards.getCards(game)) { - this.cards.put(card.getId(), new CardView(card, game)); + if (card instanceof PermanentCard && card.isFaceDown(game)) { + this.cards.put(card.getId(), new CardView(card.getMainCard())); // do not use game param, so it will take default card + } else { + this.cards.put(card.getId(), new CardView(card, game)); + } } } diff --git a/Mage.Plugins/Mage.Counter.Plugin/pom.xml b/Mage.Plugins/Mage.Counter.Plugin/pom.xml index 0ebcfdd8bc3..54eba397b58 100644 --- a/Mage.Plugins/Mage.Counter.Plugin/pom.xml +++ b/Mage.Plugins/Mage.Counter.Plugin/pom.xml @@ -7,7 +7,7 @@ org.mage mage-plugins - 1.4.55 + 1.4.56 mage-counter-plugin diff --git a/Mage.Plugins/pom.xml b/Mage.Plugins/pom.xml index 0e2b4f6138f..b3d08009be2 100644 --- a/Mage.Plugins/pom.xml +++ b/Mage.Plugins/pom.xml @@ -7,7 +7,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage-plugins diff --git a/Mage.Reports/pom.xml b/Mage.Reports/pom.xml index f251298eced..2e98574191d 100644 --- a/Mage.Reports/pom.xml +++ b/Mage.Reports/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage-reports diff --git a/Mage.Server.Console/pom.xml b/Mage.Server.Console/pom.xml index f7407a80156..a2192534a39 100644 --- a/Mage.Server.Console/pom.xml +++ b/Mage.Server.Console/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage-server-console diff --git a/Mage.Server.Console/src/main/java/mage/server/console/ConnectDialog.java b/Mage.Server.Console/src/main/java/mage/server/console/ConnectDialog.java index b0046e18c54..d2bb68b4c70 100644 --- a/Mage.Server.Console/src/main/java/mage/server/console/ConnectDialog.java +++ b/Mage.Server.Console/src/main/java/mage/server/console/ConnectDialog.java @@ -365,12 +365,17 @@ public class ConnectDialog extends JDialog { connection.setPort(Integer.parseInt(this.txtPort.getText())); connection.setAdminPassword(new String(txtPassword.getPassword())); connection.setUsername(SessionImpl.ADMIN_NAME); - connection.setProxyType((ProxyType) this.cbProxyType.getSelectedItem()); - if (!this.cbProxyType.getSelectedItem().equals(ProxyType.NONE)) { - connection.setProxyHost(this.txtProxyServer.getText()); - connection.setProxyPort(Integer.parseInt(this.txtProxyPort.getText())); - connection.setProxyUsername(this.txtProxyUserName.getText()); - connection.setProxyPassword(new String(this.txtPasswordField.getPassword())); + + if (false) { // TODO: delete proxy at all after few releases, 2025-02-09 + connection.setProxyType((ProxyType) this.cbProxyType.getSelectedItem()); + if (!this.cbProxyType.getSelectedItem().equals(ProxyType.NONE)) { + connection.setProxyHost(this.txtProxyServer.getText()); + connection.setProxyPort(Integer.parseInt(this.txtProxyPort.getText())); + connection.setProxyUsername(this.txtProxyUserName.getText()); + connection.setProxyPassword(new String(this.txtPasswordField.getPassword())); + } + } else { + connection.setProxyType(ProxyType.NONE); } logger.debug("connecting: " + connection.getProxyType() + ' ' + connection.getProxyHost() + ' ' + connection.getProxyPort()); diff --git a/Mage.Server.Console/src/main/java/mage/server/console/ConsoleFrame.java b/Mage.Server.Console/src/main/java/mage/server/console/ConsoleFrame.java index 338b6761e25..3ad175c942a 100644 --- a/Mage.Server.Console/src/main/java/mage/server/console/ConsoleFrame.java +++ b/Mage.Server.Console/src/main/java/mage/server/console/ConsoleFrame.java @@ -101,13 +101,18 @@ public class ConsoleFrame extends javax.swing.JFrame implements MageClient { newConnection.setPort(ConsoleFrame.getPreferences().getInt("serverPort", 17171)); newConnection.setUsername(SessionImpl.ADMIN_NAME); newConnection.setAdminPassword(ConsoleFrame.getPreferences().get("password", "")); - newConnection.setProxyType(Connection.ProxyType.valueOf(ConsoleFrame.getPreferences().get("proxyType", "NONE").toUpperCase(Locale.ENGLISH))); - if (!newConnection.getProxyType().equals(Connection.ProxyType.NONE)) { - newConnection.setProxyHost(ConsoleFrame.getPreferences().get("proxyAddress", "")); - newConnection.setProxyPort(ConsoleFrame.getPreferences().getInt("proxyPort", 0)); - newConnection.setProxyUsername(ConsoleFrame.getPreferences().get("proxyUsername", "")); - newConnection.setProxyPassword(ConsoleFrame.getPreferences().get("proxyPassword", "")); + if (false) { // TODO: delete proxy at all after few releases, 2025-02-09 + newConnection.setProxyType(Connection.ProxyType.valueOf(ConsoleFrame.getPreferences().get("proxyType", "NONE").toUpperCase(Locale.ENGLISH))); + if (!newConnection.getProxyType().equals(Connection.ProxyType.NONE)) { + newConnection.setProxyHost(ConsoleFrame.getPreferences().get("proxyAddress", "")); + newConnection.setProxyPort(ConsoleFrame.getPreferences().getInt("proxyPort", 0)); + newConnection.setProxyUsername(ConsoleFrame.getPreferences().get("proxyUsername", "")); + newConnection.setProxyPassword(ConsoleFrame.getPreferences().get("proxyPassword", "")); + } + } else { + newConnection.setProxyType(Connection.ProxyType.NONE); } + status = connect(newConnection); } return status; diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/pom.xml b/Mage.Server.Plugins/Mage.Deck.Constructed/pom.xml index 6b0ba470c9e..57d5a53b0f1 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/pom.xml +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-deck-constructed diff --git a/Mage.Server.Plugins/Mage.Deck.Limited/pom.xml b/Mage.Server.Plugins/Mage.Deck.Limited/pom.xml index 6093f196f59..3d53e554370 100644 --- a/Mage.Server.Plugins/Mage.Deck.Limited/pom.xml +++ b/Mage.Server.Plugins/Mage.Deck.Limited/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-deck-limited diff --git a/Mage.Server.Plugins/Mage.Game.BrawlDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.BrawlDuel/pom.xml index de808e4f9c2..b865a976eaa 100644 --- a/Mage.Server.Plugins/Mage.Game.BrawlDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.BrawlDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-brawlduel diff --git a/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/pom.xml index 23594520bd6..eb5c5913080 100644 --- a/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/pom.xml @@ -6,7 +6,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-brawlfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.CanadianHighlanderDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.CanadianHighlanderDuel/pom.xml index eb7288a4ef4..d9defa0ebad 100644 --- a/Mage.Server.Plugins/Mage.Game.CanadianHighlanderDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.CanadianHighlanderDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-canadianhighlanderduel diff --git a/Mage.Server.Plugins/Mage.Game.CommanderDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.CommanderDuel/pom.xml index 7d6919218ed..55d69cf5c38 100644 --- a/Mage.Server.Plugins/Mage.Game.CommanderDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.CommanderDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-commanderduel diff --git a/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/pom.xml index 5fd2deec0af..f5c7b42c0ca 100644 --- a/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/pom.xml @@ -6,7 +6,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-commanderfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.CustomPillarOfTheParunsDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.CustomPillarOfTheParunsDuel/pom.xml index 190112dc919..f2986e22ff7 100644 --- a/Mage.Server.Plugins/Mage.Game.CustomPillarOfTheParunsDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.CustomPillarOfTheParunsDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-custompillaroftheparunsduel diff --git a/Mage.Server.Plugins/Mage.Game.FreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.FreeForAll/pom.xml index de8f60e544e..5a9acb210ec 100644 --- a/Mage.Server.Plugins/Mage.Game.FreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.FreeForAll/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-freeforall diff --git a/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/pom.xml index a9dec0c425e..d68adb9f8a3 100644 --- a/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-freeformcommanderduel diff --git a/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/pom.xml index 0045b6ea026..1c0016007c7 100644 --- a/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/pom.xml @@ -6,7 +6,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-freeformcommanderfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.FreeformUnlimitedCommander/pom.xml b/Mage.Server.Plugins/Mage.Game.FreeformUnlimitedCommander/pom.xml index 9d091c25637..e0fc0dbc6ad 100644 --- a/Mage.Server.Plugins/Mage.Game.FreeformUnlimitedCommander/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.FreeformUnlimitedCommander/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-freeformunlimitedcommander @@ -23,7 +23,7 @@ org.mage mage-game-freeformcommanderfreeforall - 1.4.55 + 1.4.56 compile diff --git a/Mage.Server.Plugins/Mage.Game.MomirDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.MomirDuel/pom.xml index 23aaef8f534..dce83069cad 100644 --- a/Mage.Server.Plugins/Mage.Game.MomirDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.MomirDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-momirduel diff --git a/Mage.Server.Plugins/Mage.Game.MomirGame/pom.xml b/Mage.Server.Plugins/Mage.Game.MomirGame/pom.xml index 4e403e3f4cd..0451da80628 100644 --- a/Mage.Server.Plugins/Mage.Game.MomirGame/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.MomirGame/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-momirfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/pom.xml index 8a79934e501..1c1fc8cf0cc 100644 --- a/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/pom.xml @@ -6,7 +6,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-oathbreakerduel @@ -22,7 +22,7 @@ org.mage mage-game-oathbreakerfreeforall - 1.4.55 + 1.4.56 compile diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml index 6a906431afd..a87f4e138ad 100644 --- a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-oathbreakerfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/pom.xml index bc221cf10e3..9e1365bbe12 100644 --- a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/pom.xml @@ -6,7 +6,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-pennydreadfulcommanderfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.TinyLeadersDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.TinyLeadersDuel/pom.xml index ef4ea9bea06..bf33a290fbc 100644 --- a/Mage.Server.Plugins/Mage.Game.TinyLeadersDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.TinyLeadersDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-tinyleadersduel diff --git a/Mage.Server.Plugins/Mage.Game.TwoPlayerDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.TwoPlayerDuel/pom.xml index 35664806139..8a0b37c4bdc 100644 --- a/Mage.Server.Plugins/Mage.Game.TwoPlayerDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.TwoPlayerDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-game-twoplayerduel diff --git a/Mage.Server.Plugins/Mage.Player.AI.DraftBot/pom.xml b/Mage.Server.Plugins/Mage.Player.AI.DraftBot/pom.xml index 8afec9cdd1c..4599885dfe9 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.DraftBot/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AI.DraftBot/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-player-ai-draftbot diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/pom.xml b/Mage.Server.Plugins/Mage.Player.AI.MA/pom.xml index 9ae6ceb0bd5..dc71986f494 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-player-ai-ma 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 c2528aaa343..c0447fa59ca 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 @@ -155,7 +155,7 @@ public class ComputerPlayer6 extends ComputerPlayer { + (p.isTapped() ? ",tapped" : "") + (p.isAttacking() ? ",attacking" : "") + (p.getBlocking() > 0 ? ",blocking" : "") - + ":" + GameStateEvaluator2.evaluatePermanent(p, game)) + + ":" + GameStateEvaluator2.evaluatePermanent(p, game, true)) .collect(Collectors.joining("; ")); sb.append("-> Permanents: [").append(ownPermanentsInfo).append("]"); logger.info(sb.toString()); diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java index a1bd3db6dc0..3b6fa38d229 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java @@ -26,6 +26,10 @@ public final class GameStateEvaluator2 { public static final int HAND_CARD_SCORE = 5; public static PlayerEvaluateScore evaluate(UUID playerId, Game game) { + return evaluate(playerId, game, true); + } + + public static PlayerEvaluateScore evaluate(UUID playerId, Game game, boolean useCombatPermanentScore) { // TODO: add multi opponents support, so AI can take better actions Player player = game.getPlayer(playerId); Player opponent = game.getPlayer(game.getOpponents(playerId).stream().findFirst().orElse(null)); @@ -63,7 +67,7 @@ public final class GameStateEvaluator2 { // add values of player for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { - int onePermScore = evaluatePermanent(permanent, game); + int onePermScore = evaluatePermanent(permanent, game, useCombatPermanentScore); playerPermanentsScore += onePermScore; if (logger.isDebugEnabled()) { sbPlayer.append(permanent.getName()).append('[').append(onePermScore).append("] "); @@ -77,7 +81,7 @@ public final class GameStateEvaluator2 { // add values of opponent for (Permanent permanent : game.getBattlefield().getAllActivePermanents(opponent.getId())) { - int onePermScore = evaluatePermanent(permanent, game); + int onePermScore = evaluatePermanent(permanent, game, useCombatPermanentScore); opponentPermanentsScore += onePermScore; if (logger.isDebugEnabled()) { sbOpponent.append(permanent.getName()).append('[').append(onePermScore).append("] "); @@ -121,7 +125,7 @@ public final class GameStateEvaluator2 { opponentLifeScore, opponentHandScore, opponentPermanentsScore); } - public static int evaluatePermanent(Permanent permanent, Game game) { + public static int evaluatePermanent(Permanent permanent, Game game, boolean useCombatPermanentScore) { // prevent AI from attaching bad auras to its own permanents ex: Brainwash and Demonic Torment (no immediate penalty on the battlefield) int value = 0; if (!permanent.getAttachments().isEmpty()) { @@ -137,14 +141,11 @@ public final class GameStateEvaluator2 { } } } - value += ArtificialScoringSystem.getFixedPermanentScore(game, permanent) - + ArtificialScoringSystem.getVariablePermanentScore(game, permanent); - return value; - } - - public static int evaluateCreature(Permanent creature, Game game) { - int value = ArtificialScoringSystem.getFixedPermanentScore(game, creature) - + ArtificialScoringSystem.getVariablePermanentScore(game, creature); + value += ArtificialScoringSystem.getFixedPermanentScore(game, permanent); + value += ArtificialScoringSystem.getDynamicPermanentScore(game, permanent); + if (useCombatPermanentScore) { + value += ArtificialScoringSystem.getCombatPermanentScore(game, permanent); + } return value; } 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 55113d1e99e..2ad894e1a8c 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 @@ -346,7 +346,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer { logger.debug("simulating -- node #:" + SimulationNode2.getCount() + " triggered ability option"); for (Target target : ability.getTargets()) { for (UUID targetId : target.getTargets()) { - newNode.getTargets().add(targetId); + newNode.getTargets().add(targetId); // save for info only (real targets in newNode.ability already) } } parent.children.add(newNode); diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java index 78b47598bd5..35ededd0de4 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/ArtificialScoringSystem.java @@ -63,14 +63,11 @@ public final class ArtificialScoringSystem { return score; } - public static int getVariablePermanentScore(final Game game, final Permanent permanent) { + public static int getDynamicPermanentScore(final Game game, final Permanent permanent) { int score = permanent.getCounters(game).getCount(CounterType.CHARGE) * 30; score += permanent.getCounters(game).getCount(CounterType.LEVEL) * 30; score -= permanent.getDamage() * 2; - if (!canTap(game, permanent)) { - score += getTappedScore(game, permanent); - } if (permanent.getCardType(game).contains(CardType.CREATURE)) { final int power = permanent.getPower().getValue(); final int toughness = permanent.getToughness().getValue(); @@ -95,11 +92,19 @@ public final class ArtificialScoringSystem { } } score += equipments + enchantments; + } + return score; + } + public static int getCombatPermanentScore(final Game game, final Permanent permanent) { + int score = 0; + if (!canTap(game, permanent)) { + score += getTappedScore(game, permanent); + } + if (permanent.getCardType(game).contains(CardType.CREATURE)) { if (!permanent.canAttack(null, game)) { score -= 100; } - if (!permanent.canBlockAny(game)) { score -= 30; } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatInfo.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatInfo.java index f77f38df7fe..3b80dc0a38b 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatInfo.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatInfo.java @@ -15,11 +15,7 @@ public class CombatInfo { private Map> combat = new HashMap<>(); public void addPair(Permanent attacker, Permanent blocker) { - List blockers = combat.get(attacker); - if (blockers == null) { - blockers = new ArrayList<>(); - combat.put(attacker, blockers); - } + List blockers = combat.computeIfAbsent(attacker, k -> new ArrayList<>()); blockers.add(blocker); } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java index 5a3f5bc8d98..873b4c5bbcd 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java @@ -1,15 +1,19 @@ package mage.player.ai.util; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.keyword.DoubleStrikeAbility; import mage.abilities.keyword.InfectAbility; import mage.counters.CounterType; import mage.game.Game; import mage.game.combat.Combat; +import mage.game.combat.CombatGroup; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.game.turn.CombatDamageStep; import mage.game.turn.EndOfCombatStep; import mage.game.turn.Step; +import mage.player.ai.GameStateEvaluator2; import mage.players.Player; import org.apache.log4j.Logger; @@ -89,12 +93,22 @@ public final class CombatUtil { } } - public static Permanent getWorstCreature(List creatures) { - if (creatures.isEmpty()) { - return null; + public static Permanent getWorstCreature(List... lists) { + for (List list : lists) { + if (!list.isEmpty()) { + list.sort(Comparator.comparingInt(p -> p.getPower().getValue())); + return list.get(0); + } + } + return null; + } + + public static void removeWorstCreature(Permanent permanent, List... lists) { + for (List list : lists) { + if (!list.isEmpty()) { + list.remove(permanent); + } } - creatures.sort(Comparator.comparingInt(p -> p.getPower().getValue())); - return creatures.get(0); } private static int sumDamage(List attackersThatWontBeBlocked, Player defender) { @@ -144,25 +158,100 @@ public final class CombatUtil { return canBlock; } - public static CombatInfo blockWithGoodTrade(Game game, List attackers, List blockers) { - + /** + * AI related code, find better block combination for attackers + */ + public static CombatInfo blockWithGoodTrade2(Game game, List attackers, List blockers) { UUID attackerId = game.getCombat().getAttackingPlayerId(); UUID defenderId = game.getCombat().getDefenders().iterator().next(); if (attackerId == null || defenderId == null) { - log.warn("Couldn't find attacker or defender: " + attackerId + ' ' + defenderId); return new CombatInfo(); } + // TODO: implement full game simulations of all possible combinations (e.g. multiblockers support) + CombatInfo combatInfo = new CombatInfo(); for (Permanent attacker : attackers) { - //TODO: handle attackers with "can't be blocked except" - List possibleBlockers = getPossibleBlockers(game, attacker, blockers); - List survivedBlockers = getBlockersThatWillSurvive(game, attackerId, defenderId, attacker, possibleBlockers); - if (!survivedBlockers.isEmpty()) { - Permanent blocker = getWorstCreature(survivedBlockers); + // simple combat simulation (1 vs 1) + List allBlockers = getPossibleBlockers(game, attacker, blockers); + List blockerStats = getBlockersThatWillSurvive2(game, attackerId, defenderId, attacker, allBlockers); + Map blockingDiffScore = new HashMap<>(); + Map nonBlockingDiffScore = new HashMap<>(); + blockerStats.forEach(s -> { + blockingDiffScore.put(s.getBlocker(), s.getDiffBlockingScore()); + nonBlockingDiffScore.put(s.getBlocker(), s.getDiffNonblockingScore()); + }); + + // split blockers by usage priority + List survivedAndKillBlocker = new ArrayList<>(); + List survivedBlockers = new ArrayList<>(); + List diedBlockers = new ArrayList<>(); + blockerStats.forEach(stats -> { + if (stats.isAttackerDied() && !stats.isBlockerDied()) { + survivedAndKillBlocker.add(stats.getBlocker()); + } else if (!stats.isBlockerDied()) { + survivedBlockers.add(stats.getBlocker()); + } else { + diedBlockers.add(stats.getBlocker()); + } + }); + + int blockedCount = 0; + + // find good blocker + Permanent blocker = getWorstCreature(survivedAndKillBlocker, survivedBlockers); + if (blocker != null) { combatInfo.addPair(attacker, blocker); - blockers.remove(blocker); + removeWorstCreature(blocker, blockers, survivedAndKillBlocker, survivedBlockers); + blockedCount++; } + + // find good sacrifices (chump blocks also supported due bad game score on loose) + // TODO: add chump blocking support here? + // TODO: there are many triggers on damage, attack, etc - it can't be processed without real game simulations + if (blocker == null) { + blocker = getWorstCreature(diedBlockers); + if (blocker != null) { + int diffBlockingScore = blockingDiffScore.getOrDefault(blocker, 0); + int diffNonBlockingScore = nonBlockingDiffScore.getOrDefault(blocker, 0); + if (diffBlockingScore >= 0 || diffBlockingScore > diffNonBlockingScore) { + // it's good - can sacrifice and get better game state, also protect from game loose + combatInfo.addPair(attacker, blocker); + removeWorstCreature(blocker, blockers, diedBlockers); + blockedCount++; + } + } + } + + // find blockers for restrictions + while (true) { + if (blockers.isEmpty()) { + break; + } + + // TODO: add multiple use case support with min/max blockedBy conditional and other + // see all possible use cases in checkBlockRestrictions, checkBlockRequirementsAfter and checkBlockRestrictionsAfter + + // effects support: can't be blocked except by xxx or more creatures + if (blockedCount > 0 && attacker.getMinBlockedBy() > blockedCount) { + // it already has 1 blocker (killer in best use case), so no needs in second killer + blocker = getWorstCreature(survivedBlockers, survivedAndKillBlocker, diedBlockers); + if (blocker != null) { + combatInfo.addPair(attacker, blocker); + removeWorstCreature(blocker, blockers, survivedBlockers, survivedAndKillBlocker, diedBlockers); + blockedCount++; + continue; // try to find next required blocker + } else { + // invalid configuration, must stop + break; + } + } + + // no more active restrictions + break; + } + + // no more blockers to use if (blockers.isEmpty()) { break; } @@ -171,40 +260,43 @@ public final class CombatUtil { return combatInfo; } - private static List getBlockersThatWillSurvive(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List possibleBlockers) { - List blockers = new ArrayList<>(); + /** + * Game simulations to find all survived/killer blocker + */ + private static List getBlockersThatWillSurvive2(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List possibleBlockers) { + List res = new ArrayList<>(); for (Permanent blocker : possibleBlockers) { - SurviveInfo info = willItSurvive(game, attackerId, defenderId, attacker, blocker); - //if (info.isAttackerDied() && !info.isBlockerDied()) { - if (info != null) { - if (info.isAttackerDied()) { - blockers.add(blocker); - } else if (!info.isBlockerDied()) { - blockers.add(blocker); - } + // TODO: enable willItSurviveSimulation and check stability + SurviveInfo info = willItSurviveSimple(game, attackerId, defenderId, attacker, blocker); + if (info == null) { + continue; } + info.setBlocker(blocker); + res.add(info); } - return blockers; + return res; } - /** - * @deprecated TODO: unused, can be deleted? - */ - public static SurviveInfo willItSurvive(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) { - Game sim = game.createSimulationForAI(); - - // TODO: bugged, miss combat.clear code (possible bugs - wrong blocker declare by AI on multiple options?) - Combat combat = sim.getCombat(); - combat.setAttacker(attackingPlayerId); - combat.setDefenders(sim); - + public static SurviveInfo willItSurviveSimulation(Game originalGame, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) { + Game sim = originalGame.createSimulationForAI(); if (blocker == null || attacker == null || sim.getPlayer(defendingPlayerId) == null) { return null; } + // TODO: need code research, possible bugs in miss prepare code due real combat logic + // TODO: bugged, miss combat.clear code (possible bugs - wrong blocker declare by AI on multiple options?) + Combat combat = sim.getCombat(); + combat.setAttacker(attackingPlayerId); + combat.setDefenders(sim); + int startScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim).getTotalScore(); + + // real game simulation + // TODO: need debug and testing, old code from 2012 + // must have infinite/freeze protection (e.g. limit stack resolves) + + // declare sim.getPlayer(defendingPlayerId).declareBlocker(defendingPlayerId, blocker.getId(), attacker.getId(), sim); sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, defendingPlayerId, defendingPlayerId)); - sim.checkStateAndTriggered(); while (!sim.getStack().isEmpty()) { sim.getStack().resolve(sim); @@ -212,109 +304,119 @@ public final class CombatUtil { } sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId())); + // combat simulateStep(sim, new CombatDamageStep(true)); simulateStep(sim, new CombatDamageStep(false)); simulateStep(sim, new EndOfCombatStep()); - // The following commented out call produces random freezes. - //sim.checkStateAndTriggered(); + + // after + sim.checkStateAndTriggered(); while (!sim.getStack().isEmpty()) { sim.getStack().resolve(sim); sim.applyEffects(); } - return new SurviveInfo(!sim.getBattlefield().containsPermanent(attacker.getId()), !sim.getBattlefield().containsPermanent(blocker.getId())); + int endBlockingScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim).getTotalScore(); + int endNonBlockingScore = startScore; // TODO: implement + return new SurviveInfo( + !sim.getBattlefield().containsPermanent(attacker.getId()), + !sim.getBattlefield().containsPermanent(blocker.getId()), + endBlockingScore - startScore, + endNonBlockingScore - startScore + ); } - protected static void simulateStep(Game game, Step step) { - game.getPhase().setStep(step); - if (!step.skipStep(game, game.getActivePlayerId())) { - step.beginStep(game, game.getActivePlayerId()); - // The following commented out call produces random freezes. - //game.checkStateAndTriggered(); - while (!game.getStack().isEmpty()) { - game.getStack().resolve(game); - game.applyEffects(); - } - step.endStep(game, game.getActivePlayerId()); - } - } - - public static boolean canBlock(Game game, Permanent blocker) { - boolean canBlock = true; - if (!blocker.isTapped()) { - try { - canBlock = blocker.canBlock(null, game); - } catch (Exception e) { - //e.printStackTrace(); - } - } - return canBlock; - } - - public static CombatInfo blockWithGoodTrade2(Game game, List attackers, List blockers) { - - UUID attackerId = game.getCombat().getAttackingPlayerId(); - UUID defenderId = game.getCombat().getDefenders().iterator().next(); - if (attackerId == null || defenderId == null) { - log.warn("Couldn't find attacker or defender: " + attackerId + ' ' + defenderId); - return new CombatInfo(); - } - - CombatInfo combatInfo = new CombatInfo(); - for (Permanent attacker : attackers) { - //TODO: handle attackers with "can't be blocked except" - List possibleBlockers = getPossibleBlockers(game, attacker, blockers); - List survivedBlockers = getBlockersThatWillSurvive2(game, attackerId, defenderId, attacker, possibleBlockers); - if (!survivedBlockers.isEmpty()) { - Permanent blocker = getWorstCreature(survivedBlockers); - combatInfo.addPair(attacker, blocker); - blockers.remove(blocker); - } - if (blockers.isEmpty()) { - break; - } - } - - return combatInfo; - } - - private static List getBlockersThatWillSurvive2(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List possibleBlockers) { - List blockers = new ArrayList<>(); - for (Permanent blocker : possibleBlockers) { - SurviveInfo info = willItSurvive2(game, attackerId, defenderId, attacker, blocker); - //if (info.isAttackerDied() && !info.isBlockerDied()) { - if (info != null) { - if (info.isAttackerDied()) { - blockers.add(blocker); - } else if (!info.isBlockerDied()) { - blockers.add(blocker); - } - } - } - return blockers; - } - - public static SurviveInfo willItSurvive2(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) { - - Game sim = game.createSimulationForAI(); - - // TODO: bugged, miss combat.clear code (possible bugs - wrong blocker declare by AI on multiple options?) - Combat combat = sim.getCombat(); - combat.setAttacker(attackingPlayerId); - combat.setDefenders(sim); - + public static SurviveInfo willItSurviveSimple(Game originalGame, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) { + Game sim = originalGame.createSimulationForAI(); if (blocker == null || attacker == null || sim.getPlayer(defendingPlayerId) == null) { return null; } + Combat combat = sim.getCombat(); + combat.setAttacker(attackingPlayerId); + combat.setDefenders(sim); + + Game simNonBlocking = sim.copy(); + + // attacker tapped before attack, it will add additional score to blocker, but it must be ignored + // so blocker will block same creature with same score without penalty + int startScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim, false).getTotalScore(); + + // fake combat simulation (simple damage simulation) + Permanent simAttacker = sim.getPermanent(attacker.getId()); + Permanent simBlocker = sim.getPermanent(blocker.getId()); + if (simAttacker == null || simBlocker == null) { + throw new IllegalArgumentException("Broken sim game, can't find attacker or blocker"); + } + // don't ask about that hacks - just replace to real combat simulation someday (another hack but with full stack resolve) + // first damage step + simulateCombatDamage(sim, simBlocker, simAttacker, true); + simulateCombatDamage(sim, simAttacker, simBlocker, true); + simAttacker.applyDamage(sim); + simBlocker.applyDamage(sim); + sim.checkStateAndTriggered(); + sim.processAction(); + // second damage step + if (sim.getPermanent(simBlocker.getId()) != null && sim.getPermanent(simAttacker.getId()) != null) { + simulateCombatDamage(sim, simBlocker, simAttacker, false); + simulateCombatDamage(sim, simAttacker, simBlocker, false); + simAttacker.applyDamage(sim); + simBlocker.applyDamage(sim); + sim.checkStateAndTriggered(); + sim.processAction(); + } + + /* old manual PT compare if (attacker.getPower().getValue() >= blocker.getToughness().getValue()) { sim.getBattlefield().removePermanent(blocker.getId()); } if (attacker.getToughness().getValue() <= blocker.getPower().getValue()) { sim.getBattlefield().removePermanent(attacker.getId()); } + */ - return new SurviveInfo(!sim.getBattlefield().containsPermanent(attacker.getId()), !sim.getBattlefield().containsPermanent(blocker.getId())); + // fake non-block simulation + simNonBlocking.getPlayer(defendingPlayerId).damage( + attacker.getPower().getValue(), + attacker.getId(), + null, + simNonBlocking, + true, + true + ); + simNonBlocking.checkStateAndTriggered(); + simNonBlocking.processAction(); + + int endBlockingScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim, false).getTotalScore(); + int endNonBlockingScore = GameStateEvaluator2.evaluate(defendingPlayerId, simNonBlocking, false).getTotalScore(); + return new SurviveInfo( + !sim.getBattlefield().containsPermanent(attacker.getId()), + !sim.getBattlefield().containsPermanent(blocker.getId()), + endBlockingScore - startScore, + endNonBlockingScore - startScore + ); } + private static void simulateCombatDamage(Game sim, Permanent fromCreature, Permanent toCreature, boolean isFirstDamageStep) { + Ability fakeAbility = new SimpleStaticAbility(null); + if (CombatGroup.dealsDamageThisStep(fromCreature, isFirstDamageStep, sim)) { + fakeAbility.setSourceId(fromCreature.getId()); + fakeAbility.setControllerId(fromCreature.getControllerId()); + toCreature.damage(fromCreature.getPower().getValue(), fromCreature.getId(), fakeAbility, sim, true, true); + } + } + + private static void simulateStep(Game sim, Step step) { + sim.getPhase().setStep(step); + if (!step.skipStep(sim, sim.getActivePlayerId())) { + step.beginStep(sim, sim.getActivePlayerId()); + // The following commented out call produces random freezes. + //game.checkStateAndTriggered(); + while (!sim.getStack().isEmpty()) { + sim.getStack().resolve(sim); + sim.applyEffects(); + } + step.endStep(sim, sim.getActivePlayerId()); + } + } } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/SurviveInfo.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/SurviveInfo.java index 5f049da2734..20b96f123cb 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/SurviveInfo.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/SurviveInfo.java @@ -1,50 +1,48 @@ package mage.player.ai.util; -import mage.players.Player; +import mage.game.permanent.Permanent; /** - * @author noxx + * AI: combat simulation result + * + * @author noxx, JayDi85 */ public class SurviveInfo { - private boolean attackerDied; - private boolean blockerDied; + private final boolean attackerDied; + private final boolean blockerDied; + private final int diffBlockingScore; + private final int diffNonblockingScore; - private Player defender; - private boolean triggered; + private Permanent blocker; // for final result - public SurviveInfo(boolean attackerDied, boolean blockerDied, Player defender, boolean triggered) { + public SurviveInfo(boolean attackerDied, boolean blockerDied, int diffBlockingScore, int diffNonblockingScore) { this.attackerDied = attackerDied; this.blockerDied = blockerDied; - this.defender = defender; - this.triggered = triggered; + this.diffBlockingScore = diffBlockingScore; + this.diffNonblockingScore = diffNonblockingScore; } - public SurviveInfo(boolean attackerDied, boolean blockerDied) { - this.attackerDied = attackerDied; - this.blockerDied = blockerDied; + public void setBlocker(Permanent blocker) { + this.blocker = blocker; + } + + public Permanent getBlocker() { + return this.blocker; + } + + public int getDiffBlockingScore() { + return this.diffBlockingScore; + } + + public int getDiffNonblockingScore() { + return this.diffNonblockingScore; } public boolean isAttackerDied() { return attackerDied; } - public void setAttackerDied(boolean attackerDied) { - this.attackerDied = attackerDied; - } - public boolean isBlockerDied() { return blockerDied; } - - public void setBlockerDied(boolean blockerDied) { - this.blockerDied = blockerDied; - } - - public Player getDefender() { - return defender; - } - - public boolean isTriggered() { - return triggered; - } } diff --git a/Mage.Server.Plugins/Mage.Player.AI/pom.xml b/Mage.Server.Plugins/Mage.Player.AI/pom.xml index 4be51267f30..04cf099d905 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AI/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-player-ai 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 562af6d24e3..a938ef24827 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 @@ -70,9 +70,21 @@ public class ComputerPlayer extends PlayerImpl { protected int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available // debug only: set TRUE to debug simulation's code/games (on false sim thread will be stopped after few secs by timeout) - protected boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = false; + protected boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = false; // DebugUtil.AI_ENABLE_DEBUG_MODE; + + // AI agents uses game simulation thread for all calcs and it's high CPU consumption + // More AI threads - more parallel AI games can be calculate + // If you catch errors like ConcurrentModificationException, then AI implementation works with wrong data + // (e.g. with original game instead copy) or AI use wrong logic (one sim result depends on another sim result) + // How-to use: + // * 1 for debug or stable + // * 5 for good performance on average computer + // * use your's CPU cores for best performance + // TODO: add server config to control max AI threads (with CPU cores by default) + // TODO: rework AI implementation to use multiple sims calculation instead one by one + final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 5;//DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5; + - final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 1; // TODO: rework simulations logic to use multiple calcs instead one by one private final transient Map unplayable = new TreeMap<>(); private final transient List playableNonInstant = new ArrayList<>(); diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml b/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml index f2fa92296ab..d488c1ed04f 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-player-ai-mcts diff --git a/Mage.Server.Plugins/Mage.Player.Human/pom.xml b/Mage.Server.Plugins/Mage.Player.Human/pom.xml index de5ddc29d61..930c9c7787c 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.Human/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-player-human diff --git a/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/pom.xml b/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/pom.xml index 39150f8e260..520028b4293 100644 --- a/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/pom.xml +++ b/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-tournament-boosterdraft diff --git a/Mage.Server.Plugins/Mage.Tournament.Constructed/pom.xml b/Mage.Server.Plugins/Mage.Tournament.Constructed/pom.xml index 2760714e8d2..4ec19f4a47d 100644 --- a/Mage.Server.Plugins/Mage.Tournament.Constructed/pom.xml +++ b/Mage.Server.Plugins/Mage.Tournament.Constructed/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-tournament-constructed diff --git a/Mage.Server.Plugins/Mage.Tournament.Sealed/pom.xml b/Mage.Server.Plugins/Mage.Tournament.Sealed/pom.xml index 9b494faa29b..3d00fdd278b 100644 --- a/Mage.Server.Plugins/Mage.Tournament.Sealed/pom.xml +++ b/Mage.Server.Plugins/Mage.Tournament.Sealed/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.55 + 1.4.56 mage-tournament-sealed diff --git a/Mage.Server.Plugins/pom.xml b/Mage.Server.Plugins/pom.xml index 5ba184819b3..b22f00b7348 100644 --- a/Mage.Server.Plugins/pom.xml +++ b/Mage.Server.Plugins/pom.xml @@ -7,7 +7,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage-server-plugins diff --git a/Mage.Server/pom.xml b/Mage.Server/pom.xml index 9ee564862c7..9774edf32ca 100644 --- a/Mage.Server/pom.xml +++ b/Mage.Server/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage-server diff --git a/Mage.Server/release/startServer.bat b/Mage.Server/release/startServer.bat index 71842f1a80c..4a74533a826 100644 --- a/Mage.Server/release/startServer.bat +++ b/Mage.Server/release/startServer.bat @@ -4,4 +4,4 @@ set JAVA_HOME="C:\Program Files\Java\jre7\" set CLASSPATH=%JAVA_HOME%/bin;%CLASSPATH% set PATH=%JAVA_HOME%/bin;%PATH% :NOJAVADIR -java -Xmx1024m -XX:MaxPermSize=384m -jar ./lib/mage-server-${project.version}.jar +java -Xmx1024m -jar ./lib/mage-server-${project.version}.jar diff --git a/Mage.Server/release/startServer.command b/Mage.Server/release/startServer.command index 3051fb24884..04432efed29 100644 --- a/Mage.Server/release/startServer.command +++ b/Mage.Server/release/startServer.command @@ -2,4 +2,4 @@ cd "`dirname "$0"`" -java -Xmx1024m -XX:MaxPermSize=384m -jar ./lib/mage-server-${project.version}.jar +java -Xmx1024m -jar ./lib/mage-server-${project.version}.jar diff --git a/Mage.Server/release/startServer.sh b/Mage.Server/release/startServer.sh index 020b9069f7c..81059826e3f 100755 --- a/Mage.Server/release/startServer.sh +++ b/Mage.Server/release/startServer.sh @@ -1,3 +1,3 @@ #!/bin/sh -java -Xmx1024m -XX:MaxPermSize=384m -jar ./lib/mage-server-${project.version}.jar +java -Xmx1024m -jar ./lib/mage-server-${project.version}.jar diff --git a/Mage.Server/release/startServerWin7.bat b/Mage.Server/release/startServerWin7.bat index 0c62850ca35..0c41e890b3d 100644 --- a/Mage.Server/release/startServerWin7.bat +++ b/Mage.Server/release/startServerWin7.bat @@ -4,4 +4,4 @@ set JAVA_HOME="C:\Program Files (x86)\Java\jre7\" set CLASSPATH=%JAVA_HOME%/bin;%CLASSPATH% set PATH=%JAVA_HOME%/bin;%PATH% :NOJAVADIR -java -Xmx1024m -XX:MaxPermSize=384m -jar ./lib/mage-server-${project.version}.jar \ No newline at end of file +java -Xmx1024m -jar ./lib/mage-server-${project.version}.jar \ No newline at end of file diff --git a/Mage.Server/src/main/java/mage/server/MageServerImpl.java b/Mage.Server/src/main/java/mage/server/MageServerImpl.java index b6d4c23ae51..7edd074a29e 100644 --- a/Mage.Server/src/main/java/mage/server/MageServerImpl.java +++ b/Mage.Server/src/main/java/mage/server/MageServerImpl.java @@ -1001,10 +1001,18 @@ public class MageServerImpl implements MageServer { } public void handleException(Exception ex) throws MageException { - if (!ex.getMessage().equals("No message")) { - logger.fatal("", ex); + if (ex.getMessage() != null && !ex.getMessage().equals("No message")) { throw new MageException("Server error: " + ex.getMessage()); } + + if (ex instanceof ConcurrentModificationException) { + // how-to fix: game objects must be accessible by game thread only, all other threads must work with copies + logger.error("wrong threads sync error", ex); + } else { + // TODO: on logs spamming (e.g. connection problems) move it inside condition block above + logger.error("unknown error", ex); + } + } @Override diff --git a/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java b/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java index 3bb4fefae3a..e30c6ced338 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java +++ b/Mage.Server/src/main/java/mage/server/game/GameSessionWatcher.java @@ -42,6 +42,8 @@ public class GameSessionWatcher { if (!killed) { Optional user = userManager.getUser(userId); if (user.isPresent()) { + // TODO: can be called outside of the game thread, e.g. user start watching already running game + // possible fix: getGameView must use last cached value in non game thread call (split by sessions) user.get().fireCallback(new ClientCallback(ClientCallbackMethod.GAME_INIT, game.getId(), getGameView())); return true; } diff --git a/Mage.Sets/pom.xml b/Mage.Sets/pom.xml index a33ae70434b..c55e8bb1d3b 100644 --- a/Mage.Sets/pom.xml +++ b/Mage.Sets/pom.xml @@ -7,7 +7,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage-sets diff --git a/Mage.Sets/src/mage/cards/a/AccursedDuneyard.java b/Mage.Sets/src/mage/cards/a/AccursedDuneyard.java new file mode 100644 index 00000000000..4a95cb1e244 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AccursedDuneyard.java @@ -0,0 +1,57 @@ +package mage.cards.a; + +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.RegenerateTargetEffect; +import mage.abilities.mana.ColorlessManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; +import mage.target.TargetPermanent; + +/** + * + * @author sobiech + */ +public final class AccursedDuneyard extends CardImpl { + private static final FilterPermanent filter = new FilterPermanent("Shade, Skeleton, Specter, Spirit, Vampire, Wraith, or Zombie"); + + static { + filter.add(Predicates.or( + SubType.SHADE.getPredicate(), + SubType.SKELETON.getPredicate(), + SubType.SPECTER.getPredicate(), + SubType.SPIRIT.getPredicate(), + SubType.VAMPIRE.getPredicate(), + SubType.WRAITH.getPredicate(), + SubType.ZOMBIE.getPredicate() + )); + } + + public AccursedDuneyard(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); + + // {T}: Add {C}. + this.addAbility(new ColorlessManaAbility()); + // {2}, {T}: Regenerate target Shade, Skeleton, Specter, Spirit, Vampire, Wraith, or Zombie. + final Ability ability = new SimpleActivatedAbility(new RegenerateTargetEffect(), new ManaCostsImpl<>("{2}")); + ability.addTarget(new TargetPermanent(filter)); + ability.addCost(new TapSourceCost()); + this.addAbility(ability); + } + + private AccursedDuneyard(final AccursedDuneyard card) { + super(card); + } + + @Override + public AccursedDuneyard copy() { + return new AccursedDuneyard(this); + } +} diff --git a/Mage.Sets/src/mage/cards/a/AdaptiveOmnitool.java b/Mage.Sets/src/mage/cards/a/AdaptiveOmnitool.java new file mode 100644 index 00000000000..f88856700d9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AdaptiveOmnitool.java @@ -0,0 +1,66 @@ +package mage.cards.a; + +import java.util.UUID; + +import mage.abilities.common.AttacksAttachedTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.abilities.effects.common.continuous.BoostEquippedEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.abilities.keyword.EquipAbility; +import mage.constants.Outcome; +import mage.constants.PutCards; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledArtifactPermanent; +import mage.target.common.TargetControlledCreaturePermanent; + +/** + * + * @author sobiech + */ +public final class AdaptiveOmnitool extends CardImpl { + private final static DynamicValue artifactYouControlCount = new PermanentsOnBattlefieldCount(new FilterControlledArtifactPermanent()); + private final static Hint hint = new ValueHint("Artifacts you control", artifactYouControlCount); + + public AdaptiveOmnitool(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}"); + + this.subtype.add(SubType.EQUIPMENT); + + // Equipped creature gets +1/+1 for each artifact you control. + this.addAbility( + new SimpleStaticAbility(new BoostEquippedEffect(artifactYouControlCount, artifactYouControlCount)).addHint(hint) + ); + + // Whenever equipped creature attacks, look at the top six cards of your library. You may reveal an artifact card from among them and put it into your hand. Put the rest on the bottom of your library in a random order. + this.addAbility(new AttacksAttachedTriggeredAbility( + new LookLibraryAndPickControllerEffect( + 6, + 1, + StaticFilters.FILTER_CARD_ARTIFACT, + PutCards.HAND, + PutCards.BOTTOM_RANDOM + ) + )); + + // Equip {3} + this.addAbility(new EquipAbility(Outcome.BoostCreature, new GenericManaCost(3), new TargetControlledCreaturePermanent(), false)); + } + + private AdaptiveOmnitool(final AdaptiveOmnitool card) { + super(card); + } + + @Override + public AdaptiveOmnitool copy() { + return new AdaptiveOmnitool(this); + } +} diff --git a/Mage.Sets/src/mage/cards/a/AdrenalineJockey.java b/Mage.Sets/src/mage/cards/a/AdrenalineJockey.java new file mode 100644 index 00000000000..2eed3297193 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AdrenalineJockey.java @@ -0,0 +1,62 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.common.ActivateAbilityTriggeredAbility; +import mage.abilities.common.SpellCastAllTriggeredAbility; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SetTargetPointer; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.counters.CounterType; +import mage.filter.FilterSpell; +import mage.filter.FilterStackObject; +import mage.filter.predicate.other.ExhaustAbilityPredicate; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AdrenalineJockey extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("a spell, if it's not their turn"); + private static final FilterStackObject filter2 = new FilterStackObject("an exhaust ability"); + + static { + filter.add(TargetController.INACTIVE.getControllerPredicate()); + filter2.add(ExhaustAbilityPredicate.instance); + } + + public AdrenalineJockey(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); + + this.subtype.add(SubType.MINOTAUR); + this.subtype.add(SubType.PILOT); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Whenever a player casts a spell, if it's not their turn, this creature deals 4 damage to them. + this.addAbility(new SpellCastAllTriggeredAbility( + new DamageTargetEffect(4, true, "them"), + filter, false, SetTargetPointer.PLAYER + )); + + // Whenever you activate an exhaust ability, put a +1/+1 counter on this creature. + this.addAbility(new ActivateAbilityTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), filter2, SetTargetPointer.NONE + )); + } + + private AdrenalineJockey(final AdrenalineJockey card) { + super(card); + } + + @Override + public AdrenalineJockey copy() { + return new AdrenalineJockey(this); + } +} diff --git a/Mage.Sets/src/mage/cards/a/AerialModification.java b/Mage.Sets/src/mage/cards/a/AerialModification.java index 2aced485449..e52857453ab 100644 --- a/Mage.Sets/src/mage/cards/a/AerialModification.java +++ b/Mage.Sets/src/mage/cards/a/AerialModification.java @@ -1,10 +1,7 @@ - package mage.cards.a; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.Effect; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.continuous.BecomesCreatureIfVehicleEffect; import mage.abilities.effects.common.continuous.BoostEnchantedEffect; @@ -13,46 +10,39 @@ import mage.abilities.keyword.EnchantAbility; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.constants.AttachmentType; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; +import java.util.UUID; + /** - * * @author Styxo */ public final class AerialModification extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or(CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate())); - } - public AerialModification(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{W}"); this.subtype.add(SubType.AURA); // Enchant creature or Vehicle - TargetPermanent auraTarget = new TargetPermanent(filter); + TargetPermanent auraTarget = new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE); this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addEffect(new AttachEffect(Outcome.Benefit)); - Ability ability = new EnchantAbility(auraTarget); - this.addAbility(ability); + this.addAbility(new EnchantAbility(auraTarget)); // As long as enchanted permanent is a Vehicle, it's a creature in addition to its other types. this.addAbility(new SimpleStaticAbility(new BecomesCreatureIfVehicleEffect())); // Enchanted creature gets +2/+2 and has flying. - Effect effect = new BoostEnchantedEffect(2, 2); - effect.setText("Enchanted creature gets +2/+2"); - ability = new SimpleStaticAbility(effect); - effect = new GainAbilityAttachedEffect(FlyingAbility.getInstance(), AttachmentType.AURA); - effect.setText(" and has flying"); - ability.addEffect(effect); + Ability ability = new SimpleStaticAbility(new BoostEnchantedEffect(2, 2)); + ability.addEffect(new GainAbilityAttachedEffect( + FlyingAbility.getInstance(), AttachmentType.AURA + ).setText(" and has flying")); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/a/AetherMeltdown.java b/Mage.Sets/src/mage/cards/a/AetherMeltdown.java index 5a86fd7e514..08c55a99674 100644 --- a/Mage.Sets/src/mage/cards/a/AetherMeltdown.java +++ b/Mage.Sets/src/mage/cards/a/AetherMeltdown.java @@ -1,11 +1,8 @@ package mage.cards.a; -import java.util.UUID; -import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.Effect; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.continuous.BoostEnchantedEffect; import mage.abilities.effects.common.counter.GetEnergyCountersControllerEffect; @@ -13,41 +10,37 @@ import mage.abilities.keyword.EnchantAbility; import mage.abilities.keyword.FlashAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class AetherMeltdown extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or(CardType.CREATURE.getPredicate(), SubType.VEHICLE.getPredicate())); - } - public AetherMeltdown(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{1}{U}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}"); this.subtype.add(SubType.AURA); // Flash this.addAbility(FlashAbility.getInstance()); + // Enchant creature or vehicle. - TargetPermanent auraTarget = new TargetPermanent(filter); + TargetPermanent auraTarget = new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE); this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addEffect(new AttachEffect(Outcome.Detriment)); - Ability ability = new EnchantAbility(auraTarget); - this.addAbility(ability); + this.addAbility(new EnchantAbility(auraTarget)); // When Aether Meltdown enters the battlefield, you get {E}{E}. this.addAbility(new EntersBattlefieldTriggeredAbility(new GetEnergyCountersControllerEffect(2))); + // Enchanted permanent gets -4/-0. - Effect effect = new BoostEnchantedEffect(-4, 0, Duration.WhileOnBattlefield); - this.addAbility(new SimpleStaticAbility(effect)); + this.addAbility(new SimpleStaticAbility(new BoostEnchantedEffect(-4, 0).setText("enchanted permanent gets -4/-0"))); } private AetherMeltdown(final AetherMeltdown card) { diff --git a/Mage.Sets/src/mage/cards/a/AetherfluxConduit.java b/Mage.Sets/src/mage/cards/a/AetherfluxConduit.java new file mode 100644 index 00000000000..d21de17365e --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AetherfluxConduit.java @@ -0,0 +1,74 @@ +package mage.cards.a; + +import java.util.Optional; +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.costs.common.PayEnergyCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.CastFromHandWithoutPayingManaCostEffect; +import mage.abilities.effects.common.counter.GetEnergyCountersControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.stack.Spell; + +/** + * @author sobiech + */ +public final class AetherfluxConduit extends CardImpl { + + public AetherfluxConduit(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{6}"); + + // Whenever you cast a spell, you get an amount of {E} equal to the amount of mana spent to cast that spell. + this.addAbility(new SpellCastControllerTriggeredAbility( + new AetherfluxConduitEffect(),false + )); + + // {T}, Pay fifty {E}: Draw seven cards. You may cast any number of spells from your hand without paying their mana costs. + final Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(7), new TapSourceCost()); + ability.addCost(new PayEnergyCost(50).setText("Pay fifty {E}")); + ability.addEffect(new CastFromHandWithoutPayingManaCostEffect()); + this.addAbility(ability); + } + + private AetherfluxConduit(final AetherfluxConduit card) { + super(card); + } + + @Override + public AetherfluxConduit copy() { + return new AetherfluxConduit(this); + } +} +class AetherfluxConduitEffect extends OneShotEffect { + + AetherfluxConduitEffect() { + super(Outcome.Benefit); + this.staticText = "you get an amount of {E} (energy counters) equal to the amount of mana spent to cast that spell"; + } + private AetherfluxConduitEffect(AetherfluxConduitEffect effect) { + super(effect); + } + + @Override + public AetherfluxConduitEffect copy() { + return new AetherfluxConduitEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Optional.ofNullable(this.getValue("spellCast")) + .map(Spell.class::cast) + .ifPresent(spell -> new GetEnergyCountersControllerEffect(spell.getManaValue()).apply(game, source)); + return true; + } +} + diff --git a/Mage.Sets/src/mage/cards/a/AethericAmplifier.java b/Mage.Sets/src/mage/cards/a/AethericAmplifier.java new file mode 100644 index 00000000000..48737ded8e9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AethericAmplifier.java @@ -0,0 +1,149 @@ +package mage.cards.a; + +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.mana.AnyColorManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.counters.Counter; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +/** + * @author sobiech + */ +public final class AethericAmplifier extends CardImpl { + + public AethericAmplifier(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); + + // {T}: Add one mana of any color. + this.addAbility(new AnyColorManaAbility()); + + // {4}, {T}: Choose one. Activate only as a sorcery. + // * Double the number of each kind of counter on target permanent. + final Ability ability = new ActivateAsSorceryActivatedAbility(new AethericAmplifierDoublePermanentEffect(), new GenericManaCost(4)) + .withShowActivateText(false); + ability.addCost(new TapSourceCost()); + ability.addTarget(new TargetPermanent()); + ability.getModes().setChooseText("choose one. Activate only as a sorcery."); + + // * Double the number of each kind of counter you have. + ability.addMode(new Mode(new AethericAmplifierDoubleControllerEffect())); + + this.addAbility(ability); + } + + private AethericAmplifier(final AethericAmplifier card) { + super(card); + } + + @Override + public AethericAmplifier copy() { + return new AethericAmplifier(this); + } +} + +class AethericAmplifierDoublePermanentEffect extends OneShotEffect { + + AethericAmplifierDoublePermanentEffect() { + super(Outcome.Benefit); + this.staticText = "double the number of each kind of counter on target permanent"; + } + + private AethericAmplifierDoublePermanentEffect(OneShotEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + final Permanent permanent = game.getPermanent(this.getTargetPointer().getFirst(game, source)); + + if (permanent == null) { + return false; + } + + final Set counters = permanent + .getCounters(game) + .values() + .stream() + .map(counter -> CounterType + .findByName(counter.getName()) + .createInstance(counter.getCount())) + .collect(Collectors.toSet()); + + if (counters.isEmpty()) { + return false; + } + + counters.forEach(counter -> permanent.addCounters(counter, source, game)); + + return true; + } + + @Override + public AethericAmplifierDoublePermanentEffect copy() { + return new AethericAmplifierDoublePermanentEffect(this); + } +} + +class AethericAmplifierDoubleControllerEffect extends OneShotEffect { + + AethericAmplifierDoubleControllerEffect() { + super(Outcome.Benefit); + this.staticText = "double the number of each kind of counter you have"; + } + + private AethericAmplifierDoubleControllerEffect(OneShotEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + final Player controller = game.getPlayer(source.getControllerId()); + + if (controller == null) { + return false; + } + + final Set counters = controller.getCountersAsCopy() + .values() + .stream() + .map(counter -> CounterType + .findByName(counter.getName()) + .createInstance(counter.getCount())) + .collect(Collectors.toSet()); + + if (counters.isEmpty()) { + return false; + } + + counters.forEach(counter -> controller.addCounters( + counter, + source.getControllerId(), + source, + game)); + + return true; + } + + @Override + public AethericAmplifierDoubleControllerEffect copy() { + return new AethericAmplifierDoubleControllerEffect(this); + } +} + + diff --git a/Mage.Sets/src/mage/cards/a/AgonasaurRex.java b/Mage.Sets/src/mage/cards/a/AgonasaurRex.java index 93cc878db87..6d49b3de283 100644 --- a/Mage.Sets/src/mage/cards/a/AgonasaurRex.java +++ b/Mage.Sets/src/mage/cards/a/AgonasaurRex.java @@ -14,8 +14,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.counters.CounterType; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import java.util.UUID; @@ -25,15 +24,6 @@ import java.util.UUID; */ public final class AgonasaurRex extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public AgonasaurRex(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}{G}"); @@ -51,7 +41,7 @@ public final class AgonasaurRex extends CardImpl { Ability ability = new CycleTriggeredAbility(new AddCountersTargetEffect(CounterType.P1P1.createInstance(2))); ability.addEffect(new GainAbilityTargetEffect(TrampleAbility.getInstance()).setText("it gains trample")); ability.addEffect(new GainAbilityTargetEffect(IndestructibleAbility.getInstance()).setText("and indestructible until end of turn")); - ability.addTarget(new TargetPermanent(0, 1, filter)); + ability.addTarget(new TargetPermanent(0, 1, StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/a/AltarOfTheGoyf.java b/Mage.Sets/src/mage/cards/a/AltarOfTheGoyf.java index 7ad18f44840..0e4336b7179 100644 --- a/Mage.Sets/src/mage/cards/a/AltarOfTheGoyf.java +++ b/Mage.Sets/src/mage/cards/a/AltarOfTheGoyf.java @@ -5,7 +5,6 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostTargetEffect; import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -33,7 +32,7 @@ public final class AltarOfTheGoyf extends CardImpl { // Whenever a creature you control attacks alone, it gets +X/+X until end of turn, where X is the number of card types among cards in all graveyards. this.addAbility(new AttacksAloneControlledTriggeredAbility( new BoostTargetEffect(CardTypesInGraveyardCount.ALL, CardTypesInGraveyardCount.ALL, Duration.EndOfTurn), - true, false).addHint(CardTypesInGraveyardHint.ALL)); + true, false).addHint(CardTypesInGraveyardCount.ALL.getHint())); // Lhurgoyf creatures you control have trample. this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( diff --git a/Mage.Sets/src/mage/cards/a/AngelOfDeliverance.java b/Mage.Sets/src/mage/cards/a/AngelOfDeliverance.java index 7744b4ffe5e..429f510c433 100644 --- a/Mage.Sets/src/mage/cards/a/AngelOfDeliverance.java +++ b/Mage.Sets/src/mage/cards/a/AngelOfDeliverance.java @@ -4,8 +4,8 @@ import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.DealsDamageSourceTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.ExileTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -35,7 +35,7 @@ public final class AngelOfDeliverance extends CardImpl { Ability ability = new DealsDamageSourceTriggeredAbility(new ExileTargetEffect()) .withInterveningIf(DeliriumCondition.instance); ability.addTarget(new TargetOpponentsCreaturePermanent()); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability.setAbilityWord(AbilityWord.DELIRIUM)); } diff --git a/Mage.Sets/src/mage/cards/a/ArniSlaysTheTroll.java b/Mage.Sets/src/mage/cards/a/ArniSlaysTheTroll.java index 975b30bef84..23a599352f2 100644 --- a/Mage.Sets/src/mage/cards/a/ArniSlaysTheTroll.java +++ b/Mage.Sets/src/mage/cards/a/ArniSlaysTheTroll.java @@ -62,6 +62,7 @@ public final class ArniSlaysTheTroll extends CardImpl { "You gain life equal to the greatest power among creatures you control" ) ); + sagaAbility.addHint(GreatestPowerAmongControlledCreaturesValue.getHint()); this.addAbility(sagaAbility); } diff --git a/Mage.Sets/src/mage/cards/a/AutarchMammoth.java b/Mage.Sets/src/mage/cards/a/AutarchMammoth.java new file mode 100644 index 00000000000..fea21873738 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AutarchMammoth.java @@ -0,0 +1,53 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.common.AttacksWhileSaddledTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.SaddleAbility; +import mage.abilities.meta.OrTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.permanent.token.ElephantToken; + +import java.util.UUID; + +/** + * @author jackd149 + */ +public final class AutarchMammoth extends CardImpl { + + public AutarchMammoth(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}{G}"); + + this.subtype.add(SubType.ELEPHANT); + this.subtype.add(SubType.MOUNT); + this.power = new MageInt(5); + this.toughness = new MageInt(5); + + // When this creature enters and whenever it attacks while saddled, create a 3/3 green Elephant creature token. + this.addAbility(new OrTriggeredAbility( + Zone.BATTLEFIELD, + new CreateTokenEffect(new ElephantToken(), 1), + false, + "When this creature enters and whenever it attacks while saddled, ", + new EntersBattlefieldTriggeredAbility(null), + new AttacksWhileSaddledTriggeredAbility(null) + )); + + // Saddle 5 + this.addAbility(new SaddleAbility(5)); + } + + private AutarchMammoth(final AutarchMammoth card) { + super(card); + } + + @Override + public AutarchMammoth copy() { + return new AutarchMammoth(this); + } +} diff --git a/Mage.Sets/src/mage/cards/a/AuthorOfShadows.java b/Mage.Sets/src/mage/cards/a/AuthorOfShadows.java index 230117ed331..a09352a5f75 100644 --- a/Mage.Sets/src/mage/cards/a/AuthorOfShadows.java +++ b/Mage.Sets/src/mage/cards/a/AuthorOfShadows.java @@ -69,7 +69,7 @@ class AuthorOfShadowsEffect extends OneShotEffect { return false; } Cards cards = new CardsImpl(); - game.getOpponents(source.getControllerId()) + game.getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/a/AutumnalGloom.java b/Mage.Sets/src/mage/cards/a/AutumnalGloom.java index 68f7ec457de..684727c24dd 100644 --- a/Mage.Sets/src/mage/cards/a/AutumnalGloom.java +++ b/Mage.Sets/src/mage/cards/a/AutumnalGloom.java @@ -3,20 +3,19 @@ package mage.cards.a; import java.util.UUID; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.MillCardsControllerEffect; import mage.abilities.effects.common.TransformSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.TargetController; -import mage.constants.Zone; /** * @author LevelX2 @@ -34,7 +33,7 @@ public final class AutumnalGloom extends CardImpl { this.addAbility(new TransformAbility()); Ability ability = new BeginningOfEndStepTriggeredAbility(TargetController.YOU, new TransformSourceEffect(), false, DeliriumCondition.instance); ability.setAbilityWord(AbilityWord.DELIRIUM); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/b/BackwoodsSurvivalists.java b/Mage.Sets/src/mage/cards/b/BackwoodsSurvivalists.java index bbc0de902b5..3c6a074c89e 100644 --- a/Mage.Sets/src/mage/cards/b/BackwoodsSurvivalists.java +++ b/Mage.Sets/src/mage/cards/b/BackwoodsSurvivalists.java @@ -7,16 +7,15 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; import mage.constants.SubType; -import mage.constants.Zone; /** * @author LevelX2 @@ -33,7 +32,7 @@ public final class BackwoodsSurvivalists extends CardImpl { ConditionalContinuousEffect effect = new ConditionalContinuousEffect(new BoostSourceEffect(1, 1, Duration.WhileOnBattlefield), DeliriumCondition.instance, "Delirium — {this} gets +1/+1"); Ability ability = new SimpleStaticAbility(effect); ability.addEffect(new ConditionalContinuousEffect(new GainAbilitySourceEffect(TrampleAbility.getInstance()), DeliriumCondition.instance, "and has trample as long as there are four or more card types among cards in your graveyard.")); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/b/BalladOfTheBlackFlag.java b/Mage.Sets/src/mage/cards/b/BalladOfTheBlackFlag.java index d69b79191a1..8129e6229d3 100644 --- a/Mage.Sets/src/mage/cards/b/BalladOfTheBlackFlag.java +++ b/Mage.Sets/src/mage/cards/b/BalladOfTheBlackFlag.java @@ -18,7 +18,7 @@ import java.util.UUID; */ public final class BalladOfTheBlackFlag extends CardImpl { - private static final FilterCard filter = new FilterHistoricCard("historic spells you cast this turn"); + private static final FilterCard filter = new FilterHistoricCard(); public BalladOfTheBlackFlag(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}{U}"); @@ -31,7 +31,7 @@ public final class BalladOfTheBlackFlag extends CardImpl { // I, II, III -- Mill three cards. You may put a historic card from among them into your hand. sagaAbility.addChapterEffect( this, SagaChapter.CHAPTER_I, SagaChapter.CHAPTER_III, - new MillThenPutInHandEffect(3, filter) + new MillThenPutInHandEffect(3, filter).withTextOptions("them") ); // IV - Historic spells you cast this turn cost {2} less to cast. diff --git a/Mage.Sets/src/mage/cards/b/BalustradeWurm.java b/Mage.Sets/src/mage/cards/b/BalustradeWurm.java index 320e73ec35f..e81434332fb 100644 --- a/Mage.Sets/src/mage/cards/b/BalustradeWurm.java +++ b/Mage.Sets/src/mage/cards/b/BalustradeWurm.java @@ -5,8 +5,8 @@ import mage.abilities.common.ActivateIfConditionActivatedAbility; import mage.abilities.common.CantBeCounteredSourceAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.ReturnSourceFromGraveyardToBattlefieldWithCounterEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.HasteAbility; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; @@ -42,7 +42,7 @@ public final class BalustradeWurm extends CardImpl { Zone.GRAVEYARD, new ReturnSourceFromGraveyardToBattlefieldWithCounterEffect(CounterType.FINALITY.createInstance(), false), new ManaCostsImpl<>("{2}{G}{G}"), DeliriumCondition.instance, TimingRule.SORCERY - ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private BalustradeWurm(final BalustradeWurm card) { diff --git a/Mage.Sets/src/mage/cards/b/Barrowgoyf.java b/Mage.Sets/src/mage/cards/b/Barrowgoyf.java index d77d6dfc6ab..beaa3fd5259 100644 --- a/Mage.Sets/src/mage/cards/b/Barrowgoyf.java +++ b/Mage.Sets/src/mage/cards/b/Barrowgoyf.java @@ -24,8 +24,6 @@ import java.util.UUID; */ public final class Barrowgoyf extends CardImpl { - private static final DynamicValue powerValue = CardTypesInGraveyardCount.ALL; - public Barrowgoyf(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}"); this.subtype.add(SubType.LHURGOYF); @@ -40,7 +38,9 @@ public final class Barrowgoyf extends CardImpl { this.addAbility(LifelinkAbility.getInstance()); // Barrowgoyf's power is equal to the number of card types among cards in all graveyards and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.ALL) + ).addHint(CardTypesInGraveyardCount.ALL.getHint())); // Whenever Barrowgoyf deals combat damage to a player, you may mill that many cards. If you do, you may put a creature card from among them into your hand. this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/b/BeastieBeatdown.java b/Mage.Sets/src/mage/cards/b/BeastieBeatdown.java index 880054a14ad..afc5add0881 100644 --- a/Mage.Sets/src/mage/cards/b/BeastieBeatdown.java +++ b/Mage.Sets/src/mage/cards/b/BeastieBeatdown.java @@ -2,10 +2,10 @@ package mage.cards.b; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.DamageWithPowerFromOneToAnotherTargetEffect; import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -37,7 +37,7 @@ public final class BeastieBeatdown extends CardImpl { DeliriumCondition.instance, AbilityWord.DELIRIUM.formatWord() + "If there are four or more " + "card types among cards in your graveyard, put two +1/+1 counters on the creature you control." ).concatBy("
")); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); // The creature you control deals damage equal to its power to the creature an opponent controls. this.getSpellAbility().addEffect(new DamageWithPowerFromOneToAnotherTargetEffect() diff --git a/Mage.Sets/src/mage/cards/b/BecomeImmense.java b/Mage.Sets/src/mage/cards/b/BecomeImmense.java index 6d3f10f7744..1b3c623820e 100644 --- a/Mage.Sets/src/mage/cards/b/BecomeImmense.java +++ b/Mage.Sets/src/mage/cards/b/BecomeImmense.java @@ -20,7 +20,8 @@ public final class BecomeImmense extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{5}{G}"); // Delve (Each card you exile from your graveyard while casting this spell pays for {1}.) - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); + // Target creature gets +6/+6 until end of turn this.getSpellAbility().addEffect(new BoostTargetEffect(6, 6, Duration.EndOfTurn)); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); diff --git a/Mage.Sets/src/mage/cards/b/BighornerRancher.java b/Mage.Sets/src/mage/cards/b/BighornerRancher.java index c0197498421..7532b135ed4 100644 --- a/Mage.Sets/src/mage/cards/b/BighornerRancher.java +++ b/Mage.Sets/src/mage/cards/b/BighornerRancher.java @@ -38,7 +38,7 @@ public final class BighornerRancher extends CardImpl { this.addAbility(new DynamicManaAbility( Mana.GreenMana(1), GreatestPowerAmongControlledCreaturesValue.instance, new TapSourceCost(), "Add an amount of {G} equal to the greatest power among creatures you control." - )); + ).addHint(GreatestPowerAmongControlledCreaturesValue.getHint())); // Sacrifice Bighorner Rancher: You gain life equal to the greatest toughness among other creatures you control. this.addAbility(new SimpleActivatedAbility( diff --git a/Mage.Sets/src/mage/cards/b/BloodbraidMarauder.java b/Mage.Sets/src/mage/cards/b/BloodbraidMarauder.java index 60fbd087b47..8df9a479d01 100644 --- a/Mage.Sets/src/mage/cards/b/BloodbraidMarauder.java +++ b/Mage.Sets/src/mage/cards/b/BloodbraidMarauder.java @@ -6,8 +6,8 @@ import mage.abilities.common.CantBlockAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.CascadeAbility; import mage.constants.Duration; import mage.constants.SubType; @@ -45,7 +45,7 @@ public final class BloodbraidMarauder extends CardImpl { new GainAbilitySourceEffect(new CascadeAbility(), Duration.WhileOnStack, true), DeliriumCondition.instance, "Delirium — This spell has cascade as long as there are four or more card types among cards in your graveyard." + REMINDER_TEXT - )).addHint(CardTypesInGraveyardHint.YOU)); + )).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private BloodbraidMarauder(final BloodbraidMarauder card) { diff --git a/Mage.Sets/src/mage/cards/b/BloodtitheCollector.java b/Mage.Sets/src/mage/cards/b/BloodtitheCollector.java index 4be5304e1eb..5845f5f9b7a 100644 --- a/Mage.Sets/src/mage/cards/b/BloodtitheCollector.java +++ b/Mage.Sets/src/mage/cards/b/BloodtitheCollector.java @@ -31,10 +31,10 @@ public final class BloodtitheCollector extends CardImpl { // Flying this.addAbility(FlyingAbility.getInstance()); - // When Bloodtithe Collector enters the battlefield, if an opponent lost life this turn, each opponent discards a card. + // When this creature enters, if an opponent lost life this turn, each opponent discards a card. this.addAbility(new ConditionalInterveningIfTriggeredAbility( new EntersBattlefieldTriggeredAbility(new DiscardEachPlayerEffect(TargetController.OPPONENT)), - OpponentsLostLifeCondition.instance, "When {this} enters, " + + OpponentsLostLifeCondition.instance, "When this creature enters, " + "if an opponent lost life this turn, each opponent discards a card." ).addHint(OpponentsLostLifeHint.instance)); } diff --git a/Mage.Sets/src/mage/cards/b/Boommobile.java b/Mage.Sets/src/mage/cards/b/Boommobile.java new file mode 100644 index 00000000000..6bf3fc54c61 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/Boommobile.java @@ -0,0 +1,59 @@ +package mage.cards.b; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.mana.AddConditionalManaOfAnyColorEffect; +import mage.abilities.effects.mana.ManaEffect; +import mage.abilities.keyword.CrewAbility; +import mage.abilities.keyword.ExhaustAbility; +import mage.abilities.mana.builder.common.ActivatedAbilityManaBuilder; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.target.common.TargetAnyTarget; + +import java.util.UUID; + +/** + * @author jackd149 + */ +public final class Boommobile extends CardImpl { + + public Boommobile(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{R}{R}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(5); + this.toughness = new MageInt(5); + + // When this Vehicle enters, add four mana of any one color. Spend this mana only to activate abilities. + ManaEffect entersEffect = new AddConditionalManaOfAnyColorEffect(4, new ActivatedAbilityManaBuilder()); + entersEffect.setText("add four mana of any one color. Spend this mana only to activate abilities."); + this.addAbility(new EntersBattlefieldTriggeredAbility(entersEffect)); + + // Exhaust -- {X}{2}{R}: This vehicle deals X damage to any target. Put a +1/+1 counter on this Vehicle. + Ability exhaustAbility = new ExhaustAbility(new DamageTargetEffect(GetXValue.instance), new ManaCostsImpl<>("{X}{2}{R}")); + exhaustAbility.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance())); + exhaustAbility.addTarget(new TargetAnyTarget()); + this.addAbility(exhaustAbility); + + // Crew 2 + this.addAbility(new CrewAbility(2)); + } + + private Boommobile(final Boommobile card) { + super(card); + } + + @Override + public Boommobile copy() { + return new Boommobile(this); + } +} diff --git a/Mage.Sets/src/mage/cards/b/BounceOff.java b/Mage.Sets/src/mage/cards/b/BounceOff.java index cd831b8ff8d..141e4023aa3 100644 --- a/Mage.Sets/src/mage/cards/b/BounceOff.java +++ b/Mage.Sets/src/mage/cards/b/BounceOff.java @@ -4,9 +4,7 @@ import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import java.util.UUID; @@ -16,21 +14,12 @@ import java.util.UUID; */ public final class BounceOff extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public BounceOff(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}"); // Return target creature or Vehicle to its owner's hand. this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); - this.getSpellAbility().addTarget(new TargetPermanent(filter)); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); } private BounceOff(final BounceOff card) { diff --git a/Mage.Sets/src/mage/cards/b/BrainstealerDragon.java b/Mage.Sets/src/mage/cards/b/BrainstealerDragon.java index 2ac0edd6e2a..51191b395c7 100644 --- a/Mage.Sets/src/mage/cards/b/BrainstealerDragon.java +++ b/Mage.Sets/src/mage/cards/b/BrainstealerDragon.java @@ -88,7 +88,7 @@ class BrainstealerDragonExileEffect extends OneShotEffect { return false; } Cards cards = new CardsImpl(); - game.getOpponents(source.getControllerId()) + game.getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/b/BrightfieldMustang.java b/Mage.Sets/src/mage/cards/b/BrightfieldMustang.java index 554c4cd9715..f51d9844687 100644 --- a/Mage.Sets/src/mage/cards/b/BrightfieldMustang.java +++ b/Mage.Sets/src/mage/cards/b/BrightfieldMustang.java @@ -29,7 +29,7 @@ public final class BrightfieldMustang extends CardImpl { // Whenever this creature attacks while saddled, untap it and put a +1/+1 counter on it. Ability ability = new AttacksWhileSaddledTriggeredAbility(new UntapSourceEffect().setText("untap it")); - ability.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance())); + ability.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance()).setText("and put a +1/+1 counter on it")); this.addAbility(ability); // Saddle 1 diff --git a/Mage.Sets/src/mage/cards/b/BrightglassGearhulk.java b/Mage.Sets/src/mage/cards/b/BrightglassGearhulk.java index cb9fc32940a..147c231ad0e 100644 --- a/Mage.Sets/src/mage/cards/b/BrightglassGearhulk.java +++ b/Mage.Sets/src/mage/cards/b/BrightglassGearhulk.java @@ -50,7 +50,7 @@ public final class BrightglassGearhulk extends CardImpl { // When this creature enters, you may search your library for up to two artifact, creature, and/or enchantment cards with mana value 1 or less, reveal them, put them into your hand, then shuffle. this.addAbility(new EntersBattlefieldTriggeredAbility(new SearchLibraryPutInHandEffect( new TargetCardInLibrary(0, 2, filter), true - ))); + ), true)); } private BrightglassGearhulk(final BrightglassGearhulk card) { diff --git a/Mage.Sets/src/mage/cards/b/BroodheartEngine.java b/Mage.Sets/src/mage/cards/b/BroodheartEngine.java new file mode 100644 index 00000000000..31c8602756e --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BroodheartEngine.java @@ -0,0 +1,59 @@ +package mage.cards.b; + +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.effects.keyword.SurveilEffect; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.predicate.Predicates; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BroodheartEngine extends CardImpl { + + private static final FilterCard filter = new FilterCard("creature or Vehicle card from your graveyard"); + + static { + filter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + + public BroodheartEngine(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{B}{G}"); + + // At the beginning of your upkeep, surveil 1. + this.addAbility(new BeginningOfUpkeepTriggeredAbility(new SurveilEffect(1))); + + // {2}{B}{G}, {T}, Sacrifice this artifact: Return target creature or Vehicle card from your graveyard to the battlefield. Activate only as a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility( + new ReturnFromGraveyardToBattlefieldTargetEffect(), new ManaCostsImpl<>("{2}{B}{G}") + ); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeSourceCost()); + ability.addTarget(new TargetCardInYourGraveyard(filter)); + this.addAbility(ability); + } + + private BroodheartEngine(final BroodheartEngine card) { + super(card); + } + + @Override + public BroodheartEngine copy() { + return new BroodheartEngine(this); + } +} diff --git a/Mage.Sets/src/mage/cards/b/Broodspinner.java b/Mage.Sets/src/mage/cards/b/Broodspinner.java index 5300633bb6b..883f54617b2 100644 --- a/Mage.Sets/src/mage/cards/b/Broodspinner.java +++ b/Mage.Sets/src/mage/cards/b/Broodspinner.java @@ -10,7 +10,6 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.keyword.SurveilEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.ReachAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -45,7 +44,7 @@ public final class Broodspinner extends CardImpl { new ManaCostsImpl<>("{4}{B}{G}")); ability.addCost(new TapSourceCost()); ability.addCost(new SacrificeSourceCost()); - this.addAbility(ability.addHint(CardTypesInGraveyardHint.YOU)); + this.addAbility(ability.addHint(CardTypesInGraveyardCount.YOU.getHint())); } private Broodspinner(final Broodspinner card) { diff --git a/Mage.Sets/src/mage/cards/b/BurnerRocket.java b/Mage.Sets/src/mage/cards/b/BurnerRocket.java new file mode 100644 index 00000000000..b97f3782e71 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BurnerRocket.java @@ -0,0 +1,54 @@ +package mage.cards.b; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.CrewAbility; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BurnerRocket extends CardImpl { + + public BurnerRocket(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{R}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // When this Vehicle enters, target creature you control gets +2/+0 and gains trample until end of turn. + Ability ability = new EntersBattlefieldTriggeredAbility(new BoostTargetEffect(2, 0) + .setText("target creature you control gets +2/+0")); + ability.addEffect(new GainAbilityTargetEffect(TrampleAbility.getInstance()) + .setText("and gains trample until end of turn")); + ability.addTarget(new TargetControlledCreaturePermanent()); + this.addAbility(ability); + + // Crew 1 + this.addAbility(new CrewAbility(1)); + } + + private BurnerRocket(final BurnerRocket card) { + super(card); + } + + @Override + public BurnerRocket copy() { + return new BurnerRocket(this); + } +} diff --git a/Mage.Sets/src/mage/cards/b/BurnoutBashtronaut.java b/Mage.Sets/src/mage/cards/b/BurnoutBashtronaut.java index 3ca6df0dbec..ce78ba1c42e 100644 --- a/Mage.Sets/src/mage/cards/b/BurnoutBashtronaut.java +++ b/Mage.Sets/src/mage/cards/b/BurnoutBashtronaut.java @@ -38,7 +38,7 @@ public final class BurnoutBashtronaut extends CardImpl { // {2}: This creature gets +1/+0 until end of turn. this.addAbility(new SimpleActivatedAbility( - new BoostSourceEffect(2, 0, Duration.EndOfTurn), new GenericManaCost(2) + new BoostSourceEffect(1, 0, Duration.EndOfTurn), new GenericManaCost(2) )); // Max speed -- This creature has double strike. diff --git a/Mage.Sets/src/mage/cards/c/CaradoraHeartOfAlacria.java b/Mage.Sets/src/mage/cards/c/CaradoraHeartOfAlacria.java index f7765cef735..052cc2d56d8 100644 --- a/Mage.Sets/src/mage/cards/c/CaradoraHeartOfAlacria.java +++ b/Mage.Sets/src/mage/cards/c/CaradoraHeartOfAlacria.java @@ -12,8 +12,7 @@ import mage.constants.SubType; import mage.constants.SuperType; import mage.counters.CounterType; import mage.filter.FilterCard; -import mage.filter.FilterPermanent; -import mage.filter.common.FilterControlledPermanent; +import mage.filter.StaticFilters; import mage.filter.predicate.Predicates; import mage.target.common.TargetCardInLibrary; @@ -25,17 +24,12 @@ import java.util.UUID; public final class CaradoraHeartOfAlacria extends CardImpl { private static final FilterCard filter = new FilterCard("Mount or Vehicle card"); - private static final FilterPermanent filter2 = new FilterControlledPermanent("creature or Vehicle you control"); static { filter.add(Predicates.or( SubType.MOUNT.getPredicate(), SubType.VEHICLE.getPredicate() )); - filter2.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); } public CaradoraHeartOfAlacria(UUID ownerId, CardSetInfo setInfo) { @@ -49,11 +43,11 @@ public final class CaradoraHeartOfAlacria extends CardImpl { // When Caradora enters, you may search your library for a Mount or Vehicle card, reveal it, put it into your hand, then shuffle. this.addAbility(new EntersBattlefieldTriggeredAbility( - new SearchLibraryPutInHandEffect(new TargetCardInLibrary(filter), true) + new SearchLibraryPutInHandEffect(new TargetCardInLibrary(filter), true), true )); // If one or more +1/+1 counters would be put on a creature or Vehicle you control, that many plus one +1/+1 counters are put on it instead. - this.addAbility(new SimpleStaticAbility(new ModifyCountersAddedEffect(filter2, CounterType.P1P1))); + this.addAbility(new SimpleStaticAbility(new ModifyCountersAddedEffect(StaticFilters.FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE, CounterType.P1P1))); } private CaradoraHeartOfAlacria(final CaradoraHeartOfAlacria card) { diff --git a/Mage.Sets/src/mage/cards/c/CarrionCruiser.java b/Mage.Sets/src/mage/cards/c/CarrionCruiser.java new file mode 100644 index 00000000000..4e8874f642a --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CarrionCruiser.java @@ -0,0 +1,93 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.keyword.CrewAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +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.filter.predicate.Predicates; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CarrionCruiser extends CardImpl { + + public CarrionCruiser(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{B}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // When this Vehicle enters, mill two cards. Then return a creature or Vehicle card from your graveyard to your hand. + Ability ability = new EntersBattlefieldTriggeredAbility(new MillCardsControllerEffect(2)); + ability.addEffect(new CarrionCruiserEffect()); + this.addAbility(ability); + + // Crew 1 + this.addAbility(new CrewAbility(1)); + } + + private CarrionCruiser(final CarrionCruiser card) { + super(card); + } + + @Override + public CarrionCruiser copy() { + return new CarrionCruiser(this); + } +} + +class CarrionCruiserEffect extends OneShotEffect { + + private static final FilterCard filter = new FilterCard("creature or Vehicle card"); + + static { + filter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + + CarrionCruiserEffect() { + super(Outcome.Benefit); + staticText = "Then return a creature or Vehicle card from your graveyard to your hand"; + } + + private CarrionCruiserEffect(final CarrionCruiserEffect effect) { + super(effect); + } + + @Override + public CarrionCruiserEffect copy() { + return new CarrionCruiserEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null || player.getGraveyard().count(filter, game) < 1) { + return false; + } + TargetCard target = new TargetCardInYourGraveyard(filter); + target.withNotTarget(true); + player.choose(outcome, player.getGraveyard(), target, source, game); + Card card = game.getCard(target.getFirstTarget()); + return card != null && player.moveCards(card, Zone.HAND, source, game); + } +} diff --git a/Mage.Sets/src/mage/cards/c/CemeteryTampering.java b/Mage.Sets/src/mage/cards/c/CemeteryTampering.java index 1d79cd569ac..c41f83f6b6f 100644 --- a/Mage.Sets/src/mage/cards/c/CemeteryTampering.java +++ b/Mage.Sets/src/mage/cards/c/CemeteryTampering.java @@ -23,7 +23,7 @@ public final class CemeteryTampering extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{B}"); // Hideaway 5 - this.addAbility(new HideawayAbility(5)); + this.addAbility(new HideawayAbility(this, 5)); // At the beginning of your upkeep, you may mill three cards. Then if there are twenty or more cards in your graveyard, you may play the exiled card without paying its mana cost. this.addAbility(new BeginningOfUpkeepTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/c/ChandraSparkHunter.java b/Mage.Sets/src/mage/cards/c/ChandraSparkHunter.java new file mode 100644 index 00000000000..3f551c2ad1e --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ChandraSparkHunter.java @@ -0,0 +1,78 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.costs.OrCost; +import mage.abilities.costs.common.DiscardCardCost; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.GetEmblemEffect; +import mage.abilities.effects.common.continuous.AddCardTypeTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; +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.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.game.command.emblems.ChandraSparkHunterEmblem; +import mage.game.permanent.token.VehicleToken; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ChandraSparkHunter extends CardImpl { + + private static final FilterPermanent filter = new FilterControlledPermanent(SubType.VEHICLE); + + public ChandraSparkHunter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{3}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.CHANDRA); + this.setStartingLoyalty(4); + + // At the beginning of combat on your turn, choose up to one target Vehicle you control. Until end of turn, it becomes an artifact creature and gains haste. + Ability ability = new BeginningOfCombatTriggeredAbility(new AddCardTypeTargetEffect( + Duration.EndOfTurn, CardType.ARTIFACT, CardType.CREATURE + ).setText("choose up to one target Vehicle you control. Until end of turn, it becomes an artifact creature")); + ability.addEffect(new GainAbilityTargetEffect(HasteAbility.getInstance()).setText("and gains haste")); + ability.addTarget(new TargetPermanent(0, 1, filter)); + this.addAbility(ability); + + // +2: You may sacrifice an artifact or discard a card. If you do, draw a card. + this.addAbility(new LoyaltyAbility(new DoIfCostPaid( + new DrawCardSourceControllerEffect(1), + new OrCost( + "sacrifice an artifact or discard a card", + new SacrificeTargetCost(StaticFilters.FILTER_PERMANENT_ARTIFACT), + new DiscardCardCost() + ) + ), 2)); + + // +0: Create a 3/2 colorless Vehicle artifact token with crew 1. + this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new VehicleToken()), 0)); + + // -7: You get an emblem with "Whenever an artifact you control enters, this emblem deals 3 damage to any target." + this.addAbility(new LoyaltyAbility(new GetEmblemEffect(new ChandraSparkHunterEmblem()), -7)); + } + + private ChandraSparkHunter(final ChandraSparkHunter card) { + super(card); + } + + @Override + public ChandraSparkHunter copy() { + return new ChandraSparkHunter(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/ChitinGravestalker.java b/Mage.Sets/src/mage/cards/c/ChitinGravestalker.java new file mode 100644 index 00000000000..1499612e08e --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ChitinGravestalker.java @@ -0,0 +1,64 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; +import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.abilities.keyword.CyclingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.predicate.Predicates; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ChitinGravestalker extends CardImpl { + + private static final FilterCard filter = new FilterCard("artifact and/or creature card"); + + static { + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate() + )); + } + + private static final DynamicValue xValue = new CardsInControllerGraveyardCount(filter); + private static final Hint hint = new ValueHint("Instant and sorcery card in your graveyard", xValue); + + public ChitinGravestalker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{B}"); + + this.subtype.add(SubType.INSECT); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(5); + this.toughness = new MageInt(4); + + // This spell costs {1} less to cast for each artifact and/or creature card in your graveyard. + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new SpellCostReductionForEachSourceEffect(1, xValue) + ).setRuleAtTheTop(true).addHint(hint)); + + // Cycling {2} + this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{2}"))); + } + + private ChitinGravestalker(final ChitinGravestalker card) { + super(card); + } + + @Override + public ChitinGravestalker copy() { + return new ChitinGravestalker(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/CloudspireCoordinator.java b/Mage.Sets/src/mage/cards/c/CloudspireCoordinator.java new file mode 100644 index 00000000000..07606325839 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CloudspireCoordinator.java @@ -0,0 +1,133 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.keyword.ScryEffect; +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.WatcherScope; +import mage.game.Game; +import mage.game.events.EntersTheBattlefieldEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.PilotSaddleCrewToken; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CloudspireCoordinator extends CardImpl { + + public CloudspireCoordinator(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.PILOT); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + + // When this creature enters, scry 2. + this.addAbility(new EntersBattlefieldTriggeredAbility(new ScryEffect(2))); + + // {T}: Create X 1/1 colorless Pilot creature tokens, where X is the number of Mounts and/or Vehicles that entered the battlefield under your control this turn. The tokens have "This token saddles Mounts and crews Vehicles as though its power were 2 greater." + this.addAbility(new SimpleActivatedAbility(new CreateTokenEffect( + new PilotSaddleCrewToken(), CloudspireCoordinatorValue.instance + ).setText("create X 1/1 colorless Pilot creature tokens, where X is " + + "the number of Mounts and/or Vehicles that entered the battlefield " + + "under your control this turn. The tokens have \"This token saddles Mounts " + + "and crews Vehicles as though its power were 2 greater.\""), + new TapSourceCost() + ).addHint(CloudspireCoordinatorValue.getHint()), new CloudspireCoordinatorWatcher()); + } + + private CloudspireCoordinator(final CloudspireCoordinator card) { + super(card); + } + + @Override + public CloudspireCoordinator copy() { + return new CloudspireCoordinator(this); + } +} + +enum CloudspireCoordinatorValue implements DynamicValue { + instance; + private static final Hint hint = new ValueHint( + "Mounts and/or Vehicles that entered under your control this turn", instance + ); + + public static Hint getHint() { + return hint; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return CloudspireCoordinatorWatcher.getValue(sourceAbility, game); + } + + @Override + public CloudspireCoordinatorValue copy() { + return this; + } + + @Override + public String getMessage() { + return ""; + } + + @Override + public String toString() { + return "X"; + } +} + +class CloudspireCoordinatorWatcher extends Watcher { + + private final Map map = new HashMap<>(); + + CloudspireCoordinatorWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.ENTERS_THE_BATTLEFIELD) { + return; + } + Permanent permanent = ((EntersTheBattlefieldEvent) event).getTarget(); + if (permanent != null + && (permanent.hasSubtype(SubType.MOUNT, game) + || permanent.hasSubtype(SubType.VEHICLE, game))) { + map.compute(permanent.getControllerId(), CardUtil::setOrIncrementValue); + } + } + + @Override + public void reset() { + super.reset(); + map.clear(); + } + + static int getValue(Ability source, Game game) { + return game + .getState() + .getWatcher(CloudspireCoordinatorWatcher.class) + .map + .getOrDefault(source.getControllerId(), 0); + } +} diff --git a/Mage.Sets/src/mage/cards/c/CollectorsCage.java b/Mage.Sets/src/mage/cards/c/CollectorsCage.java index 76e2dd62285..7a59aa15b8f 100644 --- a/Mage.Sets/src/mage/cards/c/CollectorsCage.java +++ b/Mage.Sets/src/mage/cards/c/CollectorsCage.java @@ -27,7 +27,7 @@ public final class CollectorsCage extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{W}"); // Hideaway 5 - this.addAbility(new HideawayAbility(5)); + this.addAbility(new HideawayAbility(this, 5)); // {1}, {T}: Put a +1/+1 counter on target creature you control. Then if you control three or more creatures with different powers, you may play the exiled card without paying its mana cost. Ability ability = new SimpleActivatedAbility( diff --git a/Mage.Sets/src/mage/cards/c/ConsumingBlob.java b/Mage.Sets/src/mage/cards/c/ConsumingBlob.java index 7d4c0020801..05d372541e1 100644 --- a/Mage.Sets/src/mage/cards/c/ConsumingBlob.java +++ b/Mage.Sets/src/mage/cards/c/ConsumingBlob.java @@ -21,8 +21,6 @@ import java.util.UUID; */ public final class ConsumingBlob extends CardImpl { - private static final DynamicValue powerValue = CardTypesInGraveyardCount.YOU; - public ConsumingBlob(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}{G}"); @@ -31,7 +29,9 @@ public final class ConsumingBlob extends CardImpl { this.toughness = new MageInt(1); // Consuming Blob's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.YOU) + ).addHint(CardTypesInGraveyardCount.YOU.getHint())); // At the beginning of your end step, create a green Ooze creature token with "This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1". this.addAbility(new BeginningOfEndStepTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/c/ConvertToSlime.java b/Mage.Sets/src/mage/cards/c/ConvertToSlime.java index f41238df2af..7390d1cf0f7 100644 --- a/Mage.Sets/src/mage/cards/c/ConvertToSlime.java +++ b/Mage.Sets/src/mage/cards/c/ConvertToSlime.java @@ -2,8 +2,8 @@ package mage.cards.c; import mage.abilities.Ability; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.OneShotEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -33,7 +33,7 @@ public final class ConvertToSlime extends CardImpl { this.getSpellAbility().addTarget(new TargetArtifactPermanent(0, 1)); this.getSpellAbility().addTarget(new TargetCreaturePermanent(0, 1)); this.getSpellAbility().addTarget(new TargetEnchantmentPermanent(0, 1)); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private ConvertToSlime(final ConvertToSlime card) { diff --git a/Mage.Sets/src/mage/cards/c/CropSigil.java b/Mage.Sets/src/mage/cards/c/CropSigil.java index 7ea00a9e010..e5e3284ebe9 100644 --- a/Mage.Sets/src/mage/cards/c/CropSigil.java +++ b/Mage.Sets/src/mage/cards/c/CropSigil.java @@ -3,6 +3,7 @@ package mage.cards.c; import java.util.UUID; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.costs.common.SacrificeSourceCost; @@ -10,7 +11,6 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalActivatedAbility; import mage.abilities.effects.common.MillCardsControllerEffect; import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -42,7 +42,7 @@ public final class CropSigil extends CardImpl { ability.addTarget(new TargetCardInYourGraveyard(0, 1, StaticFilters.FILTER_CARD_CREATURE)); ability.addTarget(new TargetCardInYourGraveyard(0, 1, StaticFilters.FILTER_CARD_LAND)); ability.setAbilityWord(AbilityWord.DELIRIUM); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/c/CryptOfAgadeem.java b/Mage.Sets/src/mage/cards/c/CryptOfAgadeem.java index cb2ed04e2df..246ef8b4796 100644 --- a/Mage.Sets/src/mage/cards/c/CryptOfAgadeem.java +++ b/Mage.Sets/src/mage/cards/c/CryptOfAgadeem.java @@ -7,7 +7,9 @@ import mage.ObjectColor; import mage.abilities.common.EntersBattlefieldTappedAbility; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; +import mage.abilities.hint.ValueHint; import mage.abilities.mana.BlackManaAbility; import mage.abilities.mana.DynamicManaAbility; import mage.cards.CardImpl; @@ -36,8 +38,10 @@ public final class CryptOfAgadeem extends CardImpl { // {T}: Add {B}. this.addAbility(new BlackManaAbility()); // {2}, {T}: Add {B} for each black creature card in your graveyard. - DynamicManaAbility ability = new DynamicManaAbility(Mana.BlackMana(1), new CardsInControllerGraveyardCount(filter), new GenericManaCost(2)); + DynamicValue blackCardsInGraveyard = new CardsInControllerGraveyardCount(filter); + DynamicManaAbility ability = new DynamicManaAbility(Mana.BlackMana(1), blackCardsInGraveyard, new GenericManaCost(2)); ability.addCost(new TapSourceCost()); + ability.addHint(new ValueHint("Black creature cards in your graveyard", blackCardsInGraveyard)); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/c/CryptcallerChariot.java b/Mage.Sets/src/mage/cards/c/CryptcallerChariot.java new file mode 100644 index 00000000000..0b1cb3afe56 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CryptcallerChariot.java @@ -0,0 +1,49 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.dynamicvalue.common.SavedDiscardValue; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DiscardOneOrMoreCardsTriggeredAbility; +import mage.abilities.keyword.CrewAbility; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.game.permanent.token.ZombieToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CryptcallerChariot extends CardImpl { + + public CryptcallerChariot(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}{B}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(5); + this.toughness = new MageInt(5); + + // Menace + this.addAbility(new MenaceAbility()); + + // Whenever you discard one or more cards, create that many tapped 2/2 black Zombie creature tokens. + this.addAbility(new DiscardOneOrMoreCardsTriggeredAbility(new CreateTokenEffect( + new ZombieToken(), SavedDiscardValue.MANY, true, false + ))); + + // Crew 2 + this.addAbility(new CrewAbility(2)); + } + + private CryptcallerChariot(final CryptcallerChariot card) { + super(card); + } + + @Override + public CryptcallerChariot copy() { + return new CryptcallerChariot(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/CrypticTrilobite.java b/Mage.Sets/src/mage/cards/c/CrypticTrilobite.java index 9d8b0275a0e..3fb87094626 100644 --- a/Mage.Sets/src/mage/cards/c/CrypticTrilobite.java +++ b/Mage.Sets/src/mage/cards/c/CrypticTrilobite.java @@ -1,13 +1,9 @@ package mage.cards.c; -import mage.ConditionalMana; import mage.MageInt; -import mage.Mana; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.condition.Condition; -import mage.abilities.costs.Cost; import mage.abilities.costs.common.RemoveCountersSourceCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.GenericManaCost; @@ -15,14 +11,12 @@ import mage.abilities.dynamicvalue.common.CountersSourceCount; import mage.abilities.effects.common.EntersBattlefieldWithXCountersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.mana.ConditionalColorlessManaAbility; -import mage.abilities.mana.builder.ConditionalManaBuilder; -import mage.abilities.mana.conditional.ManaCondition; +import mage.abilities.mana.builder.common.ActivatedAbilityManaBuilder; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.counters.CounterType; -import mage.game.Game; import java.util.UUID; @@ -46,7 +40,7 @@ public final class CrypticTrilobite extends CardImpl { // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. this.addAbility(new ConditionalColorlessManaAbility( new RemoveCountersSourceCost(CounterType.P1P1.createInstance()), - 2, new CrypticTrilobiteManaBuilder(), + 2, new ActivatedAbilityManaBuilder(), new CountersSourceCount(CounterType.P1P1) )); @@ -67,40 +61,3 @@ public final class CrypticTrilobite extends CardImpl { return new CrypticTrilobite(this); } } - -class CrypticTrilobiteManaBuilder extends ConditionalManaBuilder { - - @Override - public ConditionalMana build(Object... options) { - return new CrypticTrilobiteConditionalMana(this.mana); - } - - @Override - public String getRule() { - return "Spend this mana only to activate abilities"; - } -} - -class CrypticTrilobiteConditionalMana extends ConditionalMana { - - CrypticTrilobiteConditionalMana(Mana mana) { - super(mana); - staticText = "Spend this mana only to activate abilities"; - addCondition(new CrypticTrilobiteManaCondition()); - } -} - -class CrypticTrilobiteManaCondition extends ManaCondition implements Condition { - - @Override - public boolean apply(Game game, Ability source) { - return source != null - && !source.isActivated() - && source.isActivatedAbility(); - } - - @Override - public boolean apply(Game game, Ability source, UUID originalId, Cost costsToPay) { - return apply(game, source); - } -} diff --git a/Mage.Sets/src/mage/cards/c/CutADeal.java b/Mage.Sets/src/mage/cards/c/CutADeal.java index 386083500bd..11473d97e10 100644 --- a/Mage.Sets/src/mage/cards/c/CutADeal.java +++ b/Mage.Sets/src/mage/cards/c/CutADeal.java @@ -53,7 +53,7 @@ class CutADealEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { int count = game - .getOpponents(source.getControllerId()) + .getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/d/DaringDemolition.java b/Mage.Sets/src/mage/cards/d/DaringDemolition.java index c122b858cd4..3b1164303d6 100644 --- a/Mage.Sets/src/mage/cards/d/DaringDemolition.java +++ b/Mage.Sets/src/mage/cards/d/DaringDemolition.java @@ -1,34 +1,25 @@ - package mage.cards.d; -import java.util.UUID; import mage.abilities.effects.common.DestroyTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class DaringDemolition extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or(CardType.CREATURE.getPredicate(), SubType.VEHICLE.getPredicate())); - } - public DaringDemolition(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}{B}"); // Destroy target creature or Vehicle. - getSpellAbility().addEffect(new DestroyTargetEffect()); - getSpellAbility().addTarget(new TargetPermanent(filter)); + this.getSpellAbility().addEffect(new DestroyTargetEffect()); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); } private DaringDemolition(final DaringDemolition card) { diff --git a/Mage.Sets/src/mage/cards/d/DeadDrop.java b/Mage.Sets/src/mage/cards/d/DeadDrop.java index 0d93dddd5d0..4ed711d4ca3 100644 --- a/Mage.Sets/src/mage/cards/d/DeadDrop.java +++ b/Mage.Sets/src/mage/cards/d/DeadDrop.java @@ -20,7 +20,8 @@ public final class DeadDrop extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{9}{B}"); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); + // Target player sacrifices two creatures this.getSpellAbility().addEffect(new SacrificeEffect(StaticFilters.FILTER_PERMANENT_CREATURES, 2, "Target player")); this.getSpellAbility().addTarget(new TargetPlayer()); diff --git a/Mage.Sets/src/mage/cards/d/DeathRattle.java b/Mage.Sets/src/mage/cards/d/DeathRattle.java index 8b39cdaca2c..a3017aa83b8 100644 --- a/Mage.Sets/src/mage/cards/d/DeathRattle.java +++ b/Mage.Sets/src/mage/cards/d/DeathRattle.java @@ -29,7 +29,7 @@ public final class DeathRattle extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{5}{B}"); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // Destroy target nongreen creature. It can't be regenerated. this.getSpellAbility().addEffect(new DestroyTargetEffect(true)); diff --git a/Mage.Sets/src/mage/cards/d/DeathcapCultivator.java b/Mage.Sets/src/mage/cards/d/DeathcapCultivator.java index 490a2e1f369..bb42ac47a4b 100644 --- a/Mage.Sets/src/mage/cards/d/DeathcapCultivator.java +++ b/Mage.Sets/src/mage/cards/d/DeathcapCultivator.java @@ -6,8 +6,8 @@ import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.DeathtouchAbility; import mage.abilities.mana.BlackManaAbility; import mage.abilities.mana.GreenManaAbility; @@ -16,7 +16,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @author fireshoes @@ -38,7 +37,7 @@ public final class DeathcapCultivator extends CardImpl { this.addAbility(new SimpleStaticAbility( new ConditionalContinuousEffect(new GainAbilitySourceEffect(DeathtouchAbility.getInstance(), Duration.WhileOnBattlefield), DeliriumCondition.instance, "Delirium — {this} has deathtouch as long as there are four or more card types among cards in your graveyard")) - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); } private DeathcapCultivator(final DeathcapCultivator card) { diff --git a/Mage.Sets/src/mage/cards/d/DelugeOfDoom.java b/Mage.Sets/src/mage/cards/d/DelugeOfDoom.java index a8cf96890c3..7ced7fe5dd7 100644 --- a/Mage.Sets/src/mage/cards/d/DelugeOfDoom.java +++ b/Mage.Sets/src/mage/cards/d/DelugeOfDoom.java @@ -4,7 +4,6 @@ import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.dynamicvalue.common.SignInversionDynamicValue; import mage.abilities.effects.common.continuous.BoostAllEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -25,7 +24,7 @@ public final class DelugeOfDoom extends CardImpl { // All creatures get -X/-X until end of turn, where X is the number of card types among cards in your graveyard. this.getSpellAbility().addEffect(new BoostAllEffect(xValue, xValue, Duration.EndOfTurn) .setText("all creatures get -X/-X until end of turn, where X is the number of card types among cards in your graveyard")); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private DelugeOfDoom(final DelugeOfDoom card) { diff --git a/Mage.Sets/src/mage/cards/d/DemolisherSpawn.java b/Mage.Sets/src/mage/cards/d/DemolisherSpawn.java index 4a236b49b07..952cff306cb 100644 --- a/Mage.Sets/src/mage/cards/d/DemolisherSpawn.java +++ b/Mage.Sets/src/mage/cards/d/DemolisherSpawn.java @@ -4,8 +4,8 @@ import mage.MageInt; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostAllEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.HasteAbility; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; @@ -43,7 +43,7 @@ public final class DemolisherSpawn extends CardImpl { StaticFilters.FILTER_ATTACKING_CREATURES, true )), DeliriumCondition.instance, "Whenever {this} attacks, if there are four or more " + "card types among cards in your graveyard, other attacking creatures get +4/+4 until end of turn." - ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private DemolisherSpawn(final DemolisherSpawn card) { diff --git a/Mage.Sets/src/mage/cards/d/DemonicCounsel.java b/Mage.Sets/src/mage/cards/d/DemonicCounsel.java index 2cbfc256e9d..d48798f170d 100644 --- a/Mage.Sets/src/mage/cards/d/DemonicCounsel.java +++ b/Mage.Sets/src/mage/cards/d/DemonicCounsel.java @@ -2,8 +2,8 @@ package mage.cards.d; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -38,7 +38,7 @@ public final class DemonicCounsel extends CardImpl { "If there are four or more card types among cards in your graveyard, " + "instead search your library for any card, put it into your hand, then shuffle." )); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private DemonicCounsel(final DemonicCounsel card) { diff --git a/Mage.Sets/src/mage/cards/d/DescendUponTheSinful.java b/Mage.Sets/src/mage/cards/d/DescendUponTheSinful.java index 364aa505c4f..7b6bcfc6da5 100644 --- a/Mage.Sets/src/mage/cards/d/DescendUponTheSinful.java +++ b/Mage.Sets/src/mage/cards/d/DescendUponTheSinful.java @@ -4,10 +4,10 @@ import java.util.UUID; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.Effect; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.ExileAllEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -29,7 +29,7 @@ public final class DescendUponTheSinful extends CardImpl { Effect effect = new ConditionalOneShotEffect(new CreateTokenEffect(new AngelToken()), DeliriumCondition.instance); effect.setText("
Delirium — Create a 4/4 white Angel creature token with flying if there are four or more card types among cards in your graveyard"); this.getSpellAbility().addEffect(effect); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private DescendUponTheSinful(final DescendUponTheSinful card) { diff --git a/Mage.Sets/src/mage/cards/d/DesperateSentry.java b/Mage.Sets/src/mage/cards/d/DesperateSentry.java index 1035a151ee7..ffe523efb86 100644 --- a/Mage.Sets/src/mage/cards/d/DesperateSentry.java +++ b/Mage.Sets/src/mage/cards/d/DesperateSentry.java @@ -8,15 +8,14 @@ import mage.abilities.common.DiesSourceTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.continuous.BoostSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; import mage.game.permanent.token.EldraziHorrorToken; /** @@ -39,7 +38,7 @@ public final class DesperateSentry extends CardImpl { new BoostSourceEffect(3, 0, Duration.WhileOnBattlefield), DeliriumCondition.instance, "Delirium — {this} gets +3/+0 as long as there are four or more card types among cards in your graveyard."); Ability ability = new SimpleStaticAbility(effect); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/d/DigThroughTime.java b/Mage.Sets/src/mage/cards/d/DigThroughTime.java index 0e2b3f88492..27d8f834b10 100644 --- a/Mage.Sets/src/mage/cards/d/DigThroughTime.java +++ b/Mage.Sets/src/mage/cards/d/DigThroughTime.java @@ -18,7 +18,7 @@ public final class DigThroughTime extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{6}{U}{U}"); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // Look at the top seven cards of your library. Put two of them into your hand and the rest on the bottom of your library in any order. this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect(7, 2, PutCards.HAND, PutCards.BOTTOM_ANY)); diff --git a/Mage.Sets/src/mage/cards/d/DragToTheRoots.java b/Mage.Sets/src/mage/cards/d/DragToTheRoots.java index 39072165c39..bd9889f4681 100644 --- a/Mage.Sets/src/mage/cards/d/DragToTheRoots.java +++ b/Mage.Sets/src/mage/cards/d/DragToTheRoots.java @@ -2,9 +2,9 @@ package mage.cards.d; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.DestroyTargetEffect; import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -26,7 +26,7 @@ public final class DragToTheRoots extends CardImpl { this.addAbility(new SimpleStaticAbility( Zone.ALL, new SpellCostReductionSourceEffect(2, DeliriumCondition.instance) .setText("this spell costs {2} less to cast as long as there are four or more card types among cards in your graveyard") - ).setRuleAtTheTop(true).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + ).setRuleAtTheTop(true).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); // Destroy target nonland permanent. this.getSpellAbility().addEffect(new DestroyTargetEffect()); diff --git a/Mage.Sets/src/mage/cards/d/DragonsRageChanneler.java b/Mage.Sets/src/mage/cards/d/DragonsRageChanneler.java index e506d0f759b..c8f03aed66c 100644 --- a/Mage.Sets/src/mage/cards/d/DragonsRageChanneler.java +++ b/Mage.Sets/src/mage/cards/d/DragonsRageChanneler.java @@ -7,11 +7,11 @@ import mage.abilities.common.SpellCastControllerTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; import mage.abilities.decorator.ConditionalRequirementEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.combat.AttacksIfAbleSourceEffect; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; import mage.abilities.effects.keyword.SurveilEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -55,7 +55,7 @@ public final class DragonsRageChanneler extends CardImpl { DeliriumCondition.instance, ", and attacks each combat if able" )); ability.setAbilityWord(AbilityWord.DELIRIUM); - this.addAbility(ability.addHint(CardTypesInGraveyardHint.YOU)); + this.addAbility(ability.addHint(CardTypesInGraveyardCount.YOU.getHint())); } private DragonsRageChanneler(final DragonsRageChanneler card) { diff --git a/Mage.Sets/src/mage/cards/d/DuskFeaster.java b/Mage.Sets/src/mage/cards/d/DuskFeaster.java index f9eec9322ea..6e61fc698b7 100644 --- a/Mage.Sets/src/mage/cards/d/DuskFeaster.java +++ b/Mage.Sets/src/mage/cards/d/DuskFeaster.java @@ -4,8 +4,8 @@ import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -31,7 +31,7 @@ public final class DuskFeaster extends CardImpl { Ability ability = new SimpleStaticAbility(Zone.ALL, new SpellCostReductionSourceEffect(2, DeliriumCondition.instance)); ability.setRuleAtTheTop(true); ability.setAbilityWord(AbilityWord.DELIRIUM); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); // Flying diff --git a/Mage.Sets/src/mage/cards/d/DwynensElite.java b/Mage.Sets/src/mage/cards/d/DwynensElite.java index a13e8ac859a..0e030e313fe 100644 --- a/Mage.Sets/src/mage/cards/d/DwynensElite.java +++ b/Mage.Sets/src/mage/cards/d/DwynensElite.java @@ -35,12 +35,12 @@ public final class DwynensElite extends CardImpl { this.power = new MageInt(2); this.toughness = new MageInt(2); - // When Dwynen's Elite enters the battlefield, if you control another Elf, create a 1/1 green Elf Warrior creature token. + // When this creature enters, if you control another Elf, create a 1/1 green Elf Warrior creature token. TriggeredAbility triggeredAbility = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new ElfWarriorToken())); this.addAbility(new ConditionalInterveningIfTriggeredAbility( triggeredAbility, new PermanentsOnTheBattlefieldCondition(filter), - "When {this} enters, if you control another Elf, create a 1/1 green Elf Warrior creature token.")); + "When this creature enters, if you control another Elf, create a 1/1 green Elf Warrior creature token.")); } private DwynensElite(final DwynensElite card) { diff --git a/Mage.Sets/src/mage/cards/e/EmptyThePits.java b/Mage.Sets/src/mage/cards/e/EmptyThePits.java index 2086a177778..9a9b63733f5 100644 --- a/Mage.Sets/src/mage/cards/e/EmptyThePits.java +++ b/Mage.Sets/src/mage/cards/e/EmptyThePits.java @@ -21,7 +21,7 @@ public final class EmptyThePits extends CardImpl { // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // create X 2/2 black Zombie creature tokens tapped. this.getSpellAbility().addEffect(new CreateTokenEffect(new ZombieToken(), GetXValue.instance, true, false)); diff --git a/Mage.Sets/src/mage/cards/e/EmrakulThePromisedEnd.java b/Mage.Sets/src/mage/cards/e/EmrakulThePromisedEnd.java index 796a062865f..f670fc0c853 100644 --- a/Mage.Sets/src/mage/cards/e/EmrakulThePromisedEnd.java +++ b/Mage.Sets/src/mage/cards/e/EmrakulThePromisedEnd.java @@ -7,7 +7,6 @@ import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CastSourceTriggeredAbility; import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.ProtectionAbility; import mage.abilities.keyword.TrampleAbility; @@ -46,7 +45,7 @@ public final class EmrakulThePromisedEnd extends CardImpl { new SpellCostReductionForEachSourceEffect( 1, CardTypesInGraveyardCount.YOU ).setText("this spell costs {1} less to cast for each card type among cards in your graveyard") - ).setRuleAtTheTop(true).addHint(CardTypesInGraveyardHint.YOU)); + ).setRuleAtTheTop(true).addHint(CardTypesInGraveyardCount.YOU.getHint())); // When you cast Emrakul, you gain control of target opponent during that player's next turn. After that turn, that player takes an extra turn. Ability ability = new CastSourceTriggeredAbility(new EmrakulThePromisedEndGainControlEffect()); diff --git a/Mage.Sets/src/mage/cards/e/EndriderCatalyzer.java b/Mage.Sets/src/mage/cards/e/EndriderCatalyzer.java index d183da2f11b..ee03b0610df 100644 --- a/Mage.Sets/src/mage/cards/e/EndriderCatalyzer.java +++ b/Mage.Sets/src/mage/cards/e/EndriderCatalyzer.java @@ -32,7 +32,7 @@ public final class EndriderCatalyzer extends CardImpl { // Max speed -- {T}: Add {R}{R}. this.addAbility(new MaxSpeedAbility(new SimpleManaAbility( - Zone.BATTLEFIELD, Mana.GreenMana(2), new TapSourceCost() + Zone.BATTLEFIELD, Mana.RedMana(2), new TapSourceCost() ))); } diff --git a/Mage.Sets/src/mage/cards/e/EtherealForager.java b/Mage.Sets/src/mage/cards/e/EtherealForager.java index 8fa7d93bf1c..5b748c72246 100644 --- a/Mage.Sets/src/mage/cards/e/EtherealForager.java +++ b/Mage.Sets/src/mage/cards/e/EtherealForager.java @@ -37,7 +37,7 @@ public final class EtherealForager extends CardImpl { this.toughness = new MageInt(3); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(true)); // Flying this.addAbility(FlyingAbility.getInstance()); diff --git a/Mage.Sets/src/mage/cards/e/ExtricatorOfSin.java b/Mage.Sets/src/mage/cards/e/ExtricatorOfSin.java index 0857484b68b..9187fc51e56 100644 --- a/Mage.Sets/src/mage/cards/e/ExtricatorOfSin.java +++ b/Mage.Sets/src/mage/cards/e/ExtricatorOfSin.java @@ -3,6 +3,7 @@ package mage.cards.e; import java.util.UUID; import mage.MageInt; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; @@ -11,7 +12,6 @@ import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.TransformSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -52,7 +52,7 @@ public final class ExtricatorOfSin extends CardImpl { DeliriumCondition.instance, "Delirium — At the beginning of your upkeep, if there are four or more card types among cards in your graveyard, " + " transform {this}.") - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); } private ExtricatorOfSin(final ExtricatorOfSin card) { diff --git a/Mage.Sets/src/mage/cards/f/FangGuardian.java b/Mage.Sets/src/mage/cards/f/FangGuardian.java new file mode 100644 index 00000000000..e284726a4ed --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FangGuardian.java @@ -0,0 +1,61 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.keyword.FlashAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FangGuardian extends CardImpl { + + private static final FilterPermanent filter + = new FilterControlledPermanent("another target creature or Vehicle you control"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + + public FangGuardian(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}"); + + this.subtype.add(SubType.APE); + this.subtype.add(SubType.DRUID); + this.power = new MageInt(4); + this.toughness = new MageInt(2); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // When this creature enters, another target creature or Vehicle you control gets +2/+2 until end of turn. + Ability ability = new EntersBattlefieldTriggeredAbility(new BoostTargetEffect(2, 2)); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + } + + private FangGuardian(final FangGuardian card) { + super(card); + } + + @Override + public FangGuardian copy() { + return new FangGuardian(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FarFortuneEndBoss.java b/Mage.Sets/src/mage/cards/f/FarFortuneEndBoss.java new file mode 100644 index 00000000000..fd54d9c1e11 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FarFortuneEndBoss.java @@ -0,0 +1,99 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksWithCreaturesTriggeredAbility; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.DamagePlayersEffect; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.util.CardUtil; + +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FarFortuneEndBoss extends CardImpl { + + public FarFortuneEndBoss(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.MERCENARY); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // Whenever you attack, Far Fortune deals 1 damage to each opponent. + this.addAbility(new AttacksWithCreaturesTriggeredAbility( + new DamagePlayersEffect(1, TargetController.OPPONENT), 1 + )); + + // Max speed -- If a source you control would deal damage to an opponent or a permanent an opponent controls, it deals that much damage plus 1 instead. + this.addAbility(new MaxSpeedAbility(new FarFortuneEndBossEffect())); + } + + private FarFortuneEndBoss(final FarFortuneEndBoss card) { + super(card); + } + + @Override + public FarFortuneEndBoss copy() { + return new FarFortuneEndBoss(this); + } +} + +class FarFortuneEndBossEffect extends ReplacementEffectImpl { + + FarFortuneEndBossEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "if a source you control would deal damage to an opponent " + + "or a permanent an opponent controls, it deals that much damage plus 1 instead"; + } + + private FarFortuneEndBossEffect(final FarFortuneEndBossEffect effect) { + super(effect); + } + + @Override + public FarFortuneEndBossEffect copy() { + return new FarFortuneEndBossEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + switch (event.getType()) { + case DAMAGE_PERMANENT: + case DAMAGE_PLAYER: + return true; + default: + return false; + } + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (!source.isControlledBy(game.getControllerId(event.getSourceId()))) { + return false; + } + Set opponents = game.getOpponents(source.getControllerId()); + return opponents.contains(event.getTargetId()) + && opponents.contains(game.getControllerId(event.getTargetId())); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + event.setAmount(CardUtil.overflowInc(event.getAmount(), 1)); + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/f/FearOfBurningAlive.java b/Mage.Sets/src/mage/cards/f/FearOfBurningAlive.java index 11c737ebbf4..0f04e9a404f 100644 --- a/Mage.Sets/src/mage/cards/f/FearOfBurningAlive.java +++ b/Mage.Sets/src/mage/cards/f/FearOfBurningAlive.java @@ -4,10 +4,10 @@ import mage.MageInt; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.DamagePlayersEffect; import mage.abilities.effects.common.DamageTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -60,7 +60,7 @@ class FearOfBurningAliveTriggeredAbility extends TriggeredAbilityImpl { this.setTriggerPhrase("Whenever a source you control deals noncombat damage to an opponent, " + "if there are four or more card types among cards in your graveyard, "); this.setAbilityWord(AbilityWord.DELIRIUM); - this.addHint(CardTypesInGraveyardHint.YOU); + this.addHint(CardTypesInGraveyardCount.YOU.getHint()); } private FearOfBurningAliveTriggeredAbility(final FearOfBurningAliveTriggeredAbility ability) { diff --git a/Mage.Sets/src/mage/cards/f/FearOfMissingOut.java b/Mage.Sets/src/mage/cards/f/FearOfMissingOut.java index 863e38cab8e..fc56747b5da 100644 --- a/Mage.Sets/src/mage/cards/f/FearOfMissingOut.java +++ b/Mage.Sets/src/mage/cards/f/FearOfMissingOut.java @@ -6,11 +6,11 @@ import mage.abilities.common.AttacksFirstTimeTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.AdditionalCombatPhaseEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.UntapTargetEffect; import mage.abilities.effects.common.discard.DiscardControllerEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -45,7 +45,7 @@ public final class FearOfMissingOut extends CardImpl { ); ability.addTarget(new TargetCreaturePermanent()); ability.addEffect(new AdditionalCombatPhaseEffect()); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/f/FightRigging.java b/Mage.Sets/src/mage/cards/f/FightRigging.java index 7ea5ed79be4..105fcedbc3f 100644 --- a/Mage.Sets/src/mage/cards/f/FightRigging.java +++ b/Mage.Sets/src/mage/cards/f/FightRigging.java @@ -37,7 +37,7 @@ public final class FightRigging extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); // Hideaway 5 - this.addAbility(new HideawayAbility(5)); + this.addAbility(new HideawayAbility(this, 5)); // At the beginning of combat on your turn, put a +1/+1 counter on target creature you control. Then if you control a creature with power 7 or greater, you may play the exiled card without paying its mana cost. Ability ability = new BeginningOfCombatTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/f/FloodTheEngine.java b/Mage.Sets/src/mage/cards/f/FloodTheEngine.java new file mode 100644 index 00000000000..9a84b3133fd --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FloodTheEngine.java @@ -0,0 +1,57 @@ +package mage.cards.f; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.DontUntapInControllersUntapStepEnchantedEffect; +import mage.abilities.effects.common.TapEnchantedEffect; +import mage.abilities.effects.common.continuous.LoseAllAbilitiesAttachedEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AttachmentType; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FloodTheEngine extends CardImpl { + + public FloodTheEngine(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{U}"); + + this.subtype.add(SubType.AURA); + + // Enchant creature or Vehicle + TargetPermanent auraTarget = new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + this.addAbility(new EnchantAbility(auraTarget)); + + // When this Aura enters, tap enchanted permanent. + this.addAbility(new EntersBattlefieldTriggeredAbility(new TapEnchantedEffect("permanent"))); + + // Enchanted permanent loses all abilities and doesn't untap during its controller's untap step. + Ability ability = new SimpleStaticAbility(new LoseAllAbilitiesAttachedEffect(AttachmentType.AURA) + .setText("enchanted permanent loses all abilities")); + ability.addEffect(new DontUntapInControllersUntapStepEnchantedEffect() + .setText("and doesn't untap during its controller's untap step")); + this.addAbility(ability); + } + + private FloodTheEngine(final FloodTheEngine card) { + super(card); + } + + @Override + public FloodTheEngine copy() { + return new FloodTheEngine(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FlotsamJetsam.java b/Mage.Sets/src/mage/cards/f/FlotsamJetsam.java index e99e79682c3..f9ec20d4cd8 100644 --- a/Mage.Sets/src/mage/cards/f/FlotsamJetsam.java +++ b/Mage.Sets/src/mage/cards/f/FlotsamJetsam.java @@ -77,7 +77,7 @@ class JetsamEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - Set opponents = game.getOpponents(source.getControllerId()) + Set opponents = game.getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/f/FoulWatcher.java b/Mage.Sets/src/mage/cards/f/FoulWatcher.java index 71f022b921f..2dd18f44992 100644 --- a/Mage.Sets/src/mage/cards/f/FoulWatcher.java +++ b/Mage.Sets/src/mage/cards/f/FoulWatcher.java @@ -5,9 +5,9 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.keyword.SurveilEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -41,7 +41,7 @@ public final class FoulWatcher extends CardImpl { this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect( new BoostSourceEffect(1, 0, Duration.WhileOnBattlefield), DeliriumCondition.instance, "{this} gets +1/+0 as long as there are four or more card types among cards in your graveyard" - )).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + )).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private FoulWatcher(final FoulWatcher card) { diff --git a/Mage.Sets/src/mage/cards/f/FungalSprouting.java b/Mage.Sets/src/mage/cards/f/FungalSprouting.java index 24fd99a83d5..0853e074cc3 100644 --- a/Mage.Sets/src/mage/cards/f/FungalSprouting.java +++ b/Mage.Sets/src/mage/cards/f/FungalSprouting.java @@ -18,8 +18,9 @@ public final class FungalSprouting extends CardImpl { public FungalSprouting(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{G}"); - // create X 1/1 green Saproling creature tokens, where X is the greatest power among creatures you control. + // Create X 1/1 green Saproling creature tokens, where X is the greatest power among creatures you control. this.getSpellAbility().addEffect(new CreateTokenEffect(new SaprolingToken(), GreatestPowerAmongControlledCreaturesValue.instance)); + this.getSpellAbility().addHint(GreatestPowerAmongControlledCreaturesValue.getHint()); } private FungalSprouting(final FungalSprouting card) { diff --git a/Mage.Sets/src/mage/cards/g/GarrukTheVeilCursed.java b/Mage.Sets/src/mage/cards/g/GarrukTheVeilCursed.java index 2482fa5c7b9..4bd063d6f6f 100644 --- a/Mage.Sets/src/mage/cards/g/GarrukTheVeilCursed.java +++ b/Mage.Sets/src/mage/cards/g/GarrukTheVeilCursed.java @@ -20,7 +20,6 @@ import mage.constants.SuperType; import mage.filter.StaticFilters; import mage.game.permanent.token.WolfTokenWithDeathtouch; import mage.target.common.TargetCardInLibrary; -import mage.target.common.TargetControlledPermanent; import java.util.UUID; diff --git a/Mage.Sets/src/mage/cards/g/GastalBlockbuster.java b/Mage.Sets/src/mage/cards/g/GastalBlockbuster.java new file mode 100644 index 00000000000..1031d61291e --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GastalBlockbuster.java @@ -0,0 +1,48 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.effects.common.DoWhenCostPaid; +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 GastalBlockbuster extends CardImpl { + + public GastalBlockbuster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.BERSERKER); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // When this creature enters, you may sacrifice a creature or Vehicle. When you do, destroy target artifact an opponent controls. + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(new DestroyTargetEffect(), false); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_ARTIFACT)); + this.addAbility(new EntersBattlefieldTriggeredAbility(new DoWhenCostPaid( + ability, new SacrificeTargetCost(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE), + "sacrifice a creature or Vehicle" + ))); + } + + private GastalBlockbuster(final GastalBlockbuster card) { + super(card); + } + + @Override + public GastalBlockbuster copy() { + return new GastalBlockbuster(this); + } +} diff --git a/Mage.Sets/src/mage/cards/g/GastalRaider.java b/Mage.Sets/src/mage/cards/g/GastalRaider.java new file mode 100644 index 00000000000..3d0ca604a3c --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GastalRaider.java @@ -0,0 +1,58 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.effects.common.discard.DiscardCardYouChooseTargetEffect; +import mage.abilities.keyword.MenaceAbility; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GastalRaider extends CardImpl { + + public GastalRaider(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}"); + + this.subtype.add(SubType.VAMPIRE); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // When this creature enters, target opponent reveals their hand. You choose an instant or sorcery card from it. That player discards that card. + Ability ability = new EntersBattlefieldTriggeredAbility(new DiscardCardYouChooseTargetEffect(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY)); + ability.addTarget(new TargetOpponent()); + this.addAbility(ability); + + // Max speed -- This creature gets +1/+1 and has menace. + ability = new SimpleStaticAbility(new BoostSourceEffect(1, 1, Duration.WhileOnBattlefield)); + ability.addEffect(new GainAbilitySourceEffect(new MenaceAbility(false)).setText("and has menace")); + this.addAbility(new MaxSpeedAbility(ability)); + } + + private GastalRaider(final GastalRaider card) { + super(card); + } + + @Override + public GastalRaider copy() { + return new GastalRaider(this); + } +} diff --git a/Mage.Sets/src/mage/cards/g/GastalThrillseeker.java b/Mage.Sets/src/mage/cards/g/GastalThrillseeker.java new file mode 100644 index 00000000000..0f8fb9817f3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GastalThrillseeker.java @@ -0,0 +1,58 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GastalThrillseeker extends CardImpl { + + public GastalThrillseeker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{B}{R}"); + + this.subtype.add(SubType.LIZARD); + this.subtype.add(SubType.BERSERKER); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // When this creature enters, it deals 1 damage to target opponent and you gain 1 life. + Ability ability = new EntersBattlefieldTriggeredAbility(new DamageTargetEffect(1, "it")); + ability.addEffect(new GainLifeEffect(1).concatBy("and")); + ability.addTarget(new TargetOpponent()); + this.addAbility(ability); + + // Max speed -- This creature has deathtouch and haste. + ability = new SimpleStaticAbility(new GainAbilitySourceEffect(DeathtouchAbility.getInstance())); + ability.addEffect(new GainAbilitySourceEffect(HasteAbility.getInstance()).setText("and haste")); + this.addAbility(new MaxSpeedAbility(ability)); + } + + private GastalThrillseeker(final GastalThrillseeker card) { + super(card); + } + + @Override + public GastalThrillseeker copy() { + return new GastalThrillseeker(this); + } +} diff --git a/Mage.Sets/src/mage/cards/g/GatekeeperOfMalakir.java b/Mage.Sets/src/mage/cards/g/GatekeeperOfMalakir.java index 3a6c64a0c6d..b45be20251f 100644 --- a/Mage.Sets/src/mage/cards/g/GatekeeperOfMalakir.java +++ b/Mage.Sets/src/mage/cards/g/GatekeeperOfMalakir.java @@ -40,10 +40,11 @@ public final class GatekeeperOfMalakir extends CardImpl { // Kicker {B} (You may pay an additional {B} as you cast this spell.) this.addAbility(new KickerAbility("{B}")); - // When Gatekeeper of Malakir enters the battlefield, if it was kicked, target player sacrifices a creature. + // When this creature enters, if it was kicked, target player sacrifices a creature. EntersBattlefieldTriggeredAbility ability = - new EntersBattlefieldTriggeredAbility(new SacrificeEffect(filter, 1, "target player")); - Ability conditionalAbility = new ConditionalInterveningIfTriggeredAbility(ability, KickedCondition.ONCE, "When {this} enters, if it was kicked, target player sacrifices a creature."); + new EntersBattlefieldTriggeredAbility(new SacrificeEffect(filter, 1, "target player")) + .setTriggerPhrase("When this creature enters, "); + Ability conditionalAbility = new ConditionalInterveningIfTriggeredAbility(ability, KickedCondition.ONCE, "When this creature enters, if it was kicked, target player sacrifices a creature."); conditionalAbility.addTarget(new TargetPlayer()); this.addAbility(conditionalAbility); } diff --git a/Mage.Sets/src/mage/cards/g/GearseekerSerpent.java b/Mage.Sets/src/mage/cards/g/GearseekerSerpent.java index fdb8c1f1746..581eff3fe0f 100644 --- a/Mage.Sets/src/mage/cards/g/GearseekerSerpent.java +++ b/Mage.Sets/src/mage/cards/g/GearseekerSerpent.java @@ -2,18 +2,14 @@ package mage.cards.g; import mage.MageInt; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.dynamicvalue.common.ArtifactYouControlCount; import mage.abilities.effects.common.combat.CantBeBlockedSourceEffect; -import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; -import mage.abilities.hint.common.ArtifactYouControlHint; +import mage.abilities.keyword.AffinityForArtifactsAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; import mage.constants.SubType; -import mage.constants.Zone; import java.util.UUID; @@ -28,15 +24,13 @@ public final class GearseekerSerpent extends CardImpl { this.power = new MageInt(5); this.toughness = new MageInt(6); - // Gearseeker Serpent costs {1} less to cast for each artifact you control - this.addAbility(new SimpleStaticAbility(Zone.ALL, - new SpellCostReductionForEachSourceEffect(1, ArtifactYouControlCount.instance) - ).addHint(ArtifactYouControlHint.instance)); + // Affinity for artfifacts + this.addAbility(new AffinityForArtifactsAbility()); - // 5U: Gearseeker Serpent can't be blocked this turn. + // {5}{U}: Gearseeker Serpent can't be blocked this turn. this.addAbility(new SimpleActivatedAbility( - new CantBeBlockedSourceEffect(Duration.EndOfTurn), - new ManaCostsImpl<>("{5}{U}"))); + new CantBeBlockedSourceEffect(Duration.EndOfTurn), new ManaCostsImpl<>("{5}{U}") + )); } private GearseekerSerpent(final GearseekerSerpent card) { diff --git a/Mage.Sets/src/mage/cards/g/GeistOfTheLonelyVigil.java b/Mage.Sets/src/mage/cards/g/GeistOfTheLonelyVigil.java index bc4bbf53e9e..111beec528c 100644 --- a/Mage.Sets/src/mage/cards/g/GeistOfTheLonelyVigil.java +++ b/Mage.Sets/src/mage/cards/g/GeistOfTheLonelyVigil.java @@ -6,9 +6,9 @@ import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.Effect; import mage.abilities.effects.common.combat.CanAttackAsThoughItDidntHaveDefenderSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.DefenderAbility; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; @@ -16,7 +16,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @author LevelX2 @@ -40,7 +39,7 @@ public final class GeistOfTheLonelyVigil extends CardImpl { new CanAttackAsThoughItDidntHaveDefenderSourceEffect(Duration.WhileOnBattlefield), DeliriumCondition.instance); effect.setText("Delirium — {this} can attack as though it didn't have defender as long as there are four or more card types among cards in your graveyard"); - this.addAbility(new SimpleStaticAbility(effect).addHint(CardTypesInGraveyardHint.YOU)); + this.addAbility(new SimpleStaticAbility(effect).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private GeistOfTheLonelyVigil(final GeistOfTheLonelyVigil card) { diff --git a/Mage.Sets/src/mage/cards/g/GibberingFiend.java b/Mage.Sets/src/mage/cards/g/GibberingFiend.java index b05c29f8241..4ff11c3ffdf 100644 --- a/Mage.Sets/src/mage/cards/g/GibberingFiend.java +++ b/Mage.Sets/src/mage/cards/g/GibberingFiend.java @@ -3,13 +3,13 @@ package mage.cards.g; import java.util.UUID; import mage.MageInt; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.DamagePlayersEffect; import mage.abilities.effects.common.DamageTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -37,7 +37,7 @@ public final class GibberingFiend extends CardImpl { DeliriumCondition.instance, "Delirium — At the beginning of each opponent's upkeep, if there are four or more card types among cards in your graveyard, " + "{this} deals 1 damage to that player.") - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); } private GibberingFiend(final GibberingFiend card) { diff --git a/Mage.Sets/src/mage/cards/g/GnarlwoodDryad.java b/Mage.Sets/src/mage/cards/g/GnarlwoodDryad.java index 0e28f5fbc9f..566d7974012 100644 --- a/Mage.Sets/src/mage/cards/g/GnarlwoodDryad.java +++ b/Mage.Sets/src/mage/cards/g/GnarlwoodDryad.java @@ -7,15 +7,14 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.DeathtouchAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @author fireshoes @@ -37,7 +36,7 @@ public final class GnarlwoodDryad extends CardImpl { new BoostSourceEffect(2, 2, Duration.WhileOnBattlefield), DeliriumCondition.instance, "Delirium — {this} gets +2/+2 as long as there are four or more card types among cards in your graveyard."); Ability ability = new SimpleStaticAbility(effect); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/g/GoblinBushwhacker.java b/Mage.Sets/src/mage/cards/g/GoblinBushwhacker.java index af531d8cbf6..aee5bf27ee4 100644 --- a/Mage.Sets/src/mage/cards/g/GoblinBushwhacker.java +++ b/Mage.Sets/src/mage/cards/g/GoblinBushwhacker.java @@ -34,7 +34,7 @@ public final class GoblinBushwhacker extends CardImpl { // Kicker {R} (You may pay an additional {R} as you cast this spell.) this.addAbility(new KickerAbility("{R}")); - // When Goblin Bushwhacker enters the battlefield, if it was kicked, creatures you control get +1/+0 and gain haste until end of turn. + // When this creature enters, if it was kicked, creatures you control get +1/+0 and gain haste until end of turn. EntersBattlefieldTriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new BoostControlledEffect(1, 0, Duration.EndOfTurn), false); ability.addEffect(new GainAbilityControlledEffect( HasteAbility.getInstance(), @@ -44,7 +44,7 @@ public final class GoblinBushwhacker extends CardImpl { this.addAbility(new ConditionalInterveningIfTriggeredAbility( ability, KickedCondition.ONCE, - "When {this} enters, " + "When this creature enters, " + "if it was kicked, " + "creatures you control get +1/+0 and gain haste until end of turn." )); diff --git a/Mage.Sets/src/mage/cards/g/GougedZealot.java b/Mage.Sets/src/mage/cards/g/GougedZealot.java index a8010194a03..e0bb7822c98 100644 --- a/Mage.Sets/src/mage/cards/g/GougedZealot.java +++ b/Mage.Sets/src/mage/cards/g/GougedZealot.java @@ -4,8 +4,8 @@ import mage.MageInt; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.DamageAllControlledTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.ReachAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -37,7 +37,7 @@ public final class GougedZealot extends CardImpl { new AttacksTriggeredAbility(new DamageAllControlledTargetEffect(1), false, null, SetTargetPointer.PLAYER), DeliriumCondition.instance, "Delirium — Whenever {this} attacks, if there are four or more card types among cards in your graveyard, {this} deals 1 damage to each creature defending player controls." - ).addHint(CardTypesInGraveyardHint.YOU)); + ).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private GougedZealot(final GougedZealot card) { diff --git a/Mage.Sets/src/mage/cards/g/GraftedGrowth.java b/Mage.Sets/src/mage/cards/g/GraftedGrowth.java index cd1c7924aa8..fffcd0c7724 100644 --- a/Mage.Sets/src/mage/cards/g/GraftedGrowth.java +++ b/Mage.Sets/src/mage/cards/g/GraftedGrowth.java @@ -14,9 +14,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.counters.CounterType; -import mage.filter.FilterPermanent; -import mage.filter.common.FilterControlledPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import mage.target.common.TargetLandPermanent; @@ -27,16 +25,6 @@ import java.util.UUID; */ public final class GraftedGrowth extends CardImpl { - private static final FilterPermanent filter - = new FilterControlledPermanent("creature or Vehicle you control"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public GraftedGrowth(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); @@ -52,7 +40,7 @@ public final class GraftedGrowth extends CardImpl { Ability ability = new EntersBattlefieldTriggeredAbility( new AddCountersTargetEffect(CounterType.P1P1.createInstance()) ); - ability.addTarget(new TargetPermanent(filter)); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE)); this.addAbility(ability); // Enchanted land has "{T}: Add two mana of any one color." diff --git a/Mage.Sets/src/mage/cards/g/GreenbeltGuardian.java b/Mage.Sets/src/mage/cards/g/GreenbeltGuardian.java index f00077baafa..d5d0d20920a 100644 --- a/Mage.Sets/src/mage/cards/g/GreenbeltGuardian.java +++ b/Mage.Sets/src/mage/cards/g/GreenbeltGuardian.java @@ -39,7 +39,7 @@ public final class GreenbeltGuardian extends CardImpl { // Exhaust -- {3}{G}: Put three +1/+1 counters on this creature. this.addAbility(new ExhaustAbility( - new AddCountersSourceEffect(CounterType.P1P1.createInstance()), new ManaCostsImpl<>("{3}{G}") + new AddCountersSourceEffect(CounterType.P1P1.createInstance(3)), new ManaCostsImpl<>("{3}{G}") )); } diff --git a/Mage.Sets/src/mage/cards/g/GrimFlayer.java b/Mage.Sets/src/mage/cards/g/GrimFlayer.java index bdacdade3b6..4cc289a1697 100644 --- a/Mage.Sets/src/mage/cards/g/GrimFlayer.java +++ b/Mage.Sets/src/mage/cards/g/GrimFlayer.java @@ -5,9 +5,9 @@ import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.keyword.SurveilEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -46,7 +46,7 @@ public final class GrimFlayer extends CardImpl { DeliriumCondition.instance, "{this} gets +2/+2 as long as there are " + "four or more card types among cards in your graveyard" ) - ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private GrimFlayer(final GrimFlayer card) { diff --git a/Mage.Sets/src/mage/cards/g/GrimJavelineer.java b/Mage.Sets/src/mage/cards/g/GrimJavelineer.java new file mode 100644 index 00000000000..d9c1ec6acb8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GrimJavelineer.java @@ -0,0 +1,50 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksWithCreaturesTriggeredAbility; +import mage.abilities.common.delayed.WhenTargetDiesDelayedTriggeredAbility; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.keyword.SurveilEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.target.common.TargetAttackingCreature; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GrimJavelineer extends CardImpl { + + public GrimJavelineer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // Whenever you attack, target attacking creature gets +1/+0 until end of turn. When that creature dies this turn, surveil 1. + Ability ability = new AttacksWithCreaturesTriggeredAbility( + new BoostTargetEffect(1, 0), 1 + ); + ability.addEffect(new CreateDelayedTriggeredAbilityEffect( + new WhenTargetDiesDelayedTriggeredAbility(new SurveilEffect(1)) + )); + ability.addTarget(new TargetAttackingCreature()); + this.addAbility(ability); + } + + private GrimJavelineer(final GrimJavelineer card) { + super(card); + } + + @Override + public GrimJavelineer copy() { + return new GrimJavelineer(this); + } +} diff --git a/Mage.Sets/src/mage/cards/g/GuidelightPathmaker.java b/Mage.Sets/src/mage/cards/g/GuidelightPathmaker.java index 5e8b6c2a7ed..43a1fcfdf95 100644 --- a/Mage.Sets/src/mage/cards/g/GuidelightPathmaker.java +++ b/Mage.Sets/src/mage/cards/g/GuidelightPathmaker.java @@ -37,7 +37,7 @@ public final class GuidelightPathmaker extends CardImpl { this.addAbility(VigilanceAbility.getInstance()); // When this Vehicle enters, you may search your library for an artifact card and reveal it. Put it onto the battlefield if its mana value is 2 or less. Otherwise, put it into your hand. Then shuffle. - this.addAbility(new EntersBattlefieldTriggeredAbility(new GuidelightPathmakerEffect())); + this.addAbility(new EntersBattlefieldTriggeredAbility(new GuidelightPathmakerEffect(), true)); // Crew 2 this.addAbility(new CrewAbility(2)); diff --git a/Mage.Sets/src/mage/cards/g/GuidelightSynergist.java b/Mage.Sets/src/mage/cards/g/GuidelightSynergist.java new file mode 100644 index 00000000000..a9613744133 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GuidelightSynergist.java @@ -0,0 +1,48 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.ArtifactYouControlCount; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.hint.common.ArtifactYouControlHint; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GuidelightSynergist extends CardImpl { + + public GuidelightSynergist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{3}{W}"); + + this.subtype.add(SubType.ROBOT); + this.subtype.add(SubType.ARTIFICER); + this.power = new MageInt(0); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // This creature gets +1/+0 for each artifact you control. + this.addAbility(new SimpleStaticAbility(new BoostSourceEffect( + ArtifactYouControlCount.instance, StaticValue.get(0), Duration.WhileOnBattlefield + )).addHint(ArtifactYouControlHint.instance)); + } + + private GuidelightSynergist(final GuidelightSynergist card) { + super(card); + } + + @Override + public GuidelightSynergist copy() { + return new GuidelightSynergist(this); + } +} diff --git a/Mage.Sets/src/mage/cards/g/GurmagAngler.java b/Mage.Sets/src/mage/cards/g/GurmagAngler.java index 0e650201920..95fee54fba9 100644 --- a/Mage.Sets/src/mage/cards/g/GurmagAngler.java +++ b/Mage.Sets/src/mage/cards/g/GurmagAngler.java @@ -23,7 +23,7 @@ public final class GurmagAngler extends CardImpl { this.toughness = new MageInt(5); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); } private GurmagAngler(final GurmagAngler card) { diff --git a/Mage.Sets/src/mage/cards/h/HandThatFeeds.java b/Mage.Sets/src/mage/cards/h/HandThatFeeds.java index 2026ccd0bf6..74f22ba1cab 100644 --- a/Mage.Sets/src/mage/cards/h/HandThatFeeds.java +++ b/Mage.Sets/src/mage/cards/h/HandThatFeeds.java @@ -5,9 +5,9 @@ import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.MenaceAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -37,7 +37,7 @@ public final class HandThatFeeds extends CardImpl { "card types among cards in your graveyard, it gets +2/+0 and gains menace until end of turn." ); ability.addEffect(new GainAbilitySourceEffect(new MenaceAbility(false), Duration.EndOfTurn)); - this.addAbility(ability.setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + this.addAbility(ability.setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private HandThatFeeds(final HandThatFeeds card) { diff --git a/Mage.Sets/src/mage/cards/h/HauntTheNetwork.java b/Mage.Sets/src/mage/cards/h/HauntTheNetwork.java new file mode 100644 index 00000000000..8b3f689b625 --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HauntTheNetwork.java @@ -0,0 +1,46 @@ +package mage.cards.h; + +import mage.abilities.dynamicvalue.common.ArtifactYouControlCount; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.InfoEffect; +import mage.abilities.effects.common.LoseLifeTargetEffect; +import mage.abilities.hint.common.ArtifactYouControlHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.ThopterColorlessToken; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HauntTheNetwork extends CardImpl { + + public HauntTheNetwork(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{U}{B}"); + + // Choose target opponent. Create two 1/1 colorless Thopter artifact creature tokens with flying. Then the chosen player loses X life and you gain X life, where X is the number of artifacts you control. + this.getSpellAbility().addEffect(new InfoEffect("choose target opponent")); + this.getSpellAbility().addEffect(new CreateTokenEffect(new ThopterColorlessToken(), 2)); + this.getSpellAbility().addEffect(new LoseLifeTargetEffect(ArtifactYouControlCount.instance) + .setText("Then the chosen player loses X life")); + this.getSpellAbility().addEffect(new GainLifeEffect( + ArtifactYouControlCount.instance, "and you gain X life, " + + "where X is the number of artifacts you control" + )); + this.getSpellAbility().addTarget(new TargetOpponent()); + this.getSpellAbility().addHint(ArtifactYouControlHint.instance); + } + + private HauntTheNetwork(final HauntTheNetwork card) { + super(card); + } + + @Override + public HauntTheNetwork copy() { + return new HauntTheNetwork(this); + } +} diff --git a/Mage.Sets/src/mage/cards/h/HauntwoodsShrieker.java b/Mage.Sets/src/mage/cards/h/HauntwoodsShrieker.java new file mode 100644 index 00000000000..3d5f06f34e3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HauntwoodsShrieker.java @@ -0,0 +1,102 @@ +package mage.cards.h; + +import java.util.UUID; +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.keyword.ManifestDreadEffect; +import mage.cards.Card; +import mage.cards.CardsImpl; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.FilterPermanent; +import mage.filter.predicate.card.FaceDownPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +/** + * + * @author Grath + */ +public final class HauntwoodsShrieker extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("face down creature"); + + static { + filter.add(FaceDownPredicate.instance); + } + + public HauntwoodsShrieker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}{G}"); + + this.subtype.add(SubType.BEAST); + this.subtype.add(SubType.MUTANT); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Whenever Hauntwoods Shrieker attacks, manifest dread. + this.addAbility(new AttacksTriggeredAbility(new ManifestDreadEffect())); + + // {1}{G}: Reveal target face-down permanent. If it's a creature card, you may turn it face up. + Ability ability = new SimpleActivatedAbility(new HauntwoodsShriekerEffect(), new ManaCostsImpl<>("{1}{G}")); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + } + + private HauntwoodsShrieker(final HauntwoodsShrieker card) { + super(card); + } + + @Override + public HauntwoodsShrieker copy() { + return new HauntwoodsShrieker(this); + } +} + +class HauntwoodsShriekerEffect extends OneShotEffect { + + HauntwoodsShriekerEffect() { + super(Outcome.Benefit); + this.staticText = "Reveal target face-down permanent. If it's a creature card, you may turn it face up"; + } + + private HauntwoodsShriekerEffect(final HauntwoodsShriekerEffect effect) { + super(effect); + } + + @Override + public HauntwoodsShriekerEffect copy() { + return new HauntwoodsShriekerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + MageObject mageObject = game.getObject(source); + if (player == null || mageObject == null) { + return false; + } + Permanent faceDownPermanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (faceDownPermanent != null) { + Card trueCard = (Card)faceDownPermanent.getBasicMageObject(); + player.revealCards(source, new CardsImpl(faceDownPermanent), game); + if (trueCard.isCreature() && player.chooseUse( + faceDownPermanent.getControllerId() == source.getControllerId() ? Outcome.Benefit : Outcome.Detriment, + "Turn " + trueCard.getName() + " face up?", source, game)) { + return faceDownPermanent.turnFaceUp(source, game, source.getControllerId()); + } + } else { + return false; + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/h/HeapedHarvest.java b/Mage.Sets/src/mage/cards/h/HeapedHarvest.java index ce26f3f44b6..55d52307517 100644 --- a/Mage.Sets/src/mage/cards/h/HeapedHarvest.java +++ b/Mage.Sets/src/mage/cards/h/HeapedHarvest.java @@ -26,7 +26,7 @@ public final class HeapedHarvest extends CardImpl { this.subtype.add(SubType.FOOD); // When Heaped Harvest enters and when you sacrifice it, you may search your library for a basic land card, put it onto the battlefield tapped, then shuffle. - this.addAbility(new OrTriggeredAbility(Zone.BATTLEFIELD, new SearchLibraryPutInPlayEffect( + this.addAbility(new OrTriggeredAbility(Zone.ALL, new SearchLibraryPutInPlayEffect( new TargetCardInLibrary(StaticFilters.FILTER_CARD_BASIC_LAND_A), true), true, "When {this} enters and when you sacrifice it, ", new EntersBattlefieldTriggeredAbility(null), new SacrificeSourceTriggeredAbility(null) diff --git a/Mage.Sets/src/mage/cards/h/HighcliffFelidar.java b/Mage.Sets/src/mage/cards/h/HighcliffFelidar.java index c49301426b1..1efe689abed 100644 --- a/Mage.Sets/src/mage/cards/h/HighcliffFelidar.java +++ b/Mage.Sets/src/mage/cards/h/HighcliffFelidar.java @@ -80,7 +80,7 @@ class HighcliffFelidarEffect extends OneShotEffect { return false; } Set toDestroy = new HashSet(); - game.getOpponents(source.getControllerId()) + game.getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/h/HogaakArisenNecropolis.java b/Mage.Sets/src/mage/cards/h/HogaakArisenNecropolis.java index 3f047ea4f0a..adb05ec4b52 100644 --- a/Mage.Sets/src/mage/cards/h/HogaakArisenNecropolis.java +++ b/Mage.Sets/src/mage/cards/h/HogaakArisenNecropolis.java @@ -45,7 +45,7 @@ public final class HogaakArisenNecropolis extends CardImpl { this.addAbility(new ConvokeAbility()); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // You may cast Hogaak, Arisen Necropolis from your graveyard. this.addAbility(new MayCastFromGraveyardSourceAbility()); diff --git a/Mage.Sets/src/mage/cards/h/HootingMandrills.java b/Mage.Sets/src/mage/cards/h/HootingMandrills.java index 543ab6aa2fe..9230ae993e4 100644 --- a/Mage.Sets/src/mage/cards/h/HootingMandrills.java +++ b/Mage.Sets/src/mage/cards/h/HootingMandrills.java @@ -24,7 +24,8 @@ public final class HootingMandrills extends CardImpl { this.toughness = new MageInt(4); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); + // Trample this.addAbility(TrampleAbility.getInstance()); } diff --git a/Mage.Sets/src/mage/cards/h/HoundOfTheFarbogs.java b/Mage.Sets/src/mage/cards/h/HoundOfTheFarbogs.java index ff306d916ab..cc7b247a45c 100644 --- a/Mage.Sets/src/mage/cards/h/HoundOfTheFarbogs.java +++ b/Mage.Sets/src/mage/cards/h/HoundOfTheFarbogs.java @@ -7,8 +7,8 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.MenaceAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -16,7 +16,6 @@ import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @author fireshoes @@ -37,7 +36,7 @@ public final class HoundOfTheFarbogs extends CardImpl { "{this} has menace as long as there are four or more card types among cards in your graveyard. " + "(A creature with menace can't be blocked except by two or more creatures.)")); ability.setAbilityWord(AbilityWord.DELIRIUM); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/h/HourOfVictory.java b/Mage.Sets/src/mage/cards/h/HourOfVictory.java new file mode 100644 index 00000000000..10bf400eb7f --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HourOfVictory.java @@ -0,0 +1,50 @@ +package mage.cards.h; + +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.ZombieToken; +import mage.target.common.TargetCardInLibrary; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HourOfVictory extends CardImpl { + + public HourOfVictory(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{B}"); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // When this enchantment enters, create a 2/2 black Zombie creature token. + this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new ZombieToken()))); + + // Max speed -- {1}{B}, Sacrifice this enchantment: Search your library for a card, put it into your hand, then shuffle. Activate only as a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility( + new SearchLibraryPutInHandEffect(new TargetCardInLibrary(), false), new ManaCostsImpl<>("{1}{B}") + ); + ability.addCost(new SacrificeSourceCost()); + this.addAbility(new MaxSpeedAbility(ability)); + } + + private HourOfVictory(final HourOfVictory card) { + super(card); + } + + @Override + public HourOfVictory copy() { + return new HourOfVictory(this); + } +} diff --git a/Mage.Sets/src/mage/cards/h/HowlsquadHeavy.java b/Mage.Sets/src/mage/cards/h/HowlsquadHeavy.java new file mode 100644 index 00000000000..9ad6dc1e633 --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HowlsquadHeavy.java @@ -0,0 +1,100 @@ +package mage.cards.h; + +import mage.MageInt; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.combat.AttacksIfAbleTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.abilities.mana.DynamicManaAbility; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.game.Game; +import mage.game.permanent.token.GoblinToken; +import mage.game.permanent.token.Token; +import mage.target.targetpointer.FixedTargets; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HowlsquadHeavy extends CardImpl { + + private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(new FilterControlledPermanent(SubType.GOBLIN)); + private static final Hint hint = new ValueHint("Goblins you control", xValue); + + public HowlsquadHeavy(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); + + this.subtype.add(SubType.GOBLIN); + this.subtype.add(SubType.MERCENARY); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // Other Goblins you control have haste. + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( + HasteAbility.getInstance(), Duration.WhileOnBattlefield, + StaticFilters.FILTER_PERMANENT_CREATURE_GOBLINS, true + ))); + + // At the beginning of combat on your turn, create a 1/1 red Goblin creature token. That token attacks this combat if able. + this.addAbility(new BeginningOfCombatTriggeredAbility(new HowlsquadHeavyEffect())); + + // Max Speed -- {T}: Add {R} for each Goblin you control. + this.addAbility(new MaxSpeedAbility(new DynamicManaAbility(Mana.RedMana(1), xValue)).addHint(hint)); + } + + private HowlsquadHeavy(final HowlsquadHeavy card) { + super(card); + } + + @Override + public HowlsquadHeavy copy() { + return new HowlsquadHeavy(this); + } +} + +class HowlsquadHeavyEffect extends OneShotEffect { + + HowlsquadHeavyEffect() { + super(Outcome.Benefit); + staticText = "create a 1/1 red Goblin creature token. That token attacks this combat if able"; + } + + private HowlsquadHeavyEffect(final HowlsquadHeavyEffect effect) { + super(effect); + } + + @Override + public HowlsquadHeavyEffect copy() { + return new HowlsquadHeavyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Token token = new GoblinToken(); + token.putOntoBattlefield(1, game, source); + game.addEffect(new AttacksIfAbleTargetEffect(Duration.EndOfCombat) + .setTargetPointer(new FixedTargets(token, game)), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/h/HowltoothHollow.java b/Mage.Sets/src/mage/cards/h/HowltoothHollow.java index 0b10e9cf2ce..5dd7b2425f8 100644 --- a/Mage.Sets/src/mage/cards/h/HowltoothHollow.java +++ b/Mage.Sets/src/mage/cards/h/HowltoothHollow.java @@ -34,7 +34,7 @@ public final class HowltoothHollow extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // Hideaway - this.addAbility(new HideawayAbility(4)); + this.addAbility(new HideawayAbility(this, 4)); this.addAbility(new EntersBattlefieldTappedAbility()); // {tap}: Add {B}. diff --git a/Mage.Sets/src/mage/cards/h/HuatliWarriorPoet.java b/Mage.Sets/src/mage/cards/h/HuatliWarriorPoet.java index 5b11a882b00..3fc694b3eb2 100644 --- a/Mage.Sets/src/mage/cards/h/HuatliWarriorPoet.java +++ b/Mage.Sets/src/mage/cards/h/HuatliWarriorPoet.java @@ -38,7 +38,7 @@ public final class HuatliWarriorPoet extends CardImpl { this.addAbility(new LoyaltyAbility(new GainLifeEffect( GreatestPowerAmongControlledCreaturesValue.instance, "You gain life equal to the greatest power among creatures you control" - ), 2)); + ), 2).addHint(GreatestPowerAmongControlledCreaturesValue.getHint())); // 0: Create a 3/3 green Dinosaur creature token with trample. this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new DinosaurToken()), 0)); diff --git a/Mage.Sets/src/mage/cards/i/ImpossibleInferno.java b/Mage.Sets/src/mage/cards/i/ImpossibleInferno.java index a3a1f0073d4..e3ab722f29f 100644 --- a/Mage.Sets/src/mage/cards/i/ImpossibleInferno.java +++ b/Mage.Sets/src/mage/cards/i/ImpossibleInferno.java @@ -2,9 +2,9 @@ package mage.cards.i; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.effects.common.ExileTopXMayPlayUntilEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -33,7 +33,7 @@ public final class ImpossibleInferno extends CardImpl { "four or more card types among cards in your graveyard, exile the top card of your library. " + "You may play it until the end of your next turn" )); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private ImpossibleInferno(final ImpossibleInferno card) { diff --git a/Mage.Sets/src/mage/cards/i/InexorableBlob.java b/Mage.Sets/src/mage/cards/i/InexorableBlob.java index 1c708a27b90..10feef7ff39 100644 --- a/Mage.Sets/src/mage/cards/i/InexorableBlob.java +++ b/Mage.Sets/src/mage/cards/i/InexorableBlob.java @@ -6,8 +6,8 @@ import mage.MageInt; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.CreateTokenEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -31,7 +31,7 @@ public final class InexorableBlob extends CardImpl { DeliriumCondition.instance, "Delirium — Whenever {this} attacks, if there are four or more card types among cards in your graveyard, " + "create a 3/3 green Ooze creature token that's tapped and attacking.") - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); } private InexorableBlob(final InexorableBlob card) { diff --git a/Mage.Sets/src/mage/cards/i/InquisitorsOx.java b/Mage.Sets/src/mage/cards/i/InquisitorsOx.java index c568db0a6ec..4a28b022dd2 100644 --- a/Mage.Sets/src/mage/cards/i/InquisitorsOx.java +++ b/Mage.Sets/src/mage/cards/i/InquisitorsOx.java @@ -7,16 +7,15 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.VigilanceAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @author escplan9 (Derek Monturo - dmontur1 at gmail dot com) @@ -34,7 +33,7 @@ public final class InquisitorsOx extends CardImpl { ConditionalContinuousEffect effect = new ConditionalContinuousEffect(new BoostSourceEffect(1, 0, Duration.WhileOnBattlefield), DeliriumCondition.instance, "Delirium — {this} gets +1/+0"); Ability ability = new SimpleStaticAbility(effect); ability.addEffect(new ConditionalContinuousEffect(new GainAbilitySourceEffect(VigilanceAbility.getInstance()), DeliriumCondition.instance, "and has vigilance as long as there are four or more card types among cards in your graveyard.")); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/i/IntiSeneschalOfTheSun.java b/Mage.Sets/src/mage/cards/i/IntiSeneschalOfTheSun.java index 3ebf9c7aa5a..76c2a0d35f4 100644 --- a/Mage.Sets/src/mage/cards/i/IntiSeneschalOfTheSun.java +++ b/Mage.Sets/src/mage/cards/i/IntiSeneschalOfTheSun.java @@ -1,10 +1,10 @@ package mage.cards.i; import mage.MageInt; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.AttacksWithCreaturesTriggeredAbility; import mage.abilities.common.delayed.ReflexiveTriggeredAbility; import mage.abilities.costs.common.DiscardCardCost; +import mage.abilities.effects.common.DiscardOneOrMoreCardsTriggeredAbility; import mage.abilities.effects.common.DoWhenCostPaid; import mage.abilities.effects.common.ExileTopXMayPlayUntilEffect; import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; @@ -12,10 +12,11 @@ import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.abilities.keyword.TrampleAbility; 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.counters.CounterType; -import mage.game.Game; -import mage.game.events.GameEvent; import mage.target.common.TargetAttackingCreature; import java.util.UUID; @@ -46,7 +47,10 @@ public final class IntiSeneschalOfTheSun extends CardImpl { ), 1)); // Whenever you discard one or more cards, exile the top card of your library. You may play that card until your next end step. - this.addAbility(new IntiSeneschalOfTheSunTriggeredAbility()); + this.addAbility(new DiscardOneOrMoreCardsTriggeredAbility( + new ExileTopXMayPlayUntilEffect(1, Duration.UntilYourNextEndStep) + .withTextOptions("that card", true) + )); } private IntiSeneschalOfTheSun(final IntiSeneschalOfTheSun card) { @@ -58,31 +62,3 @@ public final class IntiSeneschalOfTheSun extends CardImpl { return new IntiSeneschalOfTheSun(this); } } - -class IntiSeneschalOfTheSunTriggeredAbility extends TriggeredAbilityImpl { - - IntiSeneschalOfTheSunTriggeredAbility() { - super(Zone.BATTLEFIELD, new ExileTopXMayPlayUntilEffect(1, Duration.UntilYourNextEndStep) - .withTextOptions("that card", true)); - this.setTriggerPhrase("Whenever you discard one or more cards, "); - } - - private IntiSeneschalOfTheSunTriggeredAbility(final IntiSeneschalOfTheSunTriggeredAbility ability) { - super(ability); - } - - @Override - public IntiSeneschalOfTheSunTriggeredAbility copy() { - return new IntiSeneschalOfTheSunTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DISCARDED_CARDS; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - return isControlledBy(event.getPlayerId()); - } -} diff --git a/Mage.Sets/src/mage/cards/i/InvasiveSurgery.java b/Mage.Sets/src/mage/cards/i/InvasiveSurgery.java index 21190f5ffb2..98123488d3a 100644 --- a/Mage.Sets/src/mage/cards/i/InvasiveSurgery.java +++ b/Mage.Sets/src/mage/cards/i/InvasiveSurgery.java @@ -4,10 +4,9 @@ import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; -import mage.abilities.Mode; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -36,7 +35,7 @@ public final class InvasiveSurgery extends CardImpl { // Delirium — If there are four or more card types among cards in your graveyard, search the graveyard, hand, and library of that spell's controller for any number of cards with the same name as that spell, exile those cards, then that player shuffles their library. this.getSpellAbility().addEffect(new InvasiveSurgeryEffect()); this.getSpellAbility().addTarget(new TargetSpell(filter)); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private InvasiveSurgery(final InvasiveSurgery card) { diff --git a/Mage.Sets/src/mage/cards/i/IshkanahGrafwidow.java b/Mage.Sets/src/mage/cards/i/IshkanahGrafwidow.java index 137c3d68035..cea21a63a50 100644 --- a/Mage.Sets/src/mage/cards/i/IshkanahGrafwidow.java +++ b/Mage.Sets/src/mage/cards/i/IshkanahGrafwidow.java @@ -7,18 +7,17 @@ import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.LoseLifeTargetEffect; import mage.abilities.hint.ValueHint; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.ReachAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; -import mage.constants.Zone; import mage.filter.common.FilterControlledPermanent; import mage.game.permanent.token.SpiderToken; import mage.target.common.TargetOpponent; @@ -49,7 +48,7 @@ public final class IshkanahGrafwidow extends CardImpl { DeliriumCondition.instance, "Delirium — When {this} enters, if there are four or more card types among cards in your graveyard, " + "create three 1/2 green Spider creature tokens with reach."); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); // {5}{B}: Target opponent loses 1 life for each Spider you control. diff --git a/Mage.Sets/src/mage/cards/k/KariZevsExpertise.java b/Mage.Sets/src/mage/cards/k/KariZevsExpertise.java index a92b7571acf..9ca6330645e 100644 --- a/Mage.Sets/src/mage/cards/k/KariZevsExpertise.java +++ b/Mage.Sets/src/mage/cards/k/KariZevsExpertise.java @@ -11,10 +11,8 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.ComparisonType; import mage.constants.Duration; -import mage.constants.SubType; import mage.filter.FilterCard; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.target.TargetPermanent; @@ -25,22 +23,17 @@ import java.util.UUID; */ public final class KariZevsExpertise extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - private static final FilterCard filter2 = new FilterCard("a spell with mana value 2 or less"); + private static final FilterCard filter = new FilterCard("a spell with mana value 2 or less"); static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - filter2.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 3)); + filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 3)); } public KariZevsExpertise(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{R}{R}"); // Gain control of target creature or Vehicle until end of turn. Untap it. It gains haste until end of turn. - this.getSpellAbility().addTarget(new TargetPermanent(filter)); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); this.getSpellAbility().addEffect(new GainControlTargetEffect(Duration.EndOfTurn)); this.getSpellAbility().addEffect(new UntapTargetEffect().setText("Untap it")); this.getSpellAbility().addEffect(new GainAbilityTargetEffect( @@ -48,7 +41,7 @@ public final class KariZevsExpertise extends CardImpl { ).setText("It gains haste until end of turn")); // You may cast a card with converted mana cost 2 or less from your hand without paying its mana cost. - this.getSpellAbility().addEffect(new CastFromHandForFreeEffect(filter2).concatBy("
")); + this.getSpellAbility().addEffect(new CastFromHandForFreeEffect(filter).concatBy("
")); } private KariZevsExpertise(final KariZevsExpertise card) { diff --git a/Mage.Sets/src/mage/cards/k/KessigDireSwine.java b/Mage.Sets/src/mage/cards/k/KessigDireSwine.java index 06cc7633207..ddfd4c7da83 100644 --- a/Mage.Sets/src/mage/cards/k/KessigDireSwine.java +++ b/Mage.Sets/src/mage/cards/k/KessigDireSwine.java @@ -6,15 +6,14 @@ import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @author LevelX2 @@ -32,7 +31,7 @@ public final class KessigDireSwine extends CardImpl { this.addAbility(new SimpleStaticAbility( new ConditionalContinuousEffect(new GainAbilitySourceEffect(TrampleAbility.getInstance(), Duration.WhileOnBattlefield), DeliriumCondition.instance, "Delirium — {this} has trample as long as there are four or more card types among cards in your graveyard")) - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); } private KessigDireSwine(final KessigDireSwine card) { diff --git a/Mage.Sets/src/mage/cards/k/KickoffCelebrations.java b/Mage.Sets/src/mage/cards/k/KickoffCelebrations.java new file mode 100644 index 00000000000..6eed5fd499d --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KickoffCelebrations.java @@ -0,0 +1,64 @@ +package mage.cards.k; + +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.DiscardCardCost; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class KickoffCelebrations extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("creatures and Vehicles"); + + static { + filter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + + public KickoffCelebrations(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}"); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // When this enchantment enters, you may discard a card. If you do, draw two cards. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new DoIfCostPaid(new DrawCardSourceControllerEffect(2), new DiscardCardCost()) + )); + + // Max speed -- Sacrifice this enchantment: Creatures and Vehicles you control gain haste until end of turn. + this.addAbility(new MaxSpeedAbility(new SimpleActivatedAbility( + new GainAbilityControlledEffect( + HasteAbility.getInstance(), Duration.EndOfTurn, filter + ), new SacrificeSourceCost() + ))); + } + + private KickoffCelebrations(final KickoffCelebrations card) { + super(card); + } + + @Override + public KickoffCelebrations copy() { + return new KickoffCelebrations(this); + } +} diff --git a/Mage.Sets/src/mage/cards/k/KindlyStranger.java b/Mage.Sets/src/mage/cards/k/KindlyStranger.java index d6edfa74ece..d0cb859635d 100644 --- a/Mage.Sets/src/mage/cards/k/KindlyStranger.java +++ b/Mage.Sets/src/mage/cards/k/KindlyStranger.java @@ -4,8 +4,8 @@ import mage.MageInt; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalActivatedAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.TransformSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -34,7 +34,7 @@ public final class KindlyStranger extends CardImpl { Zone.BATTLEFIELD, new TransformSourceEffect(), new ManaCostsImpl<>("{2}{B}"), DeliriumCondition.instance, "Delirium — {2}{B}: Transform {this}. " + "Activate only if there are four or more card types among cards in your graveyard." - ).addHint(CardTypesInGraveyardHint.YOU)); + ).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private KindlyStranger(final KindlyStranger card) { diff --git a/Mage.Sets/src/mage/cards/k/KroxaTitanOfDeathsHunger.java b/Mage.Sets/src/mage/cards/k/KroxaTitanOfDeathsHunger.java index f6fe3c45f67..3cc9434f5fa 100644 --- a/Mage.Sets/src/mage/cards/k/KroxaTitanOfDeathsHunger.java +++ b/Mage.Sets/src/mage/cards/k/KroxaTitanOfDeathsHunger.java @@ -109,7 +109,7 @@ class KroxaTitanOfDeathsHungerDiscardEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Cards cards = new CardsImpl(); - game.getOpponents(source.getControllerId()) + game.getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) @@ -133,7 +133,7 @@ class KroxaTitanOfDeathsHungerDiscardEffect extends OneShotEffect { } playerSet.add(player.getId()); }); - game.getOpponents(source.getControllerId()) + game.getOpponents(source.getControllerId(), true) .stream() .filter(uuid -> !playerSet.contains(uuid)) .map(game::getPlayer) diff --git a/Mage.Sets/src/mage/cards/l/LetsPlayAGame.java b/Mage.Sets/src/mage/cards/l/LetsPlayAGame.java index 3dd5c6a9a8b..0a9acbe082d 100644 --- a/Mage.Sets/src/mage/cards/l/LetsPlayAGame.java +++ b/Mage.Sets/src/mage/cards/l/LetsPlayAGame.java @@ -2,12 +2,12 @@ package mage.cards.l; import mage.abilities.Mode; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.common.GainLifeEffect; import mage.abilities.effects.common.LoseLifeOpponentsEffect; import mage.abilities.effects.common.continuous.BoostAllEffect; import mage.abilities.effects.common.discard.DiscardEachPlayerEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -32,7 +32,7 @@ public final class LetsPlayAGame extends CardImpl { ); this.getSpellAbility().getModes().setMoreCondition(3, DeliriumCondition.instance); this.getSpellAbility().setAbilityWord(AbilityWord.DELIRIUM); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); // * Creatures your opponents control get -1/-1 until end of turn. this.getSpellAbility().addEffect(new BoostAllEffect( diff --git a/Mage.Sets/src/mage/cards/l/Lhurgoyf.java b/Mage.Sets/src/mage/cards/l/Lhurgoyf.java index 8787c128f77..e53c24dc5f1 100644 --- a/Mage.Sets/src/mage/cards/l/Lhurgoyf.java +++ b/Mage.Sets/src/mage/cards/l/Lhurgoyf.java @@ -5,6 +5,7 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.CardsInAllGraveyardsCount; import mage.abilities.effects.common.continuous.SetBasePowerToughnessPlusOneSourceEffect; +import mage.abilities.hint.ValueHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -30,7 +31,9 @@ public final class Lhurgoyf extends CardImpl { this.toughness = new MageInt(0); // Lhurgoyf's power is equal to the number of creature cards in all graveyards and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(powerValue) + ).addHint(new ValueHint("Creature cards in all graveyards", powerValue))); } private Lhurgoyf(final Lhurgoyf card) { diff --git a/Mage.Sets/src/mage/cards/l/LifecraftEngine.java b/Mage.Sets/src/mage/cards/l/LifecraftEngine.java new file mode 100644 index 00000000000..dae3206bb16 --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LifecraftEngine.java @@ -0,0 +1,101 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.ChooseCreatureTypeEffect; +import mage.abilities.effects.common.continuous.BoostAllOfChosenSubtypeEffect; +import mage.abilities.keyword.CrewAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +import java.util.List; +import java.util.UUID; + +/** + * @author jackd149 + */ +public final class LifecraftEngine extends CardImpl { + + public LifecraftEngine(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // As this Vehicle enters, choose a creature type. + this.addAbility(new AsEntersBattlefieldAbility(new ChooseCreatureTypeEffect(Outcome.BoostCreature))); + + // Vehicle creatures you control are the chosen creature type in addition to their other types. + this.addAbility(new SimpleStaticAbility(new LifecraftEngineAddSubTypeAllEffect())); + + // Each creature you control of the chosen type other than this Vehicle gets +1/+1. + BoostAllOfChosenSubtypeEffect effect = new BoostAllOfChosenSubtypeEffect(1, 1, Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT_CREATURE_CONTROLLED, true); + effect.setText("Each creature you control of the chosen type other than this Vehicle gets +1/+1."); + this.addAbility(new SimpleStaticAbility(effect)); + + // Crew 3 + this.addAbility(new CrewAbility(3)); + } + + private LifecraftEngine(final LifecraftEngine card) { + super(card); + } + + @Override + public LifecraftEngine copy() { + return new LifecraftEngine(this); + } +} + +class LifecraftEngineAddSubTypeAllEffect extends ContinuousEffectImpl { + + static FilterControlledCreaturePermanent filter = new FilterControlledCreaturePermanent(); + + static { + filter.add(SubType.VEHICLE.getPredicate()); + } + + public LifecraftEngineAddSubTypeAllEffect() { + super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Benefit); + + staticText = "Vehicle creatures you control are the chosen type in addition to their other types."; + } + + private LifecraftEngineAddSubTypeAllEffect(final LifecraftEngineAddSubTypeAllEffect effect) { + super(effect); + } + + @Override + public LifecraftEngineAddSubTypeAllEffect copy() { + return new LifecraftEngineAddSubTypeAllEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + UUID controllerId = source.getControllerId(); + Player controller = game.getPlayer(controllerId); + SubType subType = ChooseCreatureTypeEffect.getChosenCreatureType(source.getSourceId(), game); + if (controller == null || subType == null) { + return false; + } + + List creatures = game.getBattlefield().getAllActivePermanents( + filter, controllerId, game); + for (Permanent creature : creatures) { + if (creature != null) { + creature.addSubType(game, subType); + } + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/l/LightTheWay.java b/Mage.Sets/src/mage/cards/l/LightTheWay.java index 1e1e9174df2..d6ff92ae901 100644 --- a/Mage.Sets/src/mage/cards/l/LightTheWay.java +++ b/Mage.Sets/src/mage/cards/l/LightTheWay.java @@ -1,7 +1,5 @@ package mage.cards.l; -import java.util.UUID; - import mage.abilities.Mode; import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.abilities.effects.common.UntapTargetEffect; @@ -9,25 +7,18 @@ import mage.abilities.effects.common.counter.AddCountersTargetEffect; 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.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import mage.target.common.TargetControlledPermanent; +import java.util.UUID; + /** - * * @author weirddan455 */ public final class LightTheWay extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or(CardType.CREATURE.getPredicate(), SubType.VEHICLE.getPredicate())); - } - public LightTheWay(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{W}"); @@ -35,7 +26,7 @@ public final class LightTheWay extends CardImpl { // • Put a +1/+1 counter on target creature or Vehicle. Untap it. this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance())); this.getSpellAbility().addEffect(new UntapTargetEffect().setText("Untap it")); - this.getSpellAbility().addTarget(new TargetPermanent(filter)); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); // • Return target permanent you control to its owner's hand. Mode mode = new Mode(new ReturnToHandTargetEffect()); diff --git a/Mage.Sets/src/mage/cards/l/LightwheelEnhancements.java b/Mage.Sets/src/mage/cards/l/LightwheelEnhancements.java new file mode 100644 index 00000000000..ea06d10fd44 --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LightwheelEnhancements.java @@ -0,0 +1,63 @@ +package mage.cards.l; + +import mage.abilities.Ability; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.common.MayCastFromGraveyardSourceAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.continuous.BoostEnchantedEffect; +import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AttachmentType; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LightwheelEnhancements extends CardImpl { + + public LightwheelEnhancements(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{W}"); + + this.subtype.add(SubType.AURA); + + // Enchant creature or Vehicle + TargetPermanent auraTarget = new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + this.addAbility(new EnchantAbility(auraTarget)); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // Enchanted permanent gets +1/+1 and has vigilance. + Ability ability = new SimpleStaticAbility(new BoostEnchantedEffect(1, 1) + .setText("enchanted permanent gets +1/+1")); + ability.addEffect(new GainAbilityAttachedEffect( + VigilanceAbility.getInstance(), AttachmentType.AURA + ).setText("and has vigilance")); + this.addAbility(ability); + + // Max speed -- You may cast this card from your graveyard. + this.addAbility(new MaxSpeedAbility(new MayCastFromGraveyardSourceAbility())); + } + + private LightwheelEnhancements(final LightwheelEnhancements card) { + super(card); + } + + @Override + public LightwheelEnhancements copy() { + return new LightwheelEnhancements(this); + } +} diff --git a/Mage.Sets/src/mage/cards/l/LogicKnot.java b/Mage.Sets/src/mage/cards/l/LogicKnot.java index 5b2902a354d..85f11189b0d 100644 --- a/Mage.Sets/src/mage/cards/l/LogicKnot.java +++ b/Mage.Sets/src/mage/cards/l/LogicKnot.java @@ -21,7 +21,7 @@ public final class LogicKnot extends CardImpl { // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // Counter target spell unless its controller pays {X}. this.getSpellAbility().addEffect(new CounterUnlessPaysEffect(GetXValue.instance)); diff --git a/Mage.Sets/src/mage/cards/l/LotusguardDisciple.java b/Mage.Sets/src/mage/cards/l/LotusguardDisciple.java new file mode 100644 index 00000000000..e22bd55b00b --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LotusguardDisciple.java @@ -0,0 +1,52 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.IndestructibleAbility; +import mage.abilities.keyword.LifelinkAbility; +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 LotusguardDisciple extends CardImpl { + + public LotusguardDisciple(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.BIRD); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When this creature enters, target creature or Vehicle gains lifelink and indestructible until end of turn. + Ability ability = new EntersBattlefieldTriggeredAbility(new GainAbilityTargetEffect(LifelinkAbility.getInstance()) + .setText("target creature or Vehicle gains lifelink")); + ability.addEffect(new GainAbilityTargetEffect(IndestructibleAbility.getInstance()) + .setText("and indestructible until end of turn")); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); + this.addAbility(ability); + } + + private LotusguardDisciple(final LotusguardDisciple card) { + super(card); + } + + @Override + public LotusguardDisciple copy() { + return new LotusguardDisciple(this); + } +} diff --git a/Mage.Sets/src/mage/cards/l/LucidDreams.java b/Mage.Sets/src/mage/cards/l/LucidDreams.java index b4385afb815..8fee8532cee 100644 --- a/Mage.Sets/src/mage/cards/l/LucidDreams.java +++ b/Mage.Sets/src/mage/cards/l/LucidDreams.java @@ -2,7 +2,6 @@ package mage.cards.l; import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.DrawCardSourceControllerEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -20,7 +19,7 @@ public final class LucidDreams extends CardImpl { // Draw X cards, where X is the number of card types among cards in your graveyard. this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(CardTypesInGraveyardCount.YOU) .setText("draw X cards, where X is the number of card types among cards in your graveyard")); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private LucidDreams(final LucidDreams card) { diff --git a/Mage.Sets/src/mage/cards/l/LukkaBoundToRuin.java b/Mage.Sets/src/mage/cards/l/LukkaBoundToRuin.java index 09d0f88c46c..267548e8f33 100644 --- a/Mage.Sets/src/mage/cards/l/LukkaBoundToRuin.java +++ b/Mage.Sets/src/mage/cards/l/LukkaBoundToRuin.java @@ -56,6 +56,7 @@ public class LukkaBoundToRuin extends CardImpl { "where X is the greatest power among creatures you control as you activate this ability."); ability = new LoyaltyAbility(damageMultiEffect, -4); ability.setTargetAdjuster(LukkaBoundToRuinAdjuster.instance); + ability.addHint(GreatestPowerAmongControlledCreaturesValue.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/l/LukkaCoppercoatOutcast.java b/Mage.Sets/src/mage/cards/l/LukkaCoppercoatOutcast.java index 6c05efdee47..230b637b8e7 100644 --- a/Mage.Sets/src/mage/cards/l/LukkaCoppercoatOutcast.java +++ b/Mage.Sets/src/mage/cards/l/LukkaCoppercoatOutcast.java @@ -188,7 +188,7 @@ class LukkaCoppercoatOutcastDamageEffect extends OneShotEffect { StaticFilters.FILTER_PERMANENT_CREATURE, source.getControllerId(), game ); List opponentList = game - .getOpponents(source.getControllerId()) + .getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/l/LumberingWorldwagon.java b/Mage.Sets/src/mage/cards/l/LumberingWorldwagon.java new file mode 100644 index 00000000000..2dab14c8726 --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LumberingWorldwagon.java @@ -0,0 +1,52 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.LandsYouControlCount; +import mage.abilities.effects.common.continuous.SetBasePowerSourceEffect; +import mage.abilities.effects.common.search.SearchLibraryPutInPlayEffect; +import mage.abilities.keyword.CrewAbility; +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.target.common.TargetCardInLibrary; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LumberingWorldwagon extends CardImpl { + + public LumberingWorldwagon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{G}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(0); + this.toughness = new MageInt(4); + + // This Vehicle's power is equal to the number of lands you control. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerSourceEffect(LandsYouControlCount.instance))); + + // Whenever this Vehicle enters or attacks, you may search your library for a basic land card, put it onto the battlefield tapped, then shuffle. + this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new SearchLibraryPutInPlayEffect( + new TargetCardInLibrary(StaticFilters.FILTER_CARD_BASIC_LAND), true + ), true)); + + // Crew 4 + this.addAbility(new CrewAbility(4)); + } + + private LumberingWorldwagon(final LumberingWorldwagon card) { + super(card); + } + + @Override + public LumberingWorldwagon copy() { + return new LumberingWorldwagon(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MagmakinArtillerist.java b/Mage.Sets/src/mage/cards/m/MagmakinArtillerist.java new file mode 100644 index 00000000000..ef66c65152f --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MagmakinArtillerist.java @@ -0,0 +1,53 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.common.CycleTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.SavedDiscardValue; +import mage.abilities.effects.common.DamagePlayersEffect; +import mage.abilities.effects.common.DiscardOneOrMoreCardsTriggeredAbility; +import mage.abilities.keyword.CyclingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TargetController; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MagmakinArtillerist extends CardImpl { + + public MagmakinArtillerist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); + + this.subtype.add(SubType.ELEMENTAL); + this.subtype.add(SubType.PIRATE); + this.power = new MageInt(1); + this.toughness = new MageInt(4); + + // Whenever you discard one or more cards, this creature deals that much damage to each opponent. + this.addAbility(new DiscardOneOrMoreCardsTriggeredAbility( + new DamagePlayersEffect(SavedDiscardValue.MUCH, TargetController.OPPONENT) + )); + + // Cycling {1}{R} + this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{1}{R}"))); + + // When you cycle this card, it deals 1 damage to each opponent. + this.addAbility(new CycleTriggeredAbility(new DamagePlayersEffect( + 1, TargetController.OPPONENT, "it" + ))); + } + + private MagmakinArtillerist(final MagmakinArtillerist card) { + super(card); + } + + @Override + public MagmakinArtillerist copy() { + return new MagmakinArtillerist(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MagmaticSinkhole.java b/Mage.Sets/src/mage/cards/m/MagmaticSinkhole.java index b918981ffa2..328aaf84eee 100644 --- a/Mage.Sets/src/mage/cards/m/MagmaticSinkhole.java +++ b/Mage.Sets/src/mage/cards/m/MagmaticSinkhole.java @@ -18,7 +18,7 @@ public final class MagmaticSinkhole extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{5}{R}"); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // Magmatic Sinkhole deals 5 damage to target creature or planeswalker. this.getSpellAbility().addEffect(new DamageTargetEffect(5)); diff --git a/Mage.Sets/src/mage/cards/m/ManicScribe.java b/Mage.Sets/src/mage/cards/m/ManicScribe.java index cfeb585f4b4..78767f93007 100644 --- a/Mage.Sets/src/mage/cards/m/ManicScribe.java +++ b/Mage.Sets/src/mage/cards/m/ManicScribe.java @@ -1,13 +1,13 @@ package mage.cards.m; import mage.MageInt; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.MillCardsEachPlayerEffect; import mage.abilities.effects.common.MillCardsTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -42,7 +42,7 @@ public final class ManicScribe extends CardImpl { false ), DeliriumCondition.instance, "Delirium — At the beginning of each opponent's upkeep, " + "if there are four or more card types among cards in your graveyard, that player mills three cards." - ).addHint(CardTypesInGraveyardHint.YOU)); + ).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private ManicScribe(final ManicScribe card) { diff --git a/Mage.Sets/src/mage/cards/m/MaraudingMako.java b/Mage.Sets/src/mage/cards/m/MaraudingMako.java new file mode 100644 index 00000000000..90a701d9792 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MaraudingMako.java @@ -0,0 +1,47 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.SavedDiscardValue; +import mage.abilities.effects.common.DiscardOneOrMoreCardsTriggeredAbility; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.CyclingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MaraudingMako extends CardImpl { + + public MaraudingMako(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}"); + + this.subtype.add(SubType.SHARK); + this.subtype.add(SubType.PIRATE); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Whenever you discard one or more cards, put that many +1/+1 counters on this creature. + this.addAbility(new DiscardOneOrMoreCardsTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance(0), SavedDiscardValue.MANY) + )); + + // Cycling {2} + this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{2}"))); + } + + private MaraudingMako(final MaraudingMako card) { + super(card); + } + + @Override + public MaraudingMako copy() { + return new MaraudingMako(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MezzioMugger.java b/Mage.Sets/src/mage/cards/m/MezzioMugger.java index aa7e207838c..b1fb0955dac 100644 --- a/Mage.Sets/src/mage/cards/m/MezzioMugger.java +++ b/Mage.Sets/src/mage/cards/m/MezzioMugger.java @@ -72,7 +72,7 @@ class MezzioMuggerEffect extends OneShotEffect { return false; } Set cards = game - .getOpponents(source.getControllerId()) + .getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/m/MightBeyondReason.java b/Mage.Sets/src/mage/cards/m/MightBeyondReason.java index 1bddab66c32..ce0b0d439d6 100644 --- a/Mage.Sets/src/mage/cards/m/MightBeyondReason.java +++ b/Mage.Sets/src/mage/cards/m/MightBeyondReason.java @@ -4,8 +4,8 @@ import java.util.UUID; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.counter.AddCountersTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -30,7 +30,7 @@ public final class MightBeyondReason extends CardImpl { + "Delirium — Put three +1/+1 counters on that creature instead if there are four or more card types among cards in your graveyard" )); getSpellAbility().addTarget(new TargetCreaturePermanent()); - getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private MightBeyondReason(final MightBeyondReason card) { diff --git a/Mage.Sets/src/mage/cards/m/MindwrackDemon.java b/Mage.Sets/src/mage/cards/m/MindwrackDemon.java index 5099b17839d..c318d5a3d7f 100644 --- a/Mage.Sets/src/mage/cards/m/MindwrackDemon.java +++ b/Mage.Sets/src/mage/cards/m/MindwrackDemon.java @@ -4,6 +4,7 @@ import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.InvertCondition; @@ -11,7 +12,6 @@ import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.LoseLifeSourceControllerEffect; import mage.abilities.effects.common.MillCardsControllerEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; @@ -44,7 +44,7 @@ public final class MindwrackDemon extends CardImpl { new BeginningOfUpkeepTriggeredAbility(new LoseLifeSourceControllerEffect(4)), new InvertCondition(DeliriumCondition.instance), "Delirium — At the beginning of your upkeep, you lose 4 life unless there are four or more card types among cards in your graveyard."); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/m/MistsOfLittjara.java b/Mage.Sets/src/mage/cards/m/MistsOfLittjara.java index 2a1c70c564c..9888d7904a5 100644 --- a/Mage.Sets/src/mage/cards/m/MistsOfLittjara.java +++ b/Mage.Sets/src/mage/cards/m/MistsOfLittjara.java @@ -1,6 +1,5 @@ package mage.cards.m; -import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.continuous.BoostEnchantedEffect; @@ -11,8 +10,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import java.util.UUID; @@ -22,15 +20,6 @@ import java.util.UUID; */ public final class MistsOfLittjara extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public MistsOfLittjara(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}"); @@ -40,11 +29,10 @@ public final class MistsOfLittjara extends CardImpl { this.addAbility(FlashAbility.getInstance()); // Enchant creature or Vehicle - TargetPermanent auraTarget = new TargetPermanent(filter); + TargetPermanent auraTarget = new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE); this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); - Ability ability = new EnchantAbility(auraTarget); - this.addAbility(ability); + this.addAbility(new EnchantAbility(auraTarget)); // Enchanted creature gets -3/-0 this.addAbility(new SimpleStaticAbility(new BoostEnchantedEffect(-3, 0))); diff --git a/Mage.Sets/src/mage/cards/m/MnemonicBetrayal.java b/Mage.Sets/src/mage/cards/m/MnemonicBetrayal.java index fee2d8def28..b8cb7485474 100644 --- a/Mage.Sets/src/mage/cards/m/MnemonicBetrayal.java +++ b/Mage.Sets/src/mage/cards/m/MnemonicBetrayal.java @@ -76,7 +76,7 @@ class MnemonicBetrayalExileEffect extends OneShotEffect { return false; } Cards cards = new CardsImpl(); - game.getOpponents(source.getControllerId()) + game.getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/m/MoldgrafScavenger.java b/Mage.Sets/src/mage/cards/m/MoldgrafScavenger.java index b6842794785..667531c4ed7 100644 --- a/Mage.Sets/src/mage/cards/m/MoldgrafScavenger.java +++ b/Mage.Sets/src/mage/cards/m/MoldgrafScavenger.java @@ -6,14 +6,13 @@ import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @author LevelX2 @@ -31,7 +30,7 @@ public final class MoldgrafScavenger extends CardImpl { new BoostSourceEffect(3, 0, Duration.WhileOnBattlefield), DeliriumCondition.instance, "Delirium — {this} gets +3/+0 as long as there are four or more card types among cards in your graveyard")) - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); } private MoldgrafScavenger(final MoldgrafScavenger card) { diff --git a/Mage.Sets/src/mage/cards/m/MonstrousOnslaught.java b/Mage.Sets/src/mage/cards/m/MonstrousOnslaught.java index 2a5b2b02ee8..4669d980755 100644 --- a/Mage.Sets/src/mage/cards/m/MonstrousOnslaught.java +++ b/Mage.Sets/src/mage/cards/m/MonstrousOnslaught.java @@ -26,6 +26,7 @@ public final class MonstrousOnslaught extends CardImpl { effect.setText("{this} deals X damage divided as you choose among any number of target creatures, where X is the greatest power among creatures you control as you cast this spell"); this.getSpellAbility().addEffect(effect); this.getSpellAbility().addTarget(new TargetCreaturePermanentAmount(xValue)); + this.getSpellAbility().addHint(GreatestPowerAmongControlledCreaturesValue.getHint()); } private MonstrousOnslaught(final MonstrousOnslaught card) { diff --git a/Mage.Sets/src/mage/cards/m/MoorlandDrifter.java b/Mage.Sets/src/mage/cards/m/MoorlandDrifter.java index 2ac12774623..6210d3e77cb 100644 --- a/Mage.Sets/src/mage/cards/m/MoorlandDrifter.java +++ b/Mage.Sets/src/mage/cards/m/MoorlandDrifter.java @@ -6,14 +6,13 @@ import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; /** * @author escplan9 (Derek Monturo - dmontur1 at gmail dot com) @@ -28,7 +27,7 @@ public final class MoorlandDrifter extends CardImpl { // Delirium — Moorland Drifter has flying as long as there are four or more card types among cards in your graveyard. ConditionalContinuousEffect effect = new ConditionalContinuousEffect(new GainAbilitySourceEffect(FlyingAbility.getInstance()), DeliriumCondition.instance, "Delirium — Moorland Drifter has flying as long as there are four or more card types among cards in your graveyard."); - this.addAbility(new SimpleStaticAbility(effect).addHint(CardTypesInGraveyardHint.YOU)); + this.addAbility(new SimpleStaticAbility(effect).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private MoorlandDrifter(final MoorlandDrifter card) { diff --git a/Mage.Sets/src/mage/cards/m/MosswortBridge.java b/Mage.Sets/src/mage/cards/m/MosswortBridge.java index 1a24640b7f1..2df3317023a 100644 --- a/Mage.Sets/src/mage/cards/m/MosswortBridge.java +++ b/Mage.Sets/src/mage/cards/m/MosswortBridge.java @@ -28,7 +28,7 @@ public final class MosswortBridge extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // Hideaway (This land enters the battlefield tapped. When it does, look at the top four cards of your library, exile one face down, then put the rest on the bottom of your library.) - this.addAbility(new HideawayAbility(4)); + this.addAbility(new HideawayAbility(this, 4)); this.addAbility(new EntersBattlefieldTappedAbility()); // {T}: Add {G}. diff --git a/Mage.Sets/src/mage/cards/m/Mournwillow.java b/Mage.Sets/src/mage/cards/m/Mournwillow.java index 3193d0264ab..993ede54731 100644 --- a/Mage.Sets/src/mage/cards/m/Mournwillow.java +++ b/Mage.Sets/src/mage/cards/m/Mournwillow.java @@ -5,8 +5,8 @@ import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.RestrictionEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.HasteAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -40,7 +40,7 @@ public final class Mournwillow extends CardImpl { DeliriumCondition.instance, "Delirium — When {this} enters, if there are four or more card types among cards in your graveyard, " + "creatures with power 2 or less can't block this turn."); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/m/MuYanlingWindRider.java b/Mage.Sets/src/mage/cards/m/MuYanlingWindRider.java new file mode 100644 index 00000000000..bd9badc3070 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MuYanlingWindRider.java @@ -0,0 +1,70 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.OneOrMoreCombatDamagePlayerTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.FlyingAbility; +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.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.game.permanent.token.VehicleToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MuYanlingWindRider extends CardImpl { + + private static final FilterPermanent filter + = new FilterPermanent(SubType.VEHICLE, "Vehicles"); + private static final FilterCreaturePermanent filter2 + = new FilterCreaturePermanent("creatures you control with flying"); + + static { + filter.add(new AbilityPredicate(FlyingAbility.class)); + } + + public MuYanlingWindRider(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WIZARD); + this.subtype.add(SubType.PILOT); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // When this creature enters, create a 3/2 colorless Vehicle artifact token with crew 1. + this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new VehicleToken()))); + + // Vehicles you control have flying. + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( + FlyingAbility.getInstance(), Duration.WhileOnBattlefield, filter + ))); + + // Whenever one or more creatures you control with flying deal combat damage to a player, draw a card. + this.addAbility(new OneOrMoreCombatDamagePlayerTriggeredAbility( + new DrawCardSourceControllerEffect(1), filter2 + )); + } + + private MuYanlingWindRider(final MuYanlingWindRider card) { + super(card); + } + + @Override + public MuYanlingWindRider copy() { + return new MuYanlingWindRider(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MurderousCut.java b/Mage.Sets/src/mage/cards/m/MurderousCut.java index b752f040150..7cf22972755 100644 --- a/Mage.Sets/src/mage/cards/m/MurderousCut.java +++ b/Mage.Sets/src/mage/cards/m/MurderousCut.java @@ -20,7 +20,7 @@ public final class MurderousCut extends CardImpl { // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // Destroy target creature. this.getSpellAbility().addEffect(new DestroyTargetEffect()); diff --git a/Mage.Sets/src/mage/cards/m/MurktideRegent.java b/Mage.Sets/src/mage/cards/m/MurktideRegent.java index f073d69e118..3ecdf272a13 100644 --- a/Mage.Sets/src/mage/cards/m/MurktideRegent.java +++ b/Mage.Sets/src/mage/cards/m/MurktideRegent.java @@ -37,7 +37,7 @@ public final class MurktideRegent extends CardImpl { this.toughness = new MageInt(3); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(true)); // Flying this.addAbility(FlyingAbility.getInstance()); diff --git a/Mage.Sets/src/mage/cards/n/NecropolisFiend.java b/Mage.Sets/src/mage/cards/n/NecropolisFiend.java index 343aa8810e5..2f491e5ac22 100644 --- a/Mage.Sets/src/mage/cards/n/NecropolisFiend.java +++ b/Mage.Sets/src/mage/cards/n/NecropolisFiend.java @@ -48,7 +48,7 @@ public final class NecropolisFiend extends CardImpl { this.toughness = new MageInt(5); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // Flying this.addAbility(FlyingAbility.getInstance()); diff --git a/Mage.Sets/src/mage/cards/n/Nethergoyf.java b/Mage.Sets/src/mage/cards/n/Nethergoyf.java index 5b71e4679bb..bc8d5c71602 100644 --- a/Mage.Sets/src/mage/cards/n/Nethergoyf.java +++ b/Mage.Sets/src/mage/cards/n/Nethergoyf.java @@ -34,8 +34,6 @@ import java.util.stream.Collectors; */ public final class Nethergoyf extends CardImpl { - private static final DynamicValue powerValue = CardTypesInGraveyardCount.YOU; - public Nethergoyf(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{B}"); @@ -44,7 +42,9 @@ public final class Nethergoyf extends CardImpl { this.toughness = new MageInt(1); // Nethergoyf's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.YOU) + ).addHint(CardTypesInGraveyardCount.YOU.getHint())); // Escape--{2}{B}, Exile any number of other cards from your graveyard with four or more card types among them. CostsImpl additionalCost = new CostsImpl(); diff --git a/Mage.Sets/src/mage/cards/n/NighthawkScavenger.java b/Mage.Sets/src/mage/cards/n/NighthawkScavenger.java index 34641934972..956d15bb4c7 100644 --- a/Mage.Sets/src/mage/cards/n/NighthawkScavenger.java +++ b/Mage.Sets/src/mage/cards/n/NighthawkScavenger.java @@ -7,7 +7,6 @@ import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.common.continuous.SetBasePowerSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.DeathtouchAbility; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.LifelinkAbility; @@ -51,7 +50,7 @@ public final class NighthawkScavenger extends CardImpl { .setText("{this}'s power is equal to 1 plus the number of " + "card types among cards in your opponents' graveyards. " + "(Cards in graveyards have only the characteristics of their front face.)") - ).addHint(CardTypesInGraveyardHint.OPPONENTS)); + ).addHint(CardTypesInGraveyardCount.OPPONENTS.getHint())); } private NighthawkScavenger(final NighthawkScavenger card) { diff --git a/Mage.Sets/src/mage/cards/n/NullpriestOfOblivion.java b/Mage.Sets/src/mage/cards/n/NullpriestOfOblivion.java index 80c4f12020b..498c3ca4f47 100644 --- a/Mage.Sets/src/mage/cards/n/NullpriestOfOblivion.java +++ b/Mage.Sets/src/mage/cards/n/NullpriestOfOblivion.java @@ -40,10 +40,10 @@ public final class NullpriestOfOblivion extends CardImpl { // Lifelink this.addAbility(LifelinkAbility.getInstance()); - // When Nullpriest of Oblivion enters the battlefield, if it was kicked, return target creature card from your graveyard to the battlefield. + // When this creature enters the battlefield, if it was kicked, return target creature card from your graveyard to the battlefield. Ability ability = new ConditionalInterveningIfTriggeredAbility( new EntersBattlefieldTriggeredAbility(new ReturnFromGraveyardToBattlefieldTargetEffect()), - KickedCondition.ONCE, "When {this} enters, if it was kicked, " + + KickedCondition.ONCE, "When this creature enters, if it was kicked, " + "return target creature card from your graveyard to the battlefield." ); ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); diff --git a/Mage.Sets/src/mage/cards/o/ObsessiveSkinner.java b/Mage.Sets/src/mage/cards/o/ObsessiveSkinner.java index 1538f17d57d..5d61c63a5d8 100644 --- a/Mage.Sets/src/mage/cards/o/ObsessiveSkinner.java +++ b/Mage.Sets/src/mage/cards/o/ObsessiveSkinner.java @@ -4,12 +4,12 @@ import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.counter.AddCountersTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -43,7 +43,7 @@ public final class ObsessiveSkinner extends CardImpl { "Delirium — At the beginning of each opponent's upkeep, if there are four or more card types among cards in your graveyard, " + "put a +1/+1 counter on target creature."); ability.addTarget(new TargetCreaturePermanent()); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/o/OmenHawker.java b/Mage.Sets/src/mage/cards/o/OmenHawker.java index 41f0a7f71e2..28a67198073 100644 --- a/Mage.Sets/src/mage/cards/o/OmenHawker.java +++ b/Mage.Sets/src/mage/cards/o/OmenHawker.java @@ -1,19 +1,13 @@ package mage.cards.o; -import mage.ConditionalMana; import mage.MageInt; import mage.Mana; -import mage.abilities.Ability; -import mage.abilities.condition.Condition; -import mage.abilities.costs.Cost; import mage.abilities.mana.ConditionalColoredManaAbility; -import mage.abilities.mana.builder.ConditionalManaBuilder; -import mage.abilities.mana.conditional.ManaCondition; +import mage.abilities.mana.builder.common.ActivatedAbilityManaBuilder; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.game.Game; import java.util.UUID; @@ -32,7 +26,7 @@ public final class OmenHawker extends CardImpl { // {T}: Add {C}{U}. Spend this many only to activate abilities. this.addAbility(new ConditionalColoredManaAbility( - new Mana(0, 1, 0, 0, 0, 0, 0, 1), new OmenHawkerManaBuilder() + new Mana(0, 1, 0, 0, 0, 0, 0, 1), new ActivatedAbilityManaBuilder() )); } @@ -45,41 +39,3 @@ public final class OmenHawker extends CardImpl { return new OmenHawker(this); } } - -class OmenHawkerManaBuilder extends ConditionalManaBuilder { - - @Override - public ConditionalMana build(Object... options) { - return new OmenHawkerConditionalMana(this.mana); - } - - @Override - public String getRule() { - return "Spend this mana only to activate abilities"; - } -} - -class OmenHawkerConditionalMana extends ConditionalMana { - - OmenHawkerConditionalMana(Mana mana) { - super(mana); - staticText = "Spend this mana only to activate abilities"; - addCondition(new OmenHawkerManaCondition()); - } -} - -class OmenHawkerManaCondition extends ManaCondition implements Condition { - - @Override - public boolean apply(Game game, Ability source) { - if (source != null && !source.isActivated()) { - return source.isActivatedAbility(); - } - return false; - } - - @Override - public boolean apply(Game game, Ability source, UUID originalId, Cost costsToPay) { - return apply(game, source); - } -} diff --git a/Mage.Sets/src/mage/cards/o/OmnivorousFlytrap.java b/Mage.Sets/src/mage/cards/o/OmnivorousFlytrap.java index 88422143946..f6708c4923d 100644 --- a/Mage.Sets/src/mage/cards/o/OmnivorousFlytrap.java +++ b/Mage.Sets/src/mage/cards/o/OmnivorousFlytrap.java @@ -9,7 +9,6 @@ import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.counter.DistributeCountersEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -42,7 +41,7 @@ public final class OmnivorousFlytrap extends CardImpl { new OmnivorousFlytrapCondition()) .concatBy("Then")); ability.addTarget(new TargetCreaturePermanentAmount(2)); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability.setAbilityWord(AbilityWord.DELIRIUM)); } diff --git a/Mage.Sets/src/mage/cards/o/OozePatrol.java b/Mage.Sets/src/mage/cards/o/OozePatrol.java new file mode 100644 index 00000000000..5f100d36f00 --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OozePatrol.java @@ -0,0 +1,51 @@ +package mage.cards.o; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +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.counters.CounterType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class OozePatrol extends CardImpl { + + private static final DynamicValue xValue = new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_ARTIFACT_OR_CREATURE); + private static final Hint hint = new ValueHint("Artifacts and creatures in your graveyard", xValue); + + public OozePatrol(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}"); + + this.subtype.add(SubType.OOZE); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // When this creature enters, mill two cards, then put a +1/+1 counter on this creature for each artifact and/or creature card in your graveyard. + Ability ability = new EntersBattlefieldTriggeredAbility(new MillCardsControllerEffect(2)); + ability.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(0), xValue) + .setText(", then put a +1/+1 counter on this creature for each artifact and/or creature card in your graveyard")); + this.addAbility(ability.addHint(hint)); + } + + private OozePatrol(final OozePatrol card) { + super(card); + } + + @Override + public OozePatrol copy() { + return new OozePatrol(this); + } +} diff --git a/Mage.Sets/src/mage/cards/o/OrcishSiegemaster.java b/Mage.Sets/src/mage/cards/o/OrcishSiegemaster.java index 1f4b28f6dc0..536782d61f0 100644 --- a/Mage.Sets/src/mage/cards/o/OrcishSiegemaster.java +++ b/Mage.Sets/src/mage/cards/o/OrcishSiegemaster.java @@ -63,7 +63,7 @@ public final class OrcishSiegemaster extends CardImpl { GreatestPowerAmongControlledCreaturesValue.instance, StaticValue.get(0), Duration.EndOfTurn, "it" ) - )); + ).addHint(GreatestPowerAmongControlledCreaturesValue.getHint())); } private OrcishSiegemaster(final OrcishSiegemaster card) { diff --git a/Mage.Sets/src/mage/cards/o/OutpaceOblivion.java b/Mage.Sets/src/mage/cards/o/OutpaceOblivion.java new file mode 100644 index 00000000000..731fd6fb67d --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OutpaceOblivion.java @@ -0,0 +1,79 @@ +package mage.cards.o; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCreatureOrPlaneswalker; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class OutpaceOblivion extends CardImpl { + + public OutpaceOblivion(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{R}"); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // When this enchantment enters, it deals 5 damage to up to one target creature or planeswalker. + Ability ability = new EntersBattlefieldTriggeredAbility(new DamageTargetEffect(5, "it")); + ability.addTarget(new TargetCreatureOrPlaneswalker(0, 1)); + this.addAbility(ability); + + // {2}, Sacrifice this enchantment: It deals 2 damage to each player who doesn't have max speed. + ability = new SimpleActivatedAbility(new OutpaceOblivionEffect(), new GenericManaCost(2)); + ability.addCost(new SacrificeSourceCost()); + this.addAbility(ability); + } + + private OutpaceOblivion(final OutpaceOblivion card) { + super(card); + } + + @Override + public OutpaceOblivion copy() { + return new OutpaceOblivion(this); + } +} + +class OutpaceOblivionEffect extends OneShotEffect { + + OutpaceOblivionEffect() { + super(Outcome.Benefit); + staticText = "it deals 2 damage to each player who doesn't have max speed"; + } + + private OutpaceOblivionEffect(final OutpaceOblivionEffect effect) { + super(effect); + } + + @Override + public OutpaceOblivionEffect copy() { + return new OutpaceOblivionEffect(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 && player.getSpeed() < 4) { + player.damage(2, source, game); + } + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/o/OviyaAutomechArtisan.java b/Mage.Sets/src/mage/cards/o/OviyaAutomechArtisan.java new file mode 100644 index 00000000000..14865f03ca6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OviyaAutomechArtisan.java @@ -0,0 +1,129 @@ +package mage.cards.o; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainAbilityAllEffect; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInHand; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class OviyaAutomechArtisan extends CardImpl { + + private static final FilterPermanent filter + = new FilterCreaturePermanent("each creature that's attacking one of your opponents"); + + static { + filter.add(OviyaAutomechArtisanPredicate.instance); + } + + public OviyaAutomechArtisan(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ARTIFICER); + this.power = new MageInt(1); + this.toughness = new MageInt(2); + + // Each creature that's attacking one of your opponents has trample. + this.addAbility(new SimpleStaticAbility(new GainAbilityAllEffect( + TrampleAbility.getInstance(), Duration.WhileOnBattlefield, filter + ))); + + // {G}, {T}: You may put a creature or Vehicle card from your hand onto the battlefield. If you put an artifact onto the battlefield this way, put two +1/+1 counters on it. + Ability ability = new SimpleActivatedAbility(new OviyaAutomechArtisanEffect(), new ManaCostsImpl<>("{G}")); + ability.addCost(new TapSourceCost()); + this.addAbility(ability); + } + + private OviyaAutomechArtisan(final OviyaAutomechArtisan card) { + super(card); + } + + @Override + public OviyaAutomechArtisan copy() { + return new OviyaAutomechArtisan(this); + } +} + +enum OviyaAutomechArtisanPredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + return game + .getOpponents(input.getPlayerId()) + .contains(game.getCombat().getDefenderId(input.getObject().getId())); + } +} + +class OviyaAutomechArtisanEffect extends OneShotEffect { + + private static final FilterCard filter = new FilterCard("creature or Vehicle card"); + + static { + filter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + + OviyaAutomechArtisanEffect() { + super(Outcome.Benefit); + staticText = "you may put a creature or Vehicle card from your hand onto the battlefield. " + + "If you put an artifact onto the battlefield this way, put two +1/+1 counters on it"; + } + + private OviyaAutomechArtisanEffect(final OviyaAutomechArtisanEffect effect) { + super(effect); + } + + @Override + public OviyaAutomechArtisanEffect copy() { + return new OviyaAutomechArtisanEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + TargetCard target = new TargetCardInHand(0, 1, filter); + player.choose(outcome, player.getHand(), target, source, game); + Card card = game.getCard(target.getFirstTarget()); + if (card == null) { + return false; + } + player.moveCards(card, Zone.BATTLEFIELD, source, game); + Permanent permanent = game.getPermanent(card.getId()); + if (permanent != null && permanent.isArtifact(game)) { + permanent.addCounters(CounterType.P1P1.createInstance(2), source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/p/PactdollTerror.java b/Mage.Sets/src/mage/cards/p/PactdollTerror.java new file mode 100644 index 00000000000..5770f25e2ab --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PactdollTerror.java @@ -0,0 +1,46 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldThisOrAnotherTriggeredAbility; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class PactdollTerror extends CardImpl { + + public PactdollTerror(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{3}{B}"); + + this.subtype.add(SubType.TOY); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // Whenever this creature or another artifact you control enters, each opponent loses 1 life and you gain 1 life. + Ability ability = new EntersBattlefieldThisOrAnotherTriggeredAbility( + new LoseLifeOpponentsEffect(1), + StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT, + false, false + ); + ability.addEffect(new GainLifeEffect(1).concatBy("and")); + this.addAbility(ability); + } + + private PactdollTerror(final PactdollTerror card) { + super(card); + } + + @Override + public PactdollTerror copy() { + return new PactdollTerror(this); + } +} diff --git a/Mage.Sets/src/mage/cards/p/ParanoidParishBlade.java b/Mage.Sets/src/mage/cards/p/ParanoidParishBlade.java index fe65991fa3f..d2efed218cf 100644 --- a/Mage.Sets/src/mage/cards/p/ParanoidParishBlade.java +++ b/Mage.Sets/src/mage/cards/p/ParanoidParishBlade.java @@ -7,16 +7,15 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FirstStrikeAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @author fireshoes @@ -38,7 +37,7 @@ public final class ParanoidParishBlade extends CardImpl { effect = new ConditionalContinuousEffect(new GainAbilitySourceEffect(FirstStrikeAbility.getInstance(), Duration.WhileOnBattlefield), DeliriumCondition.instance, "and has first strike as long as there are four or more card types among cards in your graveyard."); ability.addEffect(effect); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/p/PatchworkBeastie.java b/Mage.Sets/src/mage/cards/p/PatchworkBeastie.java index e37891b49f0..08298aa869e 100644 --- a/Mage.Sets/src/mage/cards/p/PatchworkBeastie.java +++ b/Mage.Sets/src/mage/cards/p/PatchworkBeastie.java @@ -2,12 +2,12 @@ package mage.cards.p; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.effects.RestrictionEffect; import mage.abilities.effects.common.MillCardsControllerEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -31,7 +31,7 @@ public final class PatchworkBeastie extends CardImpl { // Delirium -- Patchwork Beastie can't attack or block unless there are four or more card types among cards in your graveyard. this.addAbility(new SimpleStaticAbility(new PatchworkBeastieEffect()) .setAbilityWord(AbilityWord.DELIRIUM) - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); // At the beginning of your upkeep, you may mill a card. this.addAbility(new BeginningOfUpkeepTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/p/PeemaAetherSeer.java b/Mage.Sets/src/mage/cards/p/PeemaAetherSeer.java index 60d0ae73e03..1b38e871a9c 100644 --- a/Mage.Sets/src/mage/cards/p/PeemaAetherSeer.java +++ b/Mage.Sets/src/mage/cards/p/PeemaAetherSeer.java @@ -36,7 +36,7 @@ public final class PeemaAetherSeer extends CardImpl { // When Peema Aether-Seer enters the battlefield, you get an amount of {E} equal to the greatest power among creatures you control. Effect effect = new GetEnergyCountersControllerEffect(GreatestPowerAmongControlledCreaturesValue.instance); effect.setText("you get an amount of {E} equal to the greatest power among creatures you control"); - this.addAbility(new EntersBattlefieldTriggeredAbility(effect)); + this.addAbility(new EntersBattlefieldTriggeredAbility(effect).addHint(GreatestPowerAmongControlledCreaturesValue.getHint())); // Pay {E}{E}{E}: Target creature blocks this turn if able. Ability ability = new SimpleActivatedAbility(new BlocksIfAbleTargetEffect(Duration.EndOfTurn), new PayEnergyCost(3)); diff --git a/Mage.Sets/src/mage/cards/p/PeemaTrailblazer.java b/Mage.Sets/src/mage/cards/p/PeemaTrailblazer.java new file mode 100644 index 00000000000..eb050ad05f8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PeemaTrailblazer.java @@ -0,0 +1,60 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.costs.common.PayEnergyCost; +import mage.abilities.dynamicvalue.common.GreatestPowerAmongControlledCreaturesValue; +import mage.abilities.dynamicvalue.common.SavedDamageValue; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.common.counter.GetEnergyCountersControllerEffect; +import mage.abilities.keyword.ExhaustAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; + +import java.util.UUID; + +/** + * @author sobiech + */ +public final class PeemaTrailblazer extends CardImpl { + + public PeemaTrailblazer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.ELEPHANT); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Whenever this creature deals combat damage to a player, you get that many {E}. + this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(new GetEnergyCountersControllerEffect(SavedDamageValue.MANY))); + + // Exhaust -- Pay six {E}: Put two +1/+1 counters on this creature. Then draw cards equal to the greatest power among creatures you control. + final Ability ability = new ExhaustAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)), new PayEnergyCost(6) + ); + ability.addEffect(new DrawCardSourceControllerEffect(GreatestPowerAmongControlledCreaturesValue.instance)); + ability.addHint(GreatestPowerAmongControlledCreaturesValue.getHint()); + this.addAbility(ability); + + + } + + private PeemaTrailblazer(final PeemaTrailblazer card) { + super(card); + } + + @Override + public PeemaTrailblazer copy() { + return new PeemaTrailblazer(this); + } +} diff --git a/Mage.Sets/src/mage/cards/p/PeerPastTheVeil.java b/Mage.Sets/src/mage/cards/p/PeerPastTheVeil.java index 48bf1c5a09b..d94f3594410 100644 --- a/Mage.Sets/src/mage/cards/p/PeerPastTheVeil.java +++ b/Mage.Sets/src/mage/cards/p/PeerPastTheVeil.java @@ -3,7 +3,6 @@ package mage.cards.p; import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.discard.DiscardHandControllerEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -21,7 +20,7 @@ public final class PeerPastTheVeil extends CardImpl { // Discard your hand. Then draw X cards, where X is the number of card types among cards in your graveyard. this.getSpellAbility().addEffect(new DiscardHandControllerEffect()); this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(CardTypesInGraveyardCount.YOU).concatBy("Then")); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private PeerPastTheVeil(final PeerPastTheVeil card) { diff --git a/Mage.Sets/src/mage/cards/p/PerilousSnare.java b/Mage.Sets/src/mage/cards/p/PerilousSnare.java new file mode 100644 index 00000000000..0163e0b0ba7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PerilousSnare.java @@ -0,0 +1,52 @@ +package mage.cards.p; + +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.common.ExileUntilSourceLeavesEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class PerilousSnare extends CardImpl { + + public PerilousSnare(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{W}"); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // When this artifact enters, exile target nonland permanent an opponent controls until this artifact leaves the battlefield. + Ability ability = new EntersBattlefieldTriggeredAbility(new ExileUntilSourceLeavesEffect()); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_NON_LAND)); + this.addAbility(ability); + + // Max speed -- {T}: Put a +1/+1 counter on target creature or Vehicle you control. Activate only as a sorcery. + ability = new ActivateAsSorceryActivatedAbility( + new AddCountersTargetEffect(CounterType.P1P1.createInstance()), new TapSourceCost() + ); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE)); + this.addAbility(new MaxSpeedAbility(ability)); + } + + private PerilousSnare(final PerilousSnare card) { + super(card); + } + + @Override + public PerilousSnare copy() { + return new PerilousSnare(this); + } +} diff --git a/Mage.Sets/src/mage/cards/p/PickTheBrain.java b/Mage.Sets/src/mage/cards/p/PickTheBrain.java index 73b8dd0c9e7..400808889f9 100644 --- a/Mage.Sets/src/mage/cards/p/PickTheBrain.java +++ b/Mage.Sets/src/mage/cards/p/PickTheBrain.java @@ -3,10 +3,9 @@ package mage.cards.p; import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.Mode; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -31,7 +30,7 @@ public final class PickTheBrain extends CardImpl { // Delirium — If there are four or more card types among cards in your graveyard, search that player's graveyard, hand, and library for any number of cards with the same name as the exiled card, exile those cards, then that player shuffles their library. this.getSpellAbility().addEffect(new PickTheBrainEffect()); this.getSpellAbility().addTarget(new TargetOpponent()); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private PickTheBrain(final PickTheBrain card) { diff --git a/Mage.Sets/src/mage/cards/p/PitAutomaton.java b/Mage.Sets/src/mage/cards/p/PitAutomaton.java new file mode 100644 index 00000000000..80b0021ae8f --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PitAutomaton.java @@ -0,0 +1,100 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.CopyTargetStackObjectEffect; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; +import mage.abilities.keyword.DefenderAbility; +import mage.abilities.keyword.ExhaustAbility; +import mage.abilities.mana.ConditionalColorlessManaAbility; +import mage.abilities.mana.builder.common.ActivatedAbilityManaBuilder; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.StackObject; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class PitAutomaton extends CardImpl { + + public PitAutomaton(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{2}"); + + this.subtype.add(SubType.CONSTRUCT); + this.power = new MageInt(0); + this.toughness = new MageInt(4); + + // Defender + this.addAbility(DefenderAbility.getInstance()); + + // {T}: Add {C}{C}. Spend this mana only to activate abilities. + this.addAbility(new ConditionalColorlessManaAbility(2, new ActivatedAbilityManaBuilder())); + + // {2}, {T}: When you next activate an exhaust ability this turn, copy it. You may choose new targets for the copy. + Ability ability = new SimpleActivatedAbility( + new CreateDelayedTriggeredAbilityEffect(new PitAutomatonTriggeredAbility()), new GenericManaCost(2) + ); + ability.addCost(new TapSourceCost()); + this.addAbility(ability); + } + + private PitAutomaton(final PitAutomaton card) { + super(card); + } + + @Override + public PitAutomaton copy() { + return new PitAutomaton(this); + } +} + +class PitAutomatonTriggeredAbility extends DelayedTriggeredAbility { + + PitAutomatonTriggeredAbility() { + super(new CopyTargetStackObjectEffect(true), Duration.EndOfTurn, true, false); + } + + private PitAutomatonTriggeredAbility(final PitAutomatonTriggeredAbility ability) { + super(ability); + } + + @Override + public PitAutomatonTriggeredAbility copy() { + return new PitAutomatonTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ACTIVATED_ABILITY; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!isControlledBy(event.getPlayerId())) { + return false; + } + StackObject stackObject = game.getStack().getStackObject(event.getTargetId()); + if (stackObject == null || !(stackObject.getStackAbility() instanceof ExhaustAbility)) { + return false; + } + this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId())); + return true; + } + + @Override + public String getRule() { + return "When you next activate an exhaust ability that isn't a mana ability this turn, copy it. You may choose new targets for the copy."; + } +} diff --git a/Mage.Sets/src/mage/cards/p/PointTheWay.java b/Mage.Sets/src/mage/cards/p/PointTheWay.java new file mode 100644 index 00000000000..24159b7c22c --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PointTheWay.java @@ -0,0 +1,90 @@ +package mage.cards.p; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +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.common.TargetCardInLibrary; + +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author TheElk801 + */ +public final class PointTheWay extends CardImpl { + + public PointTheWay(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{G}"); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // {3}{G}, Sacrifice this enchantment: Search your library for up to X basic land cards, where X is your speed. Put those cards onto the battlefield tapped, then shuffle. + Ability ability = new SimpleActivatedAbility(new PointTheWayEffect(), new ManaCostsImpl<>("{3}{G}")); + ability.addCost(new SacrificeSourceCost()); + this.addAbility(ability); + } + + private PointTheWay(final PointTheWay card) { + super(card); + } + + @Override + public PointTheWay copy() { + return new PointTheWay(this); + } +} + +class PointTheWayEffect extends OneShotEffect { + + PointTheWayEffect() { + super(Outcome.Benefit); + staticText = "search your library for up to X basic land cards, where X is your speed. " + + "Put those cards onto the battlefield tapped, then shuffle"; + } + + private PointTheWayEffect(final PointTheWayEffect effect) { + super(effect); + } + + @Override + public PointTheWayEffect copy() { + return new PointTheWayEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + TargetCardInLibrary target = new TargetCardInLibrary( + 0, player.getSpeed(), StaticFilters.FILTER_CARD_BASIC_LANDS + ); + player.searchLibrary(target, source, game); + Set cards = target + .getTargets() + .stream() + .map(uuid -> player.getLibrary().getCard(uuid, game)) + .collect(Collectors.toSet()); + player.moveCards( + cards, Zone.BATTLEFIELD, source, game, true, + false, false, null + ); + player.shuffleLibrary(source, game); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/p/Polygoyf.java b/Mage.Sets/src/mage/cards/p/Polygoyf.java index d2c4f209d63..6ec1a25d4c5 100644 --- a/Mage.Sets/src/mage/cards/p/Polygoyf.java +++ b/Mage.Sets/src/mage/cards/p/Polygoyf.java @@ -20,8 +20,6 @@ import mage.constants.Zone; */ public final class Polygoyf extends CardImpl { - private static final DynamicValue powerValue = CardTypesInGraveyardCount.ALL; - public Polygoyf(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); @@ -36,7 +34,9 @@ public final class Polygoyf extends CardImpl { this.addAbility(new MyriadAbility()); // Polygoyf's power is equal to the number of card types among cards in all graveyards and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.ALL) + ).addHint(CardTypesInGraveyardCount.ALL.getHint())); } private Polygoyf(final Polygoyf card) { diff --git a/Mage.Sets/src/mage/cards/p/PotholeMole.java b/Mage.Sets/src/mage/cards/p/PotholeMole.java new file mode 100644 index 00000000000..df67953aaf8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PotholeMole.java @@ -0,0 +1,80 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +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; +import mage.target.TargetCard; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class PotholeMole extends CardImpl { + + public PotholeMole(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.MOLE); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // When this creature enters, mill three cards, then you may return a land card from your graveyard to your hand. + Ability ability = new EntersBattlefieldTriggeredAbility(new MillCardsControllerEffect(3)); + ability.addEffect(new PotholeMoleEffect()); + this.addAbility(ability); + } + + private PotholeMole(final PotholeMole card) { + super(card); + } + + @Override + public PotholeMole copy() { + return new PotholeMole(this); + } +} + +class PotholeMoleEffect extends OneShotEffect { + + PotholeMoleEffect() { + super(Outcome.Benefit); + staticText = ", then you may return a land card from your graveyard to your hand"; + } + + private PotholeMoleEffect(final PotholeMoleEffect effect) { + super(effect); + } + + @Override + public PotholeMoleEffect copy() { + return new PotholeMoleEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + TargetCard target = new TargetCardInYourGraveyard( + 0, 1, StaticFilters.FILTER_CARD_LAND, true + ); + player.choose(outcome, player.getGraveyard(), target, source, game); + Card card = game.getCard(target.getFirstTarget()); + return card != null && player.moveCards(card, Zone.HAND, source, game); + } +} diff --git a/Mage.Sets/src/mage/cards/p/PrideOfTheRoad.java b/Mage.Sets/src/mage/cards/p/PrideOfTheRoad.java index 3bf89a5212b..410c1383a88 100644 --- a/Mage.Sets/src/mage/cards/p/PrideOfTheRoad.java +++ b/Mage.Sets/src/mage/cards/p/PrideOfTheRoad.java @@ -12,9 +12,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.filter.FilterPermanent; -import mage.filter.common.FilterControlledPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import java.util.UUID; @@ -24,15 +22,6 @@ import java.util.UUID; */ public final class PrideOfTheRoad extends CardImpl { - private static final FilterPermanent filter = new FilterControlledPermanent("creature or Vehicle you control"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public PrideOfTheRoad(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); @@ -50,7 +39,7 @@ public final class PrideOfTheRoad extends CardImpl { // Max speed -- At the beginning of combat on your turn, target creature or Vehicle you control gains double strike until end of turn. Ability ability = new BeginningOfCombatTriggeredAbility(new GainAbilityTargetEffect(DoubleStrikeAbility.getInstance())); - ability.addTarget(new TargetPermanent(filter)); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE)); this.addAbility(new MaxSpeedAbility(ability)); } diff --git a/Mage.Sets/src/mage/cards/p/PriorityBoarding.java b/Mage.Sets/src/mage/cards/p/PriorityBoarding.java new file mode 100644 index 00000000000..163cd0d3cc8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PriorityBoarding.java @@ -0,0 +1,123 @@ +package mage.cards.p; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTopXMayPlayUntilEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.CardsImpl; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.DieRolledEvent; +import mage.game.events.GameEvent; +import mage.players.Player; + +/** + * + * @author Grath + */ +public final class PriorityBoarding extends CardImpl { + + public PriorityBoarding(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{R}"); + + // Whenever you roll a die, you may reveal the top card of your library. Do this only once each turn. Whenever you reveal a card with mana value less than the result this way, you may exile it. If you do, you may play it this turn. + this.addAbility(new PriorityBoardingTriggeredAbility()); + } + + private PriorityBoarding(final PriorityBoarding card) { + super(card); + } + + @Override + public PriorityBoarding copy() { + return new PriorityBoarding(this); + } +} + +class PriorityBoardingEffect extends OneShotEffect { + + PriorityBoardingEffect() { + super(Outcome.Benefit); + } + + private PriorityBoardingEffect(final PriorityBoardingEffect effect) { + super(effect); + } + + @Override + public PriorityBoardingEffect copy() { + return new PriorityBoardingEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Card card = player.getLibrary().getFromTop(game); + if (card == null) { + return false; + } + player.revealCards(source, new CardsImpl(card), game); + if (getValue("rolled") == null) { + return true; + } + int amount = (Integer) getValue("rolled"); + if (amount > card.getManaValue()) { + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility( + new ExileTopXMayPlayUntilEffect(1, Duration.EndOfTurn) + .withTextOptions("it", true), true + ); + game.fireReflexiveTriggeredAbility(ability, source); + return true; + } + return true; + } +} + +class PriorityBoardingTriggeredAbility extends TriggeredAbilityImpl { + + PriorityBoardingTriggeredAbility() { + super(Zone.BATTLEFIELD, new PriorityBoardingEffect(), true); + this.setDoOnlyOnceEachTurn(true); + } + + private PriorityBoardingTriggeredAbility(final PriorityBoardingTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DIE_ROLLED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + DieRolledEvent drEvent = (DieRolledEvent) event; + if (this.isControlledBy(event.getTargetId()) && drEvent.getRollDieType() == RollDieType.NUMERICAL) { + int result = drEvent.getResult(); + this.getEffects().setValue("rolled", result); + return true; + } + return false; + } + + @Override + public PriorityBoardingTriggeredAbility copy() { + return new PriorityBoardingTriggeredAbility(this); + } + + @Override + public String getRule() { + return "Whenever you roll a die, you may reveal the top card of your library. Do this only once each turn. " + + "Whenever you reveal a card with mana value less than the result this way, you may exile it. If you " + + "do, you may play it this turn."; + } +} diff --git a/Mage.Sets/src/mage/cards/p/PropheticTitan.java b/Mage.Sets/src/mage/cards/p/PropheticTitan.java index 8edc084f090..f0e43e2e5c6 100644 --- a/Mage.Sets/src/mage/cards/p/PropheticTitan.java +++ b/Mage.Sets/src/mage/cards/p/PropheticTitan.java @@ -4,9 +4,9 @@ import mage.MageInt; import mage.abilities.Mode; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -58,7 +58,7 @@ class PropheticTitanTriggeredAbility extends EntersBattlefieldTriggeredAbility { "choose one. If there are four or more card types among cards in your graveyard, choose both instead." ); this.addTarget(new TargetAnyTarget()); - this.addHint(CardTypesInGraveyardHint.YOU); + this.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.setAbilityWord(AbilityWord.DELIRIUM); } diff --git a/Mage.Sets/src/mage/cards/p/Pyrogoyf.java b/Mage.Sets/src/mage/cards/p/Pyrogoyf.java index 2ca910fbf83..a3d8715e2d4 100644 --- a/Mage.Sets/src/mage/cards/p/Pyrogoyf.java +++ b/Mage.Sets/src/mage/cards/p/Pyrogoyf.java @@ -27,8 +27,6 @@ public final class Pyrogoyf extends CardImpl { private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(SubType.LHURGOYF, "Lhurgoyf creature"); - private static final DynamicValue powerValue = CardTypesInGraveyardCount.ALL; - public Pyrogoyf(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); this.subtype.add(SubType.LHURGOYF); @@ -37,7 +35,9 @@ public final class Pyrogoyf extends CardImpl { this.toughness = new MageInt(1); // Pyrogoyf's power is equal to the number of card types among cards in all graveyards and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.ALL) + ).addHint(CardTypesInGraveyardCount.ALL.getHint())); // Whenever Pyrogoyf or another Lhurgoyf creature you control enters, that creature deals damage equal to its power to any target. Ability ability = new EntersBattlefieldThisOrAnotherTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/r/RabbleRousing.java b/Mage.Sets/src/mage/cards/r/RabbleRousing.java index ba6b3c4b414..a0b5e7188fb 100644 --- a/Mage.Sets/src/mage/cards/r/RabbleRousing.java +++ b/Mage.Sets/src/mage/cards/r/RabbleRousing.java @@ -34,7 +34,7 @@ public final class RabbleRousing extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{W}"); // Hideaway 5 - this.addAbility(new HideawayAbility(5)); + this.addAbility(new HideawayAbility(this, 5)); // Whenever you attack with one or more creatures, create that many 1/1 green and white Citizen creature tokens. Then if you control ten or more creatures, you may play the exiled card without paying its mana cost. Ability ability = new AttacksWithCreaturesTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/r/RavingVisionary.java b/Mage.Sets/src/mage/cards/r/RavingVisionary.java index 860b269fbf6..5703d4286b3 100644 --- a/Mage.Sets/src/mage/cards/r/RavingVisionary.java +++ b/Mage.Sets/src/mage/cards/r/RavingVisionary.java @@ -7,9 +7,9 @@ import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.DrawDiscardControllerEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -45,7 +45,7 @@ public final class RavingVisionary extends CardImpl { new ManaCostsImpl<>("{2}{U}"), DeliriumCondition.instance ); ability.addCost(new TapSourceCost()); - this.addAbility(ability.addHint(CardTypesInGraveyardHint.YOU).setAbilityWord(AbilityWord.DELIRIUM)); + this.addAbility(ability.addHint(CardTypesInGraveyardCount.YOU.getHint()).setAbilityWord(AbilityWord.DELIRIUM)); } private RavingVisionary(final RavingVisionary card) { diff --git a/Mage.Sets/src/mage/cards/r/ReaperOfFlightMoonsilver.java b/Mage.Sets/src/mage/cards/r/ReaperOfFlightMoonsilver.java index 8c85c99c616..598639a9933 100644 --- a/Mage.Sets/src/mage/cards/r/ReaperOfFlightMoonsilver.java +++ b/Mage.Sets/src/mage/cards/r/ReaperOfFlightMoonsilver.java @@ -6,8 +6,8 @@ import mage.MageInt; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.costs.common.SacrificeTargetCost; import mage.abilities.decorator.ConditionalActivatedAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -18,8 +18,6 @@ import mage.constants.Zone; import mage.filter.StaticFilters; -import mage.target.common.TargetControlledCreaturePermanent; - /** * @author fireshoes */ @@ -42,7 +40,7 @@ public final class ReaperOfFlightMoonsilver extends CardImpl { DeliriumCondition.instance, "Delirium — Sacrifice another creature: Reaper of Flight Moonsilver gets +2/+1 until end of turn. " + "Activate only if there are four or more card types among cards in your graveyard.") - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); } private ReaperOfFlightMoonsilver(final ReaperOfFlightMoonsilver card) { diff --git a/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java b/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java new file mode 100644 index 00000000000..66781d9082d --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java @@ -0,0 +1,84 @@ +package mage.cards.r; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RecklessVelocitaur extends CardImpl { + + public RecklessVelocitaur(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); + + this.subtype.add(SubType.MINOTAUR); + this.subtype.add(SubType.PILOT); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Whenever this creature saddles a Mount or crews a Vehicle during your main phase, that Mount or Vehicle gets +2/+0 and gains trample until end of turn. + this.addAbility(new RecklessVelocitaurTriggeredAbility()); + } + + private RecklessVelocitaur(final RecklessVelocitaur card) { + super(card); + } + + @Override + public RecklessVelocitaur copy() { + return new RecklessVelocitaur(this); + } +} + +class RecklessVelocitaurTriggeredAbility extends TriggeredAbilityImpl { + + RecklessVelocitaurTriggeredAbility() { + super(Zone.BATTLEFIELD, new BoostTargetEffect(2, 0).setText("that Mount or Vehicle gets +2/+0")); + this.addEffect(new GainAbilityTargetEffect(TrampleAbility.getInstance()).setText("and gains trample until end of turn")); + this.setTriggerPhrase("Whenever {this} saddles a Mount or crews a Vehicle during your main phase, "); + } + + private RecklessVelocitaurTriggeredAbility(final RecklessVelocitaurTriggeredAbility ability) { + super(ability); + } + + @Override + public RecklessVelocitaurTriggeredAbility copy() { + return new RecklessVelocitaurTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + switch (event.getType()) { + case SADDLED_MOUNT: + case CREWED_VEHICLE: + return true; + default: + return false; + } + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!this.isControlledBy(game.getActivePlayerId()) + || !game.isMainPhase() + || !event.getTargetId().equals(this.getSourceId())) { + return false; + } + this.getEffects().setTargetPointer(new FixedTarget(event.getSourceId())); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/r/ReckonerShakedown.java b/Mage.Sets/src/mage/cards/r/ReckonerShakedown.java index 670a2c37251..0d616b47e77 100644 --- a/Mage.Sets/src/mage/cards/r/ReckonerShakedown.java +++ b/Mage.Sets/src/mage/cards/r/ReckonerShakedown.java @@ -7,12 +7,8 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; -import mage.constants.SubType; import mage.counters.CounterType; -import mage.filter.FilterPermanent; import mage.filter.StaticFilters; -import mage.filter.common.FilterControlledPermanent; -import mage.filter.predicate.Predicates; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; @@ -48,15 +44,6 @@ public final class ReckonerShakedown extends CardImpl { class ReckonerShakedownEffect extends OneShotEffect { - private static final FilterPermanent filter = new FilterControlledPermanent("creature or Vehicle you control"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - ReckonerShakedownEffect() { super(Outcome.Benefit); staticText = "target opponent reveals their hand. You may choose a nonland card from it. " + @@ -88,10 +75,10 @@ class ReckonerShakedownEffect extends OneShotEffect { player.discard(card, false, source, game); return true; } - if (!game.getBattlefield().contains(filter, source, game, 1)) { + if (!game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE, source, game, 1)) { return true; } - TargetPermanent targetPermanent = new TargetPermanent(filter); + TargetPermanent targetPermanent = new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE); targetPermanent.withNotTarget(true); controller.choose(Outcome.BoostCreature, targetPermanent, source, game); Permanent permanent = game.getPermanent(targetPermanent.getFirstTarget()); diff --git a/Mage.Sets/src/mage/cards/r/RedshiftRocketeerChief.java b/Mage.Sets/src/mage/cards/r/RedshiftRocketeerChief.java new file mode 100644 index 00000000000..f85de1ab50b --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RedshiftRocketeerChief.java @@ -0,0 +1,144 @@ +package mage.cards.r; + +import mage.MageInt; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.SourcePermanentPowerValue; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.mana.ManaEffect; +import mage.abilities.keyword.ExhaustAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.abilities.mana.SimpleManaAbility; +import mage.abilities.mana.builder.ConditionalManaBuilder; +import mage.abilities.mana.builder.common.ActivatedAbilityManaBuilder; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.choices.ChoiceColor; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInHand; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RedshiftRocketeerChief extends CardImpl { + + public RedshiftRocketeerChief(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.GOBLIN); + this.subtype.add(SubType.PILOT); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // {T}: Add X mana of any one color, where X is Redshift's power. Spend this mana only to activate abilities. + this.addAbility(new SimpleManaAbility(new RedshiftRocketeerChiefManaEffect(), new TapSourceCost())); + + // Exhaust -- {10}{R}{G}: Put any number of permanent cards from your hand onto the battlefield. + this.addAbility(new ExhaustAbility(new RedshiftRocketeerChiefEffect(), new ManaCostsImpl<>("{10}{R}{G}"))); + } + + private RedshiftRocketeerChief(final RedshiftRocketeerChief card) { + super(card); + } + + @Override + public RedshiftRocketeerChief copy() { + return new RedshiftRocketeerChief(this); + } +} + +class RedshiftRocketeerChiefManaEffect extends ManaEffect { + + private final ConditionalManaBuilder manaBuilder = new ActivatedAbilityManaBuilder(); + + RedshiftRocketeerChiefManaEffect() { + this.staticText = "Add X mana of any one color, where X is {this}'s power. " + manaBuilder.getRule(); + } + + private RedshiftRocketeerChiefManaEffect(final RedshiftRocketeerChiefManaEffect effect) { + super(effect); + } + + @Override + public List getNetMana(Game game, Ability source) { + List netMana = new ArrayList<>(); + if (game == null) { + return netMana; + } + int currentPower = SourcePermanentPowerValue.NOT_NEGATIVE.calculate(game, source, null); + netMana.add(manaBuilder.setMana(Mana.BlackMana(currentPower), source, game).build()); + netMana.add(manaBuilder.setMana(Mana.BlueMana(currentPower), source, game).build()); + netMana.add(manaBuilder.setMana(Mana.RedMana(currentPower), source, game).build()); + netMana.add(manaBuilder.setMana(Mana.GreenMana(currentPower), source, game).build()); + netMana.add(manaBuilder.setMana(Mana.WhiteMana(currentPower), source, game).build()); + return netMana; + } + + @Override + public Mana produceMana(Game game, Ability source) { + Mana mana = new Mana(); + if (game == null) { + return mana; + } + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return mana; + } + ChoiceColor choice = new ChoiceColor(); + if (!controller.choose(Outcome.PutManaInPool, choice, game)) { + return mana; + } + Mana chosen = choice.getMana(SourcePermanentPowerValue.NOT_NEGATIVE.calculate(game, source, null)); + return manaBuilder.setMana(chosen, source, game).build(); + } + + @Override + public RedshiftRocketeerChiefManaEffect copy() { + return new RedshiftRocketeerChiefManaEffect(this); + } +} + +class RedshiftRocketeerChiefEffect extends OneShotEffect { + + RedshiftRocketeerChiefEffect() { + super(Outcome.Benefit); + staticText = "put any number of permanent cards from your hand onto the battlefield"; + } + + private RedshiftRocketeerChiefEffect(final RedshiftRocketeerChiefEffect effect) { + super(effect); + } + + @Override + public RedshiftRocketeerChiefEffect copy() { + return new RedshiftRocketeerChiefEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + TargetCard target = new TargetCardInHand(0, Integer.MAX_VALUE, StaticFilters.FILTER_CARD_PERMANENTS); + player.choose(outcome, player.getHand(), target, source, game); + Cards cards = new CardsImpl(target.getTargets()); + return !cards.isEmpty() && player.moveCards(cards, Zone.BATTLEFIELD, source, game); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RepulsiveMutation.java b/Mage.Sets/src/mage/cards/r/RepulsiveMutation.java index af9c72f5d80..44a2aeace9e 100644 --- a/Mage.Sets/src/mage/cards/r/RepulsiveMutation.java +++ b/Mage.Sets/src/mage/cards/r/RepulsiveMutation.java @@ -34,6 +34,7 @@ public final class RepulsiveMutation extends CardImpl { .setTargetPointer(new SecondTargetPointer()) .setText("Then counter up to one target spell unless its controller pays mana equal to the greatest power among creatures you control.")); getSpellAbility().addTarget(new TargetSpell(0, 1, StaticFilters.FILTER_SPELL)); + getSpellAbility().addHint(GreatestPowerAmongControlledCreaturesValue.getHint()); } private RepulsiveMutation(final RepulsiveMutation card) { diff --git a/Mage.Sets/src/mage/cards/r/RepurposingBay.java b/Mage.Sets/src/mage/cards/r/RepurposingBay.java new file mode 100644 index 00000000000..4d622623d54 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RepurposingBay.java @@ -0,0 +1,99 @@ +package mage.cards.r; + +import java.util.Collection; +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.filter.common.FilterArtifactCard; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCardInLibrary; + +/** + * + * @author Callumvl + */ +public final class RepurposingBay extends CardImpl { + + public RepurposingBay(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{U}"); + + // {2}, {T}, Sacrifice another artifact: Search your library for an artifact card with mana value equal to 1 plus the sacrificed artifact's mana value, put that card onto the battlefield, then shuffle. Activate only as a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility( + new RepurposingBayEffect(), new ManaCostsImpl<>("{2}") + ); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeTargetCost(StaticFilters.FILTER_CONTROLLED_ANOTHER_ARTIFACT)); + this.addAbility(ability); + } + + private RepurposingBay(final RepurposingBay card) { + super(card); + } + + @Override + public RepurposingBay copy() { + return new RepurposingBay(this); + } +} + +class RepurposingBayEffect extends OneShotEffect { + + RepurposingBayEffect() { + super(Outcome.Benefit); + staticText = "search your library for an artifact card with mana value equal to 1 plus the " + + "sacrificed artifact's mana value, put that card onto the battlefield, then shuffle"; + } + + private RepurposingBayEffect(final RepurposingBayEffect effect) { + super(effect); + } + + @Override + public RepurposingBayEffect copy() { + return new RepurposingBayEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent sacrificed = source + .getCosts() + .stream() + .filter(SacrificeTargetCost.class::isInstance) + .map(SacrificeTargetCost.class::cast) + .map(SacrificeTargetCost::getPermanents) + .flatMap(Collection::stream) + .findFirst() + .orElse(null); + if (player == null || sacrificed == null) { + return false; + } + FilterCard filterCard = new FilterArtifactCard( + "artifact card with mana value " + (sacrificed.getManaValue() + 1) + ); + filterCard.add(new ManaValuePredicate(ComparisonType.EQUAL_TO, sacrificed.getManaValue() + 1)); + TargetCardInLibrary target = new TargetCardInLibrary(filterCard); + player.searchLibrary(target, source, game); + Card card = player.getLibrary().getCard(target.getFirstTarget(), game); + player.moveCards(card, Zone.BATTLEFIELD, source, game); + player.shuffleLibrary(source, game); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/r/ResurrectedCultist.java b/Mage.Sets/src/mage/cards/r/ResurrectedCultist.java index dabb83e56b8..fb1f574e1b9 100644 --- a/Mage.Sets/src/mage/cards/r/ResurrectedCultist.java +++ b/Mage.Sets/src/mage/cards/r/ResurrectedCultist.java @@ -4,8 +4,8 @@ import mage.MageInt; import mage.abilities.common.ActivateIfConditionActivatedAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.ReturnSourceFromGraveyardToBattlefieldWithCounterEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -31,7 +31,7 @@ public final class ResurrectedCultist extends CardImpl { Zone.GRAVEYARD, new ReturnSourceFromGraveyardToBattlefieldWithCounterEffect(CounterType.FINALITY.createInstance(), false), new ManaCostsImpl<>("{2}{B}{B}"), DeliriumCondition.instance, TimingRule.SORCERY - ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private ResurrectedCultist(final ResurrectedCultist card) { diff --git a/Mage.Sets/src/mage/cards/r/RhetTombMystic.java b/Mage.Sets/src/mage/cards/r/RhetTombMystic.java new file mode 100644 index 00000000000..0cd220bf0e7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RhetTombMystic.java @@ -0,0 +1,78 @@ +package mage.cards.r; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.keyword.CyclingAbility; +import mage.constants.*; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; + +/** + * + * @author sobiech + */ +public final class RhetTombMystic extends CardImpl { + + public RhetTombMystic(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.BIRD); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Each creature card in your hand has cycling {1}{U}. + this.addAbility(new SimpleStaticAbility(new RhetTombMysticEffect())); + } + + private RhetTombMystic(final RhetTombMystic card) { + super(card); + } + + @Override + public RhetTombMystic copy() { + return new RhetTombMystic(this); + } +} + +class RhetTombMysticEffect extends ContinuousEffectImpl { + + RhetTombMysticEffect() { + super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + this.staticText = "Each creature card in your hand has cycling {1}{B}"; + } + + private RhetTombMysticEffect(final RhetTombMysticEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + final Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) + return false; + + controller.getHand().getCards(StaticFilters.FILTER_CARD_CREATURE, game) + .forEach(card -> game.getState().addOtherAbility(card, new CyclingAbility(new ManaCostsImpl<>("{1}{B}")))); + + return true; + } + + @Override + public ContinuousEffect copy() { + return new RhetTombMysticEffect(this); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RidesEnd.java b/Mage.Sets/src/mage/cards/r/RidesEnd.java index f47ffe711b7..2f4721e3c20 100644 --- a/Mage.Sets/src/mage/cards/r/RidesEnd.java +++ b/Mage.Sets/src/mage/cards/r/RidesEnd.java @@ -8,11 +8,10 @@ import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Zone; import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.Predicates; import mage.filter.predicate.permanent.TappedPredicate; import mage.target.TargetPermanent; @@ -23,15 +22,10 @@ import java.util.UUID; */ public final class RidesEnd extends CardImpl { - private static final FilterPermanent filter = new FilterCreaturePermanent("a tapped creature"); - private static final FilterPermanent filter2 = new FilterPermanent("creature or Vehicle"); + private static final FilterPermanent filter = new FilterCreaturePermanent("a tapped permanent"); static { filter.add(TappedPredicate.TAPPED); - filter2.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); } private static final Condition condition = new SourceTargetsPermanentCondition(filter); @@ -46,7 +40,7 @@ public final class RidesEnd extends CardImpl { // Exile target creature or Vehicle. this.getSpellAbility().addEffect(new ExileTargetEffect()); - this.getSpellAbility().addTarget(new TargetPermanent(filter2)); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); } private RidesEnd(final RidesEnd card) { diff --git a/Mage.Sets/src/mage/cards/r/RiseFromTheWreck.java b/Mage.Sets/src/mage/cards/r/RiseFromTheWreck.java new file mode 100644 index 00000000000..e388e7f6066 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RiseFromTheWreck.java @@ -0,0 +1,54 @@ +package mage.cards.r; + +import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.filter.predicate.mageobject.NoAbilityPredicate; +import mage.target.common.TargetCardInYourGraveyard; +import mage.target.targetpointer.EachTargetPointer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RiseFromTheWreck extends CardImpl { + + private static final FilterCard filter = new FilterCard("Mount card"); + private static final FilterCard filter2 = new FilterCard("Vehicle card"); + private static final FilterCard filter3 = new FilterCard("creature card with no abilities"); + + static { + filter.add(SubType.MOUNT.getPredicate()); + filter2.add(SubType.VEHICLE.getPredicate()); + filter3.add(NoAbilityPredicate.instance); + } + + public RiseFromTheWreck(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{G}"); + + // Return up to one target creature card, up to one target Mount card, up to one target Vehicle card, and up to one target creature card with no abilities from your graveyard to your hand. + this.getSpellAbility().addEffect(new ReturnFromGraveyardToHandTargetEffect() + .setTargetPointer(new EachTargetPointer()) + .setText("return up to one target creature card, up to one target Mount card, " + + "up to one target Vehicle card, and up to one target creature card " + + "with no abilities from your graveyard to your hand")); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(0, 1, StaticFilters.FILTER_CARD_CREATURE)); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(0, 1, filter)); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(0, 1, filter2)); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(0, 1, filter3)); + } + + private RiseFromTheWreck(final RiseFromTheWreck card) { + super(card); + } + + @Override + public RiseFromTheWreck copy() { + return new RiseFromTheWreck(this); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RishkarsExpertise.java b/Mage.Sets/src/mage/cards/r/RishkarsExpertise.java index e2635e62af7..21473b135bf 100644 --- a/Mage.Sets/src/mage/cards/r/RishkarsExpertise.java +++ b/Mage.Sets/src/mage/cards/r/RishkarsExpertise.java @@ -31,6 +31,7 @@ public final class RishkarsExpertise extends CardImpl { Effect effect = new DrawCardSourceControllerEffect(GreatestPowerAmongControlledCreaturesValue.instance); effect.setText("Draw cards equal to the greatest power among creatures you control"); this.getSpellAbility().addEffect(effect); + this.getSpellAbility().addHint(GreatestPowerAmongControlledCreaturesValue.getHint()); // You may cast a card with converted mana cost 5 or less from your hand without paying its mana cost. this.getSpellAbility().addEffect(new CastFromHandForFreeEffect(filter).concatBy("
")); diff --git a/Mage.Sets/src/mage/cards/r/RiteOfUndoing.java b/Mage.Sets/src/mage/cards/r/RiteOfUndoing.java index 809df0f9c2f..ac49502d802 100644 --- a/Mage.Sets/src/mage/cards/r/RiteOfUndoing.java +++ b/Mage.Sets/src/mage/cards/r/RiteOfUndoing.java @@ -29,7 +29,7 @@ public final class RiteOfUndoing extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{4}{U}"); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // Return target nonland permanent you control and target nonland permanent you don't control to their owners' hands. this.getSpellAbility().addEffect(new ReturnToHandTargetEffect().setTargetPointer(new EachTargetPointer())); diff --git a/Mage.Sets/src/mage/cards/r/RoadRage.java b/Mage.Sets/src/mage/cards/r/RoadRage.java new file mode 100644 index 00000000000..c9992324db8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RoadRage.java @@ -0,0 +1,61 @@ +package mage.cards.r; + +import mage.abilities.dynamicvalue.AdditiveDynamicValue; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.common.DamageTargetEffect; +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.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.target.common.TargetCreatureOrPlaneswalker; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RoadRage extends CardImpl { + + private static final FilterPermanent filter = new FilterControlledPermanent("Mounts and Vehicles you control"); + + static { + filter.add(Predicates.or( + SubType.MOUNT.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + + private static final DynamicValue xValue = new AdditiveDynamicValue( + new PermanentsOnBattlefieldCount(filter), StaticValue.get(2) + ); + private static final Hint hint = new ValueHint( + "Mounts and Vehicles you control", new PermanentsOnBattlefieldCount(filter) + ); + + public RoadRage(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}"); + + // Road Rage deals X damage to target creature or planeswalker, where X is 2 plus the number of Mounts and Vehicles you control. + this.getSpellAbility().addEffect(new DamageTargetEffect(xValue) + .setText("{this} deals X damage to target creature or planeswalker, " + + "where X is 2 plus the number of Mounts and Vehicles you control")); + this.getSpellAbility().addTarget(new TargetCreatureOrPlaneswalker()); + this.getSpellAbility().addHint(hint); + } + + private RoadRage(final RoadRage card) { + super(card); + } + + @Override + public RoadRage copy() { + return new RoadRage(this); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RoadsideAssistance.java b/Mage.Sets/src/mage/cards/r/RoadsideAssistance.java index 1f28f26c923..b9556ce79e9 100644 --- a/Mage.Sets/src/mage/cards/r/RoadsideAssistance.java +++ b/Mage.Sets/src/mage/cards/r/RoadsideAssistance.java @@ -15,8 +15,7 @@ import mage.constants.AttachmentType; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.game.permanent.token.PilotSaddleCrewToken; import mage.target.TargetPermanent; @@ -27,22 +26,13 @@ import java.util.UUID; */ public final class RoadsideAssistance extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public RoadsideAssistance(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}"); this.subtype.add(SubType.AURA); // Enchant creature or Vehicle - TargetPermanent auraTarget = new TargetPermanent(filter); + TargetPermanent auraTarget = new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE); this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); this.addAbility(new EnchantAbility(auraTarget)); diff --git a/Mage.Sets/src/mage/cards/r/RoaringEarth.java b/Mage.Sets/src/mage/cards/r/RoaringEarth.java index 56dbbd2c758..1a1e6b03baf 100644 --- a/Mage.Sets/src/mage/cards/r/RoaringEarth.java +++ b/Mage.Sets/src/mage/cards/r/RoaringEarth.java @@ -13,10 +13,7 @@ import mage.constants.CardType; import mage.constants.Duration; import mage.constants.SubType; import mage.counters.CounterType; -import mage.filter.FilterPermanent; import mage.filter.StaticFilters; -import mage.filter.common.FilterControlledPermanent; -import mage.filter.predicate.Predicates; import mage.game.permanent.token.custom.CreatureToken; import mage.target.TargetPermanent; @@ -27,21 +24,12 @@ import java.util.UUID; */ public final class RoaringEarth extends CardImpl { - private static final FilterPermanent filter = new FilterControlledPermanent("creature or Vehicle you control"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public RoaringEarth(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{G}"); // Whenever a land you control enters, put a +1/+1 counter on target creature or Vehicle you control. Ability ability = new LandfallAbility(new AddCountersTargetEffect(CounterType.P1P1.createInstance())); - ability.addTarget(new TargetPermanent(filter)); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE)); this.addAbility(ability); // Channel — {X}{G}{G}, Discard Roaring Earth; Put X +1/+1 counters on target land you control. It becomes a 0/0 green Spirit creature with haste. It's still a land. diff --git a/Mage.Sets/src/mage/cards/r/RoverBlades.java b/Mage.Sets/src/mage/cards/r/RoverBlades.java index e9aba3f9705..a47c95132c1 100644 --- a/Mage.Sets/src/mage/cards/r/RoverBlades.java +++ b/Mage.Sets/src/mage/cards/r/RoverBlades.java @@ -32,7 +32,7 @@ public final class RoverBlades extends CardImpl { // Equipped creature has double strike. this.addAbility(new SimpleStaticAbility(new GainAbilityAttachedEffect( - DoubleStrikeAbility.getInstance(), AttachmentType.AURA + DoubleStrikeAbility.getInstance(), AttachmentType.EQUIPMENT ))); // Equip {4} diff --git a/Mage.Sets/src/mage/cards/r/RubblebeltRioters.java b/Mage.Sets/src/mage/cards/r/RubblebeltRioters.java index 372ef042f9c..5639bff276a 100644 --- a/Mage.Sets/src/mage/cards/r/RubblebeltRioters.java +++ b/Mage.Sets/src/mage/cards/r/RubblebeltRioters.java @@ -34,7 +34,7 @@ public final class RubblebeltRioters extends CardImpl { this.addAbility(new AttacksTriggeredAbility(new BoostSourceEffect( GreatestPowerAmongControlledCreaturesValue.instance, StaticValue.get(0), Duration.EndOfTurn - ), false)); + ), false).addHint(GreatestPowerAmongControlledCreaturesValue.getHint())); } private RubblebeltRioters(final RubblebeltRioters card) { diff --git a/Mage.Sets/src/mage/cards/r/RunOver.java b/Mage.Sets/src/mage/cards/r/RunOver.java new file mode 100644 index 00000000000..c45b577423c --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RunOver.java @@ -0,0 +1,60 @@ +package mage.cards.r; + +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SourceTargetsPermanentCondition; +import mage.abilities.effects.common.DamageWithPowerFromOneToAnotherTargetEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.common.TargetOpponentsCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RunOver extends CardImpl { + + private static final FilterPermanent filter + = new FilterControlledPermanent("a Mount or Vehicle you control"); + + static { + filter.add(Predicates.or( + SubType.MOUNT.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + + private static final Condition condition = new SourceTargetsPermanentCondition(filter); + + public RunOver(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{G}"); + + // This spell costs {1} less to cast if it targets a Mount or Vehicle you control. + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new SpellCostReductionSourceEffect(1, condition).setCanWorksOnStackOnly(true) + ).setRuleAtTheTop(true)); + + // Target creature you control deals damage equal to its power to target creature an opponent controls. + this.getSpellAbility().addEffect(new DamageWithPowerFromOneToAnotherTargetEffect()); + this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent()); + this.getSpellAbility().addTarget(new TargetOpponentsCreaturePermanent()); + } + + private RunOver(final RunOver card) { + super(card); + } + + @Override + public RunOver copy() { + return new RunOver(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SabSunenLuxaEmbodied.java b/Mage.Sets/src/mage/cards/s/SabSunenLuxaEmbodied.java index da37bff00f9..ff9fc16cecf 100644 --- a/Mage.Sets/src/mage/cards/s/SabSunenLuxaEmbodied.java +++ b/Mage.Sets/src/mage/cards/s/SabSunenLuxaEmbodied.java @@ -54,7 +54,7 @@ public final class SabSunenLuxaEmbodied extends CardImpl { ); ability.addEffect(new ConditionalOneShotEffect( new DrawCardSourceControllerEffect(2), SabSunenLuxaEmbodiedCondition.ODD, - "then if it has an odd number of counters on it, draw two cards" + "Then if it has an odd number of counters on it, draw two cards" )); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/s/SabotageStrategist.java b/Mage.Sets/src/mage/cards/s/SabotageStrategist.java new file mode 100644 index 00000000000..43d5f1f6a0b --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SabotageStrategist.java @@ -0,0 +1,92 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.ExhaustAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.DefenderAttackedEvent; +import mage.game.events.GameEvent; +import mage.target.targetpointer.FixedTargets; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SabotageStrategist extends CardImpl { + + public SabotageStrategist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{U}"); + + this.subtype.add(SubType.VEDALKEN); + this.subtype.add(SubType.RANGER); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Whenever one or more creatures attack you, those creatures get -1/-0 until end of turn. + this.addAbility(new SabotageStrategistTriggeredAbility()); + + // Exhaust -- {5}{U}{U}: Put three +1/+1 counters on this creature. + this.addAbility(new ExhaustAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance(3)), new ManaCostsImpl<>("{5}{U}{U}") + )); + } + + private SabotageStrategist(final SabotageStrategist card) { + super(card); + } + + @Override + public SabotageStrategist copy() { + return new SabotageStrategist(this); + } +} + +class SabotageStrategistTriggeredAbility extends TriggeredAbilityImpl { + + SabotageStrategistTriggeredAbility() { + super(Zone.BATTLEFIELD, new BoostTargetEffect(-1, 0).setText("those creatures get -1/-0 until end of turn")); + this.setTriggerPhrase("Whenever one or more creatures attack you, "); + } + + private SabotageStrategistTriggeredAbility(final SabotageStrategistTriggeredAbility ability) { + super(ability); + } + + @Override + public SabotageStrategistTriggeredAbility copy() { + return new SabotageStrategistTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DEFENDER_ATTACKED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + DefenderAttackedEvent dEvent = (DefenderAttackedEvent) event; + if (!isControlledBy(dEvent.getTargetId())) { + return false; + } + this.getEffects().setTargetPointer(new FixedTargets(dEvent.getAttackers(game), game)); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/s/SamutTheDrivingForce.java b/Mage.Sets/src/mage/cards/s/SamutTheDrivingForce.java new file mode 100644 index 00000000000..23211469d08 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SamutTheDrivingForce.java @@ -0,0 +1,75 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.ControllerSpeedCount; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.cost.SpellsCostReductionControllerEffect; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.abilities.keyword.VigilanceAbility; +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.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.filter.common.FilterNoncreatureCard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SamutTheDrivingForce extends CardImpl { + + private static final FilterCard filter = new FilterNoncreatureCard("noncreature spells"); + + public SamutTheDrivingForce(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}{G}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // First strike + this.addAbility(FirstStrikeAbility.getInstance()); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // Other creatures you control get +X/+0, where X is your speed. + this.addAbility(new SimpleStaticAbility(new BoostControlledEffect( + ControllerSpeedCount.instance, StaticValue.get(0), Duration.WhileOnBattlefield, + StaticFilters.FILTER_PERMANENT_CREATURES, true + ))); + + // Noncreature spells you cast cost {X} less to cast, where X is your speed. + this.addAbility(new SimpleStaticAbility( + new SpellsCostReductionControllerEffect(filter, ControllerSpeedCount.instance) + .setText("noncreature spells you cast cost {X} less to cast, where X is your speed") + )); + } + + private SamutTheDrivingForce(final SamutTheDrivingForce card) { + super(card); + } + + @Override + public SamutTheDrivingForce copy() { + return new SamutTheDrivingForce(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/ScourTheLaboratory.java b/Mage.Sets/src/mage/cards/s/ScourTheLaboratory.java index 861e4cecf08..8736d7d06c0 100644 --- a/Mage.Sets/src/mage/cards/s/ScourTheLaboratory.java +++ b/Mage.Sets/src/mage/cards/s/ScourTheLaboratory.java @@ -4,9 +4,9 @@ import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -25,7 +25,7 @@ public final class ScourTheLaboratory extends CardImpl { Ability ability = new SimpleStaticAbility(Zone.ALL, new SpellCostReductionSourceEffect(2, DeliriumCondition.instance)); ability.setRuleAtTheTop(true); ability.setAbilityWord(AbilityWord.DELIRIUM); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); // Draw three cards. diff --git a/Mage.Sets/src/mage/cards/s/ScourgeWolf.java b/Mage.Sets/src/mage/cards/s/ScourgeWolf.java index 66ac9fd185e..16f9c5aab1c 100644 --- a/Mage.Sets/src/mage/cards/s/ScourgeWolf.java +++ b/Mage.Sets/src/mage/cards/s/ScourgeWolf.java @@ -6,8 +6,8 @@ import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.DoubleStrikeAbility; import mage.abilities.keyword.FirstStrikeAbility; import mage.cards.CardImpl; @@ -15,7 +15,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @author fireshoes @@ -35,7 +34,7 @@ public final class ScourgeWolf extends CardImpl { // Delirium — Scourge Wolf has double strike as long as there are four or more card types among cards in your graveyard. ConditionalContinuousEffect effect = new ConditionalContinuousEffect(new GainAbilitySourceEffect(DoubleStrikeAbility.getInstance(), Duration.WhileOnBattlefield), DeliriumCondition.instance, "Delirium — {this} has double strike as long as there are four or more card types among cards in your graveyard."); - this.addAbility(new SimpleStaticAbility(effect).addHint(CardTypesInGraveyardHint.YOU)); + this.addAbility(new SimpleStaticAbility(effect).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private ScourgeWolf(final ScourgeWolf card) { diff --git a/Mage.Sets/src/mage/cards/s/ScrapCompactor.java b/Mage.Sets/src/mage/cards/s/ScrapCompactor.java new file mode 100644 index 00000000000..509b81b69fa --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/ScrapCompactor.java @@ -0,0 +1,52 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ScrapCompactor extends CardImpl { + + public ScrapCompactor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}"); + + // {3}, {T}, Sacrifice this artifact: It deals 3 damage to target creature. + Ability ability = new SimpleActivatedAbility( + new DamageTargetEffect(3, "it"), new GenericManaCost(3) + ); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeSourceCost()); + ability.addTarget(new TargetCreaturePermanent()); + this.addAbility(ability); + + // {6}, {T}, Sacrifice this artifact: Destroy target creature or Vehicle. + ability = new SimpleActivatedAbility(new DestroyTargetEffect(), new GenericManaCost(6)); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeSourceCost()); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); + this.addAbility(ability); + } + + private ScrapCompactor(final ScrapCompactor card) { + super(card); + } + + @Override + public ScrapCompactor copy() { + return new ScrapCompactor(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/ScroungingSkyray.java b/Mage.Sets/src/mage/cards/s/ScroungingSkyray.java new file mode 100644 index 00000000000..c459cc7a81a --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/ScroungingSkyray.java @@ -0,0 +1,51 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.SavedDiscardValue; +import mage.abilities.effects.common.DiscardOneOrMoreCardsTriggeredAbility; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.CyclingAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ScroungingSkyray extends CardImpl { + + public ScroungingSkyray(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.FISH); + this.subtype.add(SubType.PIRATE); + this.power = new MageInt(1); + this.toughness = new MageInt(2); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever you discard one or more cards, put that many +1/+1 counters on this creature. + this.addAbility(new DiscardOneOrMoreCardsTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance(0), SavedDiscardValue.MANY) + )); + + // Cycling {2} + this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{2}"))); + } + + private ScroungingSkyray(final ScroungingSkyray card) { + super(card); + } + + @Override + public ScroungingSkyray copy() { + return new ScroungingSkyray(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/Scuttletide.java b/Mage.Sets/src/mage/cards/s/Scuttletide.java index 42263d3e348..28cb2724b06 100644 --- a/Mage.Sets/src/mage/cards/s/Scuttletide.java +++ b/Mage.Sets/src/mage/cards/s/Scuttletide.java @@ -7,9 +7,9 @@ import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.costs.common.DiscardCardCost; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.continuous.BoostControlledEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; @@ -42,7 +42,7 @@ public final class Scuttletide extends CardImpl { 1, 1, Duration.WhileOnBattlefield, filter, false ), DeliriumCondition.instance, "Crabs you control get +1/+1 as long " + "as there are four or more card types among cards in your graveyard" - )).addHint(CardTypesInGraveyardHint.YOU).setAbilityWord(AbilityWord.DELIRIUM)); + )).addHint(CardTypesInGraveyardCount.YOU.getHint()).setAbilityWord(AbilityWord.DELIRIUM)); } private Scuttletide(final Scuttletide card) { diff --git a/Mage.Sets/src/mage/cards/s/SeasonOfGathering.java b/Mage.Sets/src/mage/cards/s/SeasonOfGathering.java index 628ee0dad99..5c6e94ba823 100644 --- a/Mage.Sets/src/mage/cards/s/SeasonOfGathering.java +++ b/Mage.Sets/src/mage/cards/s/SeasonOfGathering.java @@ -56,6 +56,7 @@ public final class SeasonOfGathering extends CardImpl { .setText("Draw cards equal to the greatest power among creatures you control.") ); this.getSpellAbility().addMode(mode3.withPawPrintValue(3)); + this.getSpellAbility().addHint(GreatestPowerAmongControlledCreaturesValue.getHint()); } private SeasonOfGathering(final SeasonOfGathering card) { diff --git a/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java b/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java index 6514a80f92e..6acdd4fcd86 100644 --- a/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java +++ b/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java @@ -45,6 +45,7 @@ public final class SelvalaHeartOfTheWilds extends CardImpl { ColoredManaSymbol.W, ColoredManaSymbol.U, ColoredManaSymbol.B, ColoredManaSymbol.R, ColoredManaSymbol.G); Ability ability = new SimpleManaAbility(Zone.BATTLEFIELD, manaEffect, new ManaCostsImpl<>("{G}")); ability.addCost(new TapSourceCost()); + ability.addHint(GreatestPowerAmongControlledCreaturesValue.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/s/SetAdrift.java b/Mage.Sets/src/mage/cards/s/SetAdrift.java index 1451c5da27c..e7f9840916a 100644 --- a/Mage.Sets/src/mage/cards/s/SetAdrift.java +++ b/Mage.Sets/src/mage/cards/s/SetAdrift.java @@ -20,7 +20,8 @@ public final class SetAdrift extends CardImpl { // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); + // Put target nonland permanent on top of its owner's library this.getSpellAbility().addEffect(new PutOnLibraryTargetEffect(true)); this.getSpellAbility().addTarget(new TargetNonlandPermanent()); diff --git a/Mage.Sets/src/mage/cards/s/SevenTailMentor.java b/Mage.Sets/src/mage/cards/s/SevenTailMentor.java index 36bddfaa18e..692a682e134 100644 --- a/Mage.Sets/src/mage/cards/s/SevenTailMentor.java +++ b/Mage.Sets/src/mage/cards/s/SevenTailMentor.java @@ -9,9 +9,7 @@ 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.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import java.util.UUID; @@ -21,15 +19,6 @@ import java.util.UUID; */ public final class SevenTailMentor extends CardImpl { - private static final FilterPermanent filter = new FilterControlledPermanent("creature or Vehicle you control"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public SevenTailMentor(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); @@ -42,7 +31,7 @@ public final class SevenTailMentor extends CardImpl { Ability ability = new EntersBattlefieldOrDiesSourceTriggeredAbility( new AddCountersTargetEffect(CounterType.P1P1.createInstance()), false ); - ability.addTarget(new TargetPermanent(filter)); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE)); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/s/ShadowKin.java b/Mage.Sets/src/mage/cards/s/ShadowKin.java index 6109aa5006b..79ab66772b1 100644 --- a/Mage.Sets/src/mage/cards/s/ShadowKin.java +++ b/Mage.Sets/src/mage/cards/s/ShadowKin.java @@ -15,6 +15,7 @@ import mage.game.permanent.Permanent; import mage.game.permanent.PermanentCard; import mage.players.Player; import mage.target.common.TargetCardInGraveyard; +import mage.util.CardUtil; import mage.util.functions.CopyApplier; import java.util.UUID; @@ -94,7 +95,7 @@ class ShadowKinEffect extends OneShotEffect { return true; } controller.moveCards(card, Zone.EXILED, source, game); - Permanent blueprint = new PermanentCard(card, source.getControllerId(), game); + Permanent blueprint = new PermanentCard(CardUtil.getDefaultCardSideForBattlefield(game, card), source.getControllerId(), game); blueprint.assignNewId(); CopyApplier applier = new ShadowKinApplier(); applier.apply(game, blueprint, source, sourcePermanent.getId()); diff --git a/Mage.Sets/src/mage/cards/s/ShamblingAttendants.java b/Mage.Sets/src/mage/cards/s/ShamblingAttendants.java index 561f29ab882..7e8c5a9c44b 100644 --- a/Mage.Sets/src/mage/cards/s/ShamblingAttendants.java +++ b/Mage.Sets/src/mage/cards/s/ShamblingAttendants.java @@ -24,7 +24,8 @@ public final class ShamblingAttendants extends CardImpl { this.toughness = new MageInt(5); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); + // Deathtouch this.addAbility(DeathtouchAbility.getInstance()); } diff --git a/Mage.Sets/src/mage/cards/s/ShelldockIsle.java b/Mage.Sets/src/mage/cards/s/ShelldockIsle.java index 1feb7b775ed..c451b5de043 100644 --- a/Mage.Sets/src/mage/cards/s/ShelldockIsle.java +++ b/Mage.Sets/src/mage/cards/s/ShelldockIsle.java @@ -29,7 +29,7 @@ public final class ShelldockIsle extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // Hideaway - this.addAbility(new HideawayAbility(4)); + this.addAbility(new HideawayAbility(this, 4)); this.addAbility(new EntersBattlefieldTappedAbility()); // {tap}: Add {U}. diff --git a/Mage.Sets/src/mage/cards/s/ShiftingWoodland.java b/Mage.Sets/src/mage/cards/s/ShiftingWoodland.java index 3f4edcdcdda..b38ec519e59 100644 --- a/Mage.Sets/src/mage/cards/s/ShiftingWoodland.java +++ b/Mage.Sets/src/mage/cards/s/ShiftingWoodland.java @@ -6,12 +6,13 @@ import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.condition.common.YouControlPermanentCondition; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalActivatedAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.OneShotEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.mana.GreenManaAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.cards.ModalDoubleFacedCard; import mage.constants.*; import mage.filter.FilterCard; import mage.filter.FilterPermanent; @@ -21,6 +22,7 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.permanent.PermanentCard; import mage.target.common.TargetCardInYourGraveyard; +import mage.util.CardUtil; import mage.util.functions.EmptyCopyApplier; import java.util.UUID; @@ -52,7 +54,7 @@ public final class ShiftingWoodland extends CardImpl { ); ability.addTarget(new TargetCardInYourGraveyard(1, filterCard)); ability.setAbilityWord(AbilityWord.DELIRIUM); - this.addAbility(ability.addHint(CardTypesInGraveyardHint.YOU)); + this.addAbility(ability.addHint(CardTypesInGraveyardCount.YOU.getHint())); } private ShiftingWoodland(final ShiftingWoodland card) { @@ -91,7 +93,7 @@ class ShiftingWoodlandCopyEffect extends OneShotEffect { if (copyFromCard == null) { return false; } - Permanent blueprint = new PermanentCard(copyFromCard, source.getControllerId(), game); + Permanent blueprint = new PermanentCard(CardUtil.getDefaultCardSideForBattlefield(game, copyFromCard), source.getControllerId(), game); game.copyPermanent(Duration.EndOfTurn, blueprint, sourcePermanent.getId(), source, new EmptyCopyApplier()); return true; } diff --git a/Mage.Sets/src/mage/cards/s/SibsigMuckdraggers.java b/Mage.Sets/src/mage/cards/s/SibsigMuckdraggers.java index 28c896aedc4..f544bdd87f6 100644 --- a/Mage.Sets/src/mage/cards/s/SibsigMuckdraggers.java +++ b/Mage.Sets/src/mage/cards/s/SibsigMuckdraggers.java @@ -28,7 +28,8 @@ public final class SibsigMuckdraggers extends CardImpl { this.toughness = new MageInt(6); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); + // When Sibsig Muckdraggers enters the battlefield, return target creature card from your graveyard to your hand. Ability ability = new EntersBattlefieldTriggeredAbility(new ReturnFromGraveyardToHandTargetEffect(), false); ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); diff --git a/Mage.Sets/src/mage/cards/s/SiegeModification.java b/Mage.Sets/src/mage/cards/s/SiegeModification.java index 484f5a87b8e..b455c62ee52 100644 --- a/Mage.Sets/src/mage/cards/s/SiegeModification.java +++ b/Mage.Sets/src/mage/cards/s/SiegeModification.java @@ -1,10 +1,8 @@ package mage.cards.s; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.Effect; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.continuous.BecomesCreatureIfVehicleEffect; import mage.abilities.effects.common.continuous.BoostEnchantedEffect; @@ -13,45 +11,39 @@ import mage.abilities.keyword.EnchantAbility; import mage.abilities.keyword.FirstStrikeAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.constants.AttachmentType; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; +import java.util.UUID; + /** * @author JRHerlehy */ public final class SiegeModification extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or(CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate())); - } - public SiegeModification(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}{R}"); this.subtype.add(SubType.AURA); // Enchant creature or Vehicle - TargetPermanent auraTarget = new TargetPermanent(filter); + TargetPermanent auraTarget = new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE); this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addEffect(new AttachEffect(Outcome.Benefit)); - Ability ability = new EnchantAbility(auraTarget); - this.addAbility(ability); + this.addAbility(new EnchantAbility(auraTarget)); // As long as enchanted permanent is a Vehicle, it's a creature in addition to its other types. this.addAbility(new SimpleStaticAbility(new BecomesCreatureIfVehicleEffect())); // Enchanted creature gets +3/+0 and has first strike. - Effect effect = new BoostEnchantedEffect(3, 0); - effect.setText("Enchanted creature gets +3/+0"); - ability = new SimpleStaticAbility(effect); - effect = new GainAbilityAttachedEffect(FirstStrikeAbility.getInstance(), AttachmentType.AURA); - effect.setText(" and has first strike"); - ability.addEffect(effect); + Ability ability = new SimpleStaticAbility(new BoostEnchantedEffect(3, 0)); + ability.addEffect(new GainAbilityAttachedEffect( + FirstStrikeAbility.getInstance(), AttachmentType.AURA + ).setText(" and has first strike")); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/s/SilkenStrength.java b/Mage.Sets/src/mage/cards/s/SilkenStrength.java index 617f66d152a..09c97459173 100644 --- a/Mage.Sets/src/mage/cards/s/SilkenStrength.java +++ b/Mage.Sets/src/mage/cards/s/SilkenStrength.java @@ -16,8 +16,7 @@ import mage.constants.AttachmentType; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import java.util.UUID; @@ -27,15 +26,6 @@ import java.util.UUID; */ public final class SilkenStrength extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public SilkenStrength(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{G}"); @@ -45,7 +35,7 @@ public final class SilkenStrength extends CardImpl { this.addAbility(FlashAbility.getInstance()); // Enchant creature or Vehicle - TargetPermanent auraTarget = new TargetPermanent(filter); + TargetPermanent auraTarget = new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE); this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); this.addAbility(new EnchantAbility(auraTarget)); @@ -57,7 +47,7 @@ public final class SilkenStrength extends CardImpl { // Enchanted permanent gets +1/+2 and has reach. Ability ability = new SimpleStaticAbility(new BoostEnchantedEffect(1, 2) - .setText("enchanted permanent has +1/+2")); + .setText("enchanted permanent gets +1/+2")); ability.addEffect(new GainAbilityAttachedEffect( ReachAbility.getInstance(), AttachmentType.AURA ).setText("and has reach")); diff --git a/Mage.Sets/src/mage/cards/s/SinisterConcierge.java b/Mage.Sets/src/mage/cards/s/SinisterConcierge.java index 8d52de34b6d..294258a3ec4 100644 --- a/Mage.Sets/src/mage/cards/s/SinisterConcierge.java +++ b/Mage.Sets/src/mage/cards/s/SinisterConcierge.java @@ -4,7 +4,8 @@ import mage.MageInt; import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.common.DiesSourceTriggeredAbility; -import mage.abilities.costs.common.ExileSourceFromGraveCost; +import mage.abilities.costs.Cost; +import mage.abilities.costs.common.ExileSourceWithTimeCountersCost; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; @@ -45,12 +46,15 @@ public class SinisterConcierge extends CardImpl { // If you do, exile up to one target creature and put three time counters on it. // Each card exiled this way that doesn't have suspend gains suspend. // (For each card with suspend, its owner removes a time counter from it at the beginning of their upkeep. - // When the last is removed, they cast it without paying its mana cost. Those creature spells have haste.) - Ability ability = new DiesSourceTriggeredAbility( + // When the last is removed, they may cast it without paying its mana cost. Those creature spells have haste.) + Cost cost = new ExileSourceWithTimeCountersCost(3, false, true, Zone.GRAVEYARD); + // Paying the cost sends the Concierge to the right exile zone (Suspended cards of…) and gives it suspend. + cost.setText("exile it and put three time counters on it"); + Ability ability = new DiesSourceTriggeredAbility( new DoIfCostPaid( new SinisterConciergeEffect(), - new ExileSourceFromGraveCost() - ).setText("you may exile it and put three time counters on it") + cost + ) ); ability.addTarget(new TargetCreaturePermanent(0, 1)); this.addAbility(ability); @@ -69,11 +73,10 @@ public class SinisterConcierge extends CardImpl { class SinisterConciergeEffect extends OneShotEffect { public SinisterConciergeEffect() { super(Outcome.Removal); - this.staticText = "you may exile it and put three time counters on it. " + - "If you do, exile up to one target creature and put three time counters on it. " + + this.staticText = "exile up to one target creature and put three time counters on it. " + "Each card exiled this way that doesn't have suspend gains suspend. " + "(For each card with suspend, its owner removes a time counter from it at the beginning of their upkeep. " + - "When the last is removed, they cast it without paying its mana cost. Those creature spells have haste.)"; + "When the last is removed, they may cast it without paying its mana cost. Those creature spells have haste.)"; } private SinisterConciergeEffect(final SinisterConciergeEffect effect) { @@ -88,23 +91,15 @@ class SinisterConciergeEffect extends OneShotEffect { return false; } - // Put the time counters on the Sinister Concierge and give it Suspend - if (game.getState().getZone(card.getId()) == Zone.EXILED) { - Effect addCountersSourceEffect = new AddCountersSourceEffect(CounterType.TIME.createInstance(), StaticValue.get(3), false ,true); - boolean sourceCardShouldGetSuspend = addCountersSourceEffect.apply(game, source); - - if (sourceCardShouldGetSuspend && !card.getAbilities(game).containsClass(SuspendAbility.class)) { - game.addEffect(new GainSuspendEffect(new MageObjectReference(card, game)), source); - } - } - Permanent targetCreature = game.getPermanent(this.getTargetPointer().getFirst(game, source)); if (targetCreature == null){ return false; } // Exile, put time counters, and give suspend for target - Effect exileTarget = new ExileTargetEffect(); + Player controllerTarget = game.getPlayer(targetCreature.getControllerId()); + UUID exileId = SuspendAbility.getSuspendExileId(controllerTarget.getId(), game); + Effect exileTarget = new ExileTargetEffect(exileId, "Suspended cards of " + controllerTarget.getName()); exileTarget.setTargetPointer(this.getTargetPointer().copy()); if (exileTarget.apply(game, source)) { Effect addCountersTargetEffect = new AddCountersTargetEffect(CounterType.TIME.createInstance(3)); diff --git a/Mage.Sets/src/mage/cards/s/SlickImitator.java b/Mage.Sets/src/mage/cards/s/SlickImitator.java new file mode 100644 index 00000000000..3766f26ee29 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SlickImitator.java @@ -0,0 +1,57 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.CopyTargetStackObjectEffect; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.filter.FilterSpell; +import mage.target.TargetSpell; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SlickImitator extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("spell you control"); + + static { + filter.add(TargetController.YOU.getControllerPredicate()); + } + + public SlickImitator(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.OOZE); + this.power = new MageInt(1); + this.toughness = new MageInt(3); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // Max speed -- {1}, Sacrifice this creature: Copy target spell you control. You may choose new targets for the copy. + Ability ability = new SimpleActivatedAbility(new CopyTargetStackObjectEffect(), new GenericManaCost(1)); + ability.addCost(new SacrificeSourceCost()); + ability.addTarget(new TargetSpell(filter)); + this.addAbility(new MaxSpeedAbility(ability)); + } + + private SlickImitator(final SlickImitator card) { + super(card); + } + + @Override + public SlickImitator copy() { + return new SlickImitator(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SmugglersBuggy.java b/Mage.Sets/src/mage/cards/s/SmugglersBuggy.java index 7b0c86d4b40..73f3d113536 100644 --- a/Mage.Sets/src/mage/cards/s/SmugglersBuggy.java +++ b/Mage.Sets/src/mage/cards/s/SmugglersBuggy.java @@ -35,7 +35,7 @@ public class SmugglersBuggy extends CardImpl { // Hideaway 4 // (When this artifact enters the battlefield, look at the top four cards of your library, // exile one face down, then put the rest on the bottom in a random order.) - this.addAbility(new HideawayAbility(4)); + this.addAbility(new HideawayAbility(this, 4)); // Whenever Smuggler’s Buggy deals combat damage to a player, you may cast the exiled card without paying its mana cost. // If you do, return Smuggler’s Buggy to its owner’s hand. diff --git a/Mage.Sets/src/mage/cards/s/SongOfStupefaction.java b/Mage.Sets/src/mage/cards/s/SongOfStupefaction.java index 13779edd11b..afb7cf4a0d3 100644 --- a/Mage.Sets/src/mage/cards/s/SongOfStupefaction.java +++ b/Mage.Sets/src/mage/cards/s/SongOfStupefaction.java @@ -16,9 +16,7 @@ import mage.constants.AbilityWord; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; -import mage.filter.FilterPermanent; import mage.filter.StaticFilters; -import mage.filter.predicate.Predicates; import mage.target.TargetPermanent; import java.util.UUID; @@ -28,15 +26,6 @@ import java.util.UUID; */ public final class SongOfStupefaction extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - private static final DynamicValue xValue = new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_PERMANENT, -1); public SongOfStupefaction(UUID ownerId, CardSetInfo setInfo) { @@ -45,7 +34,7 @@ public final class SongOfStupefaction extends CardImpl { this.subtype.add(SubType.AURA); // Enchant creature or Vehicle - TargetPermanent auraTarget = new TargetPermanent(filter); + TargetPermanent auraTarget = new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE); this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); this.addAbility(new EnchantAbility(auraTarget)); diff --git a/Mage.Sets/src/mage/cards/s/SorcerousSquall.java b/Mage.Sets/src/mage/cards/s/SorcerousSquall.java index c998a191c88..9b99a1a1994 100644 --- a/Mage.Sets/src/mage/cards/s/SorcerousSquall.java +++ b/Mage.Sets/src/mage/cards/s/SorcerousSquall.java @@ -33,7 +33,7 @@ public final class SorcerousSquall extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{6}{U}{U}{U}"); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // Target opponent mills nine cards, then you may cast an instant or sorcery spell from that player's graveyard without paying its mana cost. If that spell would be put into a graveyard, exile it instead. this.getSpellAbility().addEffect(new MillCardsTargetEffect(9)); diff --git a/Mage.Sets/src/mage/cards/s/SoulSwallower.java b/Mage.Sets/src/mage/cards/s/SoulSwallower.java index 355a2b3c58b..53a2b1ca105 100644 --- a/Mage.Sets/src/mage/cards/s/SoulSwallower.java +++ b/Mage.Sets/src/mage/cards/s/SoulSwallower.java @@ -3,11 +3,11 @@ package mage.cards.s; import java.util.UUID; import mage.MageInt; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.counter.AddCountersSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -35,7 +35,7 @@ public final class SoulSwallower extends CardImpl { DeliriumCondition.instance, "Delirium — At the beginning of your upkeep, if there are four or more card types among cards in your graveyard, " + "put three +1/+1 counters on Soul Swallower.") - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); } private SoulSwallower(final SoulSwallower card) { diff --git a/Mage.Sets/src/mage/cards/s/Soulflayer.java b/Mage.Sets/src/mage/cards/s/Soulflayer.java index 269177ae832..60babb63a59 100644 --- a/Mage.Sets/src/mage/cards/s/Soulflayer.java +++ b/Mage.Sets/src/mage/cards/s/Soulflayer.java @@ -31,7 +31,7 @@ public final class Soulflayer extends CardImpl { this.toughness = new MageInt(4); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(true)); // If a creature card with flying was exiled with Soulflayer's delve ability, Soulflayer has flying. The same is true for first strike, double strike, deathtouch, haste, hexproof, indestructible, lifelink, reach, trample, and vigilance. this.addAbility(new SimpleStaticAbility(new SoulflayerEffect())); diff --git a/Mage.Sets/src/mage/cards/s/SoulsOfTheLost.java b/Mage.Sets/src/mage/cards/s/SoulsOfTheLost.java index d685d5fd648..496e46e7a6e 100644 --- a/Mage.Sets/src/mage/cards/s/SoulsOfTheLost.java +++ b/Mage.Sets/src/mage/cards/s/SoulsOfTheLost.java @@ -41,8 +41,9 @@ public final class SoulsOfTheLost extends CardImpl { )); // Fathomless descent -- Souls of the Lost's power is equal to the number of permanent cards in your graveyard and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(xValue)) - .setAbilityWord(AbilityWord.FATHOMLESS_DESCENT).addHint(DescendCondition.getHint())); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(xValue) + ).setAbilityWord(AbilityWord.FATHOMLESS_DESCENT).addHint(DescendCondition.getHint())); } private SoulsOfTheLost(final SoulsOfTheLost card) { diff --git a/Mage.Sets/src/mage/cards/s/SpikeshellHarrier.java b/Mage.Sets/src/mage/cards/s/SpikeshellHarrier.java new file mode 100644 index 00000000000..0cee12c0bb2 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SpikeshellHarrier.java @@ -0,0 +1,101 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SpikeshellHarrier extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle an opponent controls"); + + static { + filter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + filter.add(TargetController.OPPONENT.getControllerPredicate()); + } + + public SpikeshellHarrier(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{4}{U}"); + + this.subtype.add(SubType.ROBOT); + this.subtype.add(SubType.TURTLE); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // When this creature enters, return target creature or Vehicle an opponent controls to its owner's hand. If that opponent's speed is greater than each other player's speed, reduce that opponent's speed by 1. This effect can't reduce their speed below 1. + Ability ability = new EntersBattlefieldTriggeredAbility(new SpikeshellHarrierEffect()); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + } + + private SpikeshellHarrier(final SpikeshellHarrier card) { + super(card); + } + + @Override + public SpikeshellHarrier copy() { + return new SpikeshellHarrier(this); + } +} + +class SpikeshellHarrierEffect extends OneShotEffect { + + SpikeshellHarrierEffect() { + super(Outcome.Benefit); + staticText = "return target creature or Vehicle an opponent controls to its owner's hand. " + + "If that opponent's speed is greater than each other player's speed, " + + "reduce that opponent's speed by 1. This effect can't reduce their speed below 1"; + } + + private SpikeshellHarrierEffect(final SpikeshellHarrierEffect effect) { + super(effect); + } + + @Override + public SpikeshellHarrierEffect copy() { + return new SpikeshellHarrierEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + Player player = game.getPlayer(permanent.getControllerId()); + if (player == null) { + return false; + } + player.moveCards(permanent, Zone.HAND, source, game); + int minSpeed = game + .getState() + .getPlayersInRange(source.getControllerId(), game) + .stream() + .filter(uuid -> !uuid.equals(player.getId())) + .map(game::getPlayer) + .mapToInt(Player::getSpeed) + .min() + .orElse(0); + if (player.getSpeed() > minSpeed) { + player.decreaseSpeed(game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/s/SpinOut.java b/Mage.Sets/src/mage/cards/s/SpinOut.java index 11350583803..a2381336389 100644 --- a/Mage.Sets/src/mage/cards/s/SpinOut.java +++ b/Mage.Sets/src/mage/cards/s/SpinOut.java @@ -4,9 +4,7 @@ import mage.abilities.effects.common.DestroyTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import java.util.UUID; @@ -16,21 +14,12 @@ import java.util.UUID; */ public final class SpinOut extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public SpinOut(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{B}{B}"); // Destroy target creature or Vehicle. this.getSpellAbility().addEffect(new DestroyTargetEffect()); - this.getSpellAbility().addTarget(new TargetPermanent(filter)); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); } private SpinOut(final SpinOut card) { diff --git a/Mage.Sets/src/mage/cards/s/SpinerockKnoll.java b/Mage.Sets/src/mage/cards/s/SpinerockKnoll.java index d8bdf71aedb..c4ec14b8c23 100644 --- a/Mage.Sets/src/mage/cards/s/SpinerockKnoll.java +++ b/Mage.Sets/src/mage/cards/s/SpinerockKnoll.java @@ -32,7 +32,7 @@ public final class SpinerockKnoll extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // Hideaway - this.addAbility(new HideawayAbility(4)); + this.addAbility(new HideawayAbility(this, 4)); this.addAbility(new EntersBattlefieldTappedAbility()); // {tap}: Add {R}. diff --git a/Mage.Sets/src/mage/cards/s/SpineseekerCentipede.java b/Mage.Sets/src/mage/cards/s/SpineseekerCentipede.java index 24bfe7bb6fe..c3a0b467236 100644 --- a/Mage.Sets/src/mage/cards/s/SpineseekerCentipede.java +++ b/Mage.Sets/src/mage/cards/s/SpineseekerCentipede.java @@ -6,10 +6,10 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.VigilanceAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -48,7 +48,7 @@ public final class SpineseekerCentipede extends CardImpl { new GainAbilitySourceEffect(VigilanceAbility.getInstance()), DeliriumCondition.instance, "and has vigilance as long as there are four or more card types among cards in your graveyard" )); - this.addAbility(ability.addHint(CardTypesInGraveyardHint.YOU).setAbilityWord(AbilityWord.DELIRIUM)); + this.addAbility(ability.addHint(CardTypesInGraveyardCount.YOU.getHint()).setAbilityWord(AbilityWord.DELIRIUM)); } private SpineseekerCentipede(final SpineseekerCentipede card) { diff --git a/Mage.Sets/src/mage/cards/s/StallOut.java b/Mage.Sets/src/mage/cards/s/StallOut.java new file mode 100644 index 00000000000..bb99762dbd0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StallOut.java @@ -0,0 +1,42 @@ +package mage.cards.s; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.TapTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.CyclingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StallOut extends CardImpl { + + public StallOut(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{U}"); + + // Tap target creature or Vehicle, then put three stun counters on it. + this.getSpellAbility().addEffect(new TapTargetEffect()); + this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.STUN.createInstance(3)) + .setText(", then put three stun counters on it")); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); + + // Cycling {2} + this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{2}"))); + } + + private StallOut(final StallOut card) { + super(card); + } + + @Override + public StallOut copy() { + return new StallOut(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/StallionOfAshmouth.java b/Mage.Sets/src/mage/cards/s/StallionOfAshmouth.java index 4edc1928659..4caf6afde6e 100644 --- a/Mage.Sets/src/mage/cards/s/StallionOfAshmouth.java +++ b/Mage.Sets/src/mage/cards/s/StallionOfAshmouth.java @@ -4,8 +4,8 @@ import mage.MageInt; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalActivatedAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -30,7 +30,7 @@ public final class StallionOfAshmouth extends CardImpl { Zone.BATTLEFIELD, new BoostSourceEffect(1, 1, Duration.EndOfTurn), new ManaCostsImpl<>("{1}{B}"), DeliriumCondition.instance - ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private StallionOfAshmouth(final StallionOfAshmouth card) { diff --git a/Mage.Sets/src/mage/cards/s/StormFleetSpy.java b/Mage.Sets/src/mage/cards/s/StormFleetSpy.java index bf36f77b9be..cf37d6515c4 100644 --- a/Mage.Sets/src/mage/cards/s/StormFleetSpy.java +++ b/Mage.Sets/src/mage/cards/s/StormFleetSpy.java @@ -29,11 +29,11 @@ public final class StormFleetSpy extends CardImpl { this.power = new MageInt(2); this.toughness = new MageInt(2); - // Raid — When Storm Fleet Spy enters the battlefield, if you attacked this turn, draw a card. + // Raid — When this creature enters, if you attacked this turn, draw a card. Ability ability = new ConditionalInterveningIfTriggeredAbility( new EntersBattlefieldTriggeredAbility(new DrawCardSourceControllerEffect(1)), RaidCondition.instance, - "When {this} enters, if you attacked this turn, draw a card."); + "When this creature enters, if you attacked this turn, draw a card."); ability.setAbilityWord(AbilityWord.RAID); ability.addHint(RaidHint.instance); this.addAbility(ability, new PlayerAttackedWatcher()); diff --git a/Mage.Sets/src/mage/cards/s/StrangeAugmentation.java b/Mage.Sets/src/mage/cards/s/StrangeAugmentation.java index f4bc4494ea1..1fee8ac212a 100644 --- a/Mage.Sets/src/mage/cards/s/StrangeAugmentation.java +++ b/Mage.Sets/src/mage/cards/s/StrangeAugmentation.java @@ -6,16 +6,15 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.continuous.BoostEnchantedEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Outcome; -import mage.constants.Zone; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; @@ -41,7 +40,7 @@ public final class StrangeAugmentation extends CardImpl { // Delirium &mdash Enchanted creature gets an additional +2/+2 as long as there are four or more card types among cards in your graveyard. this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect(new BoostEnchantedEffect(2, 2), DeliriumCondition.instance, "Delirium — Enchanted creature gets an additional +2/+2 as long as there are four or more card types among cards in your graveyard.")) - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); } private StrangeAugmentation(final StrangeAugmentation card) { diff --git a/Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java b/Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java new file mode 100644 index 00000000000..1599ab4bb2c --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java @@ -0,0 +1,156 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.condition.common.ModeChoiceSourceCondition; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +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.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.ControllerIdPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +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 JayDi85 + */ +public final class StruggleForProjectPurity extends CardImpl { + + private static final String ruleTrigger1 = "&bull Brotherhood — At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way."; + private static final String ruleTrigger2 = "&bull Enclave — Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters."; + + public StruggleForProjectPurity(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{U}"); + + // As Struggle for Project Purity enters, choose Brotherhood or Enclave. + this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Brotherhood or Enclave?", "Brotherhood", "Enclave"), null, + "As {this} enters, choose Brotherhood or Enclave.", "")); + + // * Brotherhood - At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way. + Ability ability = new ConditionalTriggeredAbility( + new BeginningOfUpkeepTriggeredAbility(new StruggleForProjectDrawEffect()), + new ModeChoiceSourceCondition("Brotherhood"), + ruleTrigger1); + this.addAbility(ability); + + // * Enclave - Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters. + ability = new ConditionalTriggeredAbility( + new StruggleForProjectRadCountersTriggeredAbility(), + new ModeChoiceSourceCondition("Enclave"), + ruleTrigger2); + this.addAbility(ability); + } + + private StruggleForProjectPurity(final StruggleForProjectPurity card) { + super(card); + } + + @Override + public StruggleForProjectPurity copy() { + return new StruggleForProjectPurity(this); + } +} + +class StruggleForProjectDrawEffect extends OneShotEffect { + + StruggleForProjectDrawEffect() { + super(Outcome.DrawCard); + this.staticText = "Each opponent draws a card. You draw a card for each card drawn this way."; + } + + private StruggleForProjectDrawEffect(final StruggleForProjectDrawEffect effect) { + super(effect); + } + + @Override + public StruggleForProjectDrawEffect copy() { + return new StruggleForProjectDrawEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + int count = game + .getOpponents(source.getControllerId(), true) + .stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .mapToInt(player -> player.drawCards(1, source, game)) + .sum(); + if (count > 0) { + controller.drawCards(count, source, game); + return true; + } + return false; + } +} + +class StruggleForProjectRadCountersTriggeredAbility extends TriggeredAbilityImpl { + + public StruggleForProjectRadCountersTriggeredAbility() { + super(Zone.BATTLEFIELD, null); + setTriggerPhrase("Whenever a player attacks you with one or more creatures, "); + } + + private StruggleForProjectRadCountersTriggeredAbility(final StruggleForProjectRadCountersTriggeredAbility ability) { + super(ability); + } + + @Override + public StruggleForProjectRadCountersTriggeredAbility copy() { + return new StruggleForProjectRadCountersTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DECLARED_ATTACKERS; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Player attackingPlayer = game.getPlayer(event.getPlayerId()); + if (attackingPlayer == null) { + return false; + } + + Set attackersOnYou = game.getCombat().getGroups().stream() + .filter(g -> Objects.equals(g.getDefenderId(), getControllerId())) + .flatMap(g -> g.getAttackers().stream()) + .collect(Collectors.toSet()); + if (attackersOnYou.isEmpty()) { + return false; + } + + this.getEffects().clear(); + FilterCreaturePermanent filter = new FilterCreaturePermanent(); + filter.add(new ControllerIdPredicate(event.getPlayerId())); + Effect effect = new AddCountersTargetEffect( + CounterType.RAD.createInstance(), + StaticValue.get(attackersOnYou.size() * 2) + ); + effect.setTargetPointer(new FixedTarget(attackingPlayer.getId())); + this.getEffects().add(effect); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/s/SuitUp.java b/Mage.Sets/src/mage/cards/s/SuitUp.java index f9a805969b4..1730816d360 100644 --- a/Mage.Sets/src/mage/cards/s/SuitUp.java +++ b/Mage.Sets/src/mage/cards/s/SuitUp.java @@ -7,9 +7,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; -import mage.constants.SubType; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import java.util.UUID; @@ -19,15 +17,6 @@ import java.util.UUID; */ public final class SuitUp extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public SuitUp(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{U}"); @@ -38,7 +27,7 @@ public final class SuitUp extends CardImpl { this.getSpellAbility().addEffect(new SetBasePowerToughnessTargetEffect( 4, 5, Duration.EndOfTurn ).setText("with base power and toughness 4/5")); - this.getSpellAbility().addTarget(new TargetPermanent(filter)); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE)); // Draw a card. this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1).concatBy("
")); diff --git a/Mage.Sets/src/mage/cards/s/SultaiScavenger.java b/Mage.Sets/src/mage/cards/s/SultaiScavenger.java index c3f133be5b7..6fe4bcf18a2 100644 --- a/Mage.Sets/src/mage/cards/s/SultaiScavenger.java +++ b/Mage.Sets/src/mage/cards/s/SultaiScavenger.java @@ -25,7 +25,8 @@ public final class SultaiScavenger extends CardImpl { this.toughness = new MageInt(3); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); + // Flying this.addAbility(FlyingAbility.getInstance()); } diff --git a/Mage.Sets/src/mage/cards/s/SwiftReconfiguration.java b/Mage.Sets/src/mage/cards/s/SwiftReconfiguration.java index 1791aaaf9c6..c54818a1bfc 100644 --- a/Mage.Sets/src/mage/cards/s/SwiftReconfiguration.java +++ b/Mage.Sets/src/mage/cards/s/SwiftReconfiguration.java @@ -10,8 +10,7 @@ import mage.abilities.keyword.FlashAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; -import mage.filter.FilterPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; @@ -23,15 +22,6 @@ import java.util.UUID; */ public final class SwiftReconfiguration extends CardImpl { - private static final FilterPermanent filter = new FilterPermanent("creature or Vehicle"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public SwiftReconfiguration(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{W}"); @@ -41,7 +31,7 @@ public final class SwiftReconfiguration extends CardImpl { this.addAbility(FlashAbility.getInstance()); // Enchant creature or Vehicle - TargetPermanent auraTarget = new TargetPermanent(filter); + TargetPermanent auraTarget = new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE_OR_VEHICLE); this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); this.addAbility(new EnchantAbility(auraTarget)); diff --git a/Mage.Sets/src/mage/cards/s/SzatsWill.java b/Mage.Sets/src/mage/cards/s/SzatsWill.java index 2bd46e1b517..55aeae3f9e7 100644 --- a/Mage.Sets/src/mage/cards/s/SzatsWill.java +++ b/Mage.Sets/src/mage/cards/s/SzatsWill.java @@ -89,7 +89,7 @@ class SzatsWillEffect extends OneShotEffect { return false; } Cards cards = new CardsImpl(game - .getOpponents(source.getControllerId()) + .getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/t/TalesOfMasterSeshiro.java b/Mage.Sets/src/mage/cards/t/TalesOfMasterSeshiro.java index 04a1d542911..6f85e081486 100644 --- a/Mage.Sets/src/mage/cards/t/TalesOfMasterSeshiro.java +++ b/Mage.Sets/src/mage/cards/t/TalesOfMasterSeshiro.java @@ -13,9 +13,7 @@ import mage.constants.CardType; import mage.constants.SagaChapter; import mage.constants.SubType; import mage.counters.CounterType; -import mage.filter.FilterPermanent; -import mage.filter.common.FilterControlledPermanent; -import mage.filter.predicate.Predicates; +import mage.filter.StaticFilters; import mage.target.TargetPermanent; import java.util.UUID; @@ -25,16 +23,6 @@ import java.util.UUID; */ public final class TalesOfMasterSeshiro extends CardImpl { - private static final FilterPermanent filter - = new FilterControlledPermanent("creature or Vehicle you control"); - - static { - filter.add(Predicates.or( - CardType.CREATURE.getPredicate(), - SubType.VEHICLE.getPredicate() - )); - } - public TalesOfMasterSeshiro(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{G}"); @@ -51,7 +39,7 @@ public final class TalesOfMasterSeshiro extends CardImpl { new AddCountersTargetEffect(CounterType.P1P1.createInstance()), new GainAbilityTargetEffect(VigilanceAbility.getInstance()) .setText("It gains vigilance until end of turn") - ), new TargetPermanent(filter) + ), new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE) ); // III — Exile this Saga, then return it to the battlefield transformed under your control. diff --git a/Mage.Sets/src/mage/cards/t/Tarmogoyf.java b/Mage.Sets/src/mage/cards/t/Tarmogoyf.java index f0d292c4318..d8ed06f564f 100644 --- a/Mage.Sets/src/mage/cards/t/Tarmogoyf.java +++ b/Mage.Sets/src/mage/cards/t/Tarmogoyf.java @@ -18,8 +18,6 @@ import java.util.UUID; */ public final class Tarmogoyf extends CardImpl { - private static final DynamicValue powerValue = CardTypesInGraveyardCount.ALL; - public Tarmogoyf(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}"); this.subtype.add(SubType.LHURGOYF); @@ -28,7 +26,9 @@ public final class Tarmogoyf extends CardImpl { this.toughness = new MageInt(1); // Tarmogoyf's power is equal to the number of card types among cards in all graveyards and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.ALL) + ).addHint(CardTypesInGraveyardCount.ALL.getHint())); } private Tarmogoyf(final Tarmogoyf card) { diff --git a/Mage.Sets/src/mage/cards/t/TasigurTheGoldenFang.java b/Mage.Sets/src/mage/cards/t/TasigurTheGoldenFang.java index 94c1d80bae7..050939d6980 100644 --- a/Mage.Sets/src/mage/cards/t/TasigurTheGoldenFang.java +++ b/Mage.Sets/src/mage/cards/t/TasigurTheGoldenFang.java @@ -35,7 +35,7 @@ public final class TasigurTheGoldenFang extends CardImpl { this.toughness = new MageInt(5); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // {2}{G/U}{G/U}: Put the top two cards of your library into your graveyard, then return a nonland card of an opponent's choice from your graveyard to your hand. Ability ability = new SimpleActivatedAbility(new MillCardsControllerEffect(2), new ManaCostsImpl<>("{2}{G/U}{G/U}")); diff --git a/Mage.Sets/src/mage/cards/t/TasigursCruelty.java b/Mage.Sets/src/mage/cards/t/TasigursCruelty.java index a59ae456232..4bb70e38965 100644 --- a/Mage.Sets/src/mage/cards/t/TasigursCruelty.java +++ b/Mage.Sets/src/mage/cards/t/TasigursCruelty.java @@ -20,7 +20,7 @@ public final class TasigursCruelty extends CardImpl { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{5}{B}"); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); // Each opponent discards two cards. this.getSpellAbility().addEffect(new DiscardEachPlayerEffect(StaticValue.get(2), false, TargetController.OPPONENT)); diff --git a/Mage.Sets/src/mage/cards/t/TeachingsOfTheArchaics.java b/Mage.Sets/src/mage/cards/t/TeachingsOfTheArchaics.java index e87db4b3e5c..019f1eae79d 100644 --- a/Mage.Sets/src/mage/cards/t/TeachingsOfTheArchaics.java +++ b/Mage.Sets/src/mage/cards/t/TeachingsOfTheArchaics.java @@ -62,7 +62,7 @@ class TeachingsOfTheArchaicsEffect extends OneShotEffect { return false; } int diff = game - .getOpponents(source.getControllerId()) + .getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/t/TemporalTrespass.java b/Mage.Sets/src/mage/cards/t/TemporalTrespass.java index d493616320d..8977c71a945 100644 --- a/Mage.Sets/src/mage/cards/t/TemporalTrespass.java +++ b/Mage.Sets/src/mage/cards/t/TemporalTrespass.java @@ -19,7 +19,8 @@ public final class TemporalTrespass extends CardImpl { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{8}{U}{U}{U}"); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); + // Take an extra turn after this one. Exile Temporal Trespass. this.getSpellAbility().addEffect(new AddExtraTurnControllerEffect()); this.getSpellAbility().addEffect(new ExileSpellEffect()); diff --git a/Mage.Sets/src/mage/cards/t/TheAetherspark.java b/Mage.Sets/src/mage/cards/t/TheAetherspark.java new file mode 100644 index 00000000000..c85453d48c8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheAetherspark.java @@ -0,0 +1,115 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.DealsCombatDamageEquippedTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.AttachedToMatchesFilterCondition; +import mage.abilities.condition.common.MyTurnCondition; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.effects.mana.AddManaOfAnyColorEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TheAetherspark extends CardImpl { + + private static final Condition condition = new AttachedToMatchesFilterCondition(StaticFilters.FILTER_PERMANENT_CREATURE); + private static final String rule = "Whenever equipped creature deals combat damage during your turn, put that many loyalty counters on {this}."; + + public TheAetherspark(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.PLANESWALKER}, "{4}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.EQUIPMENT); + this.setStartingLoyalty(4); + + // As long as The Aetherspark is attached to a creature, The Aetherspark can't be attacked and has "Whenever equipped creature deals combat damage during your turn, put that many loyalty counters on The Aetherspark." + Ability ability = new SimpleStaticAbility(new TheAethersparkEffect()); + ability.addEffect(new ConditionalContinuousEffect(new GainAbilitySourceEffect( + new ConditionalTriggeredAbility( + new DealsCombatDamageEquippedTriggeredAbility(new AddCountersSourceEffect( + CounterType.LOYALTY.createInstance(0), SavedDamageValue.MANY + )), MyTurnCondition.instance, rule + ) + ), condition, "and has \"" + rule + "\"")); + this.addAbility(ability); + + // +1: Attach The Aetherspark to up to one target creature you control. Put a +1/+1 counter on that creature. + ability = new LoyaltyAbility(new AttachEffect( + Outcome.BoostCreature, "attach {this} to up to one target creature you control" + ), 1); + ability.addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance()) + .setText("put a +1/+1 counter on that creature")); + ability.addTarget(new TargetControlledCreaturePermanent()); + this.addAbility(ability); + + // -5: Draw two cards. + this.addAbility(new LoyaltyAbility(new DrawCardSourceControllerEffect(2), -5)); + + // -10: Add ten mana of any one color. + this.addAbility(new LoyaltyAbility(new AddManaOfAnyColorEffect(10), -10)); + } + + private TheAetherspark(final TheAetherspark card) { + super(card); + } + + @Override + public TheAetherspark copy() { + return new TheAetherspark(this); + } +} + +class TheAethersparkEffect extends RestrictionEffect { + + TheAethersparkEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "as long as {this} is attached to a creature, {this} can't be attacked"; + } + + private TheAethersparkEffect(final TheAethersparkEffect effect) { + super(effect); + } + + @Override + public TheAethersparkEffect copy() { + return new TheAethersparkEffect(this); + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + return Optional + .ofNullable(source.getControllerId()) + .map(game::getPermanent) + .map(Permanent::getAttachedTo) + .map(game::getPermanent) + .map(p -> p.isCreature(game)) + .orElse(false); + } + + @Override + public boolean canAttack(Permanent attacker, UUID defenderId, Ability source, Game game, boolean canUseChooseDialogs) { + return !source.getSourceId().equals(defenderId); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TheEnigmaJewel.java b/Mage.Sets/src/mage/cards/t/TheEnigmaJewel.java index 1b5dfaa5a6d..6a515394202 100644 --- a/Mage.Sets/src/mage/cards/t/TheEnigmaJewel.java +++ b/Mage.Sets/src/mage/cards/t/TheEnigmaJewel.java @@ -1,16 +1,10 @@ package mage.cards.t; -import mage.ConditionalMana; import mage.MageObject; -import mage.Mana; -import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTappedAbility; -import mage.abilities.condition.Condition; -import mage.abilities.costs.Cost; import mage.abilities.keyword.CraftAbility; import mage.abilities.mana.ConditionalColorlessManaAbility; -import mage.abilities.mana.builder.ConditionalManaBuilder; -import mage.abilities.mana.conditional.ManaCondition; +import mage.abilities.mana.builder.common.ActivatedAbilityManaBuilder; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -36,7 +30,7 @@ public final class TheEnigmaJewel extends CardImpl { this.addAbility(new EntersBattlefieldTappedAbility()); // {T}: Add {C}{C}. Spend this mana only to activate abilities. - this.addAbility(new ConditionalColorlessManaAbility(2, new TheEnigmaJewelManaBuilder())); + this.addAbility(new ConditionalColorlessManaAbility(2, new ActivatedAbilityManaBuilder())); // Craft with four or more nonlands with activated abilities {8}{U} this.addAbility(new CraftAbility( @@ -67,43 +61,4 @@ enum TheEnigmaJewelPredicate implements Predicate { .stream() .anyMatch(a -> (a.isActivatedAbility())); } - -} - -class TheEnigmaJewelManaBuilder extends ConditionalManaBuilder { - - @Override - public ConditionalMana build(Object... options) { - return new TheEnigmaJewelConditionalMana(this.mana); - } - - @Override - public String getRule() { - return "Spend this mana only to activate abilities"; - } -} - -class TheEnigmaJewelConditionalMana extends ConditionalMana { - - TheEnigmaJewelConditionalMana(Mana mana) { - super(mana); - staticText = "Spend this mana only to activate abilities"; - addCondition(new TheEnigmaJewelManaCondition()); - } -} - -class TheEnigmaJewelManaCondition extends ManaCondition implements Condition { - - @Override - public boolean apply(Game game, Ability source) { - if (source != null && !source.isActivated()) { - return source.isActivatedAbility(); - } - return false; - } - - @Override - public boolean apply(Game game, Ability source, UUID originalId, Cost costsToPay) { - return apply(game, source); - } } diff --git a/Mage.Sets/src/mage/cards/t/TheFirstDoctor.java b/Mage.Sets/src/mage/cards/t/TheFirstDoctor.java new file mode 100644 index 00000000000..e6609282cc0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheFirstDoctor.java @@ -0,0 +1,67 @@ +package mage.cards.t; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.effects.common.search.SearchLibraryGraveyardPutInHandEffect; +import mage.abilities.keyword.CascadeAbility; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.CardType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.FilterSpell; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.filter.predicate.mageobject.NamePredicate; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; + +/** + * + * @author padfoot + */ +public final class TheFirstDoctor extends CardImpl { + + private static final FilterSpell filterSpell = new FilterSpell("a spell with cascade"); + private static final FilterCard filterTARDIS = new FilterCard("TARDIS"); + + static { + filterSpell.add(new AbilityPredicate(CascadeAbility.class)); + filterTARDIS.add(new NamePredicate("TARDIS")); + } + + public TheFirstDoctor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.TIME_LORD, SubType.DOCTOR); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // When The First Doctor enters the battlefield, search your library and/or graveyard for a card named TARDIS, reveal it, and put it into your hand. If you search your library this way, shuffle. + this.addAbility(new EntersBattlefieldTriggeredAbility(new SearchLibraryGraveyardPutInHandEffect(filterTARDIS), false)); + + // Whenever you cast a spell with cascade, put a +1/+1 counter on target artifact or creature. + Ability ability = new SpellCastControllerTriggeredAbility( + new AddCountersTargetEffect(CounterType.P1P1.createInstance()), + filterSpell, + false + ); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_CREATURE)); + this.addAbility(ability); + } + + private TheFirstDoctor(final TheFirstDoctor card) { + super(card); + } + + @Override + public TheFirstDoctor copy() { + return new TheFirstDoctor(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TheRollercrusherRide.java b/Mage.Sets/src/mage/cards/t/TheRollercrusherRide.java index 1dbe303bd32..d36dd9455d3 100644 --- a/Mage.Sets/src/mage/cards/t/TheRollercrusherRide.java +++ b/Mage.Sets/src/mage/cards/t/TheRollercrusherRide.java @@ -6,10 +6,10 @@ import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.DamageTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.constants.*; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -34,7 +34,7 @@ public final class TheRollercrusherRide extends CardImpl { // it deals double that damage instead. this.addAbility(new SimpleStaticAbility(new TheRollercrusherRideEffect()) .setAbilityWord(AbilityWord.DELIRIUM) - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); // When The Rollercrusher Ride enters, it deals X damage to each of up to X target creatures. Ability ability = new EntersBattlefieldTriggeredAbility(new DamageTargetEffect(GetXValue.instance) diff --git a/Mage.Sets/src/mage/cards/t/TheSwarmweaver.java b/Mage.Sets/src/mage/cards/t/TheSwarmweaver.java index eb12ffe1b25..b16aaa71d2b 100644 --- a/Mage.Sets/src/mage/cards/t/TheSwarmweaver.java +++ b/Mage.Sets/src/mage/cards/t/TheSwarmweaver.java @@ -6,10 +6,10 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.continuous.BoostControlledEffect; import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.DeathtouchAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -62,7 +62,7 @@ public final class TheSwarmweaver extends CardImpl { DeathtouchAbility.getInstance(), Duration.WhileOnBattlefield, filter2 ), DeliriumCondition.instance, "and have deathtouch" )); - this.addAbility(ability.setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + this.addAbility(ability.setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private TheSwarmweaver(final TheSwarmweaver card) { diff --git a/Mage.Sets/src/mage/cards/t/ThickestInTheThicket.java b/Mage.Sets/src/mage/cards/t/ThickestInTheThicket.java new file mode 100644 index 00000000000..47c286482af --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/ThickestInTheThicket.java @@ -0,0 +1,48 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.ControlsCreatureGreatestPowerCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.TargetPermanentPowerCount; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author PurpleCrowbar + */ +public final class ThickestInTheThicket extends CardImpl { + + public ThickestInTheThicket(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{G}{G}"); + + // When Thickest in the Thicket enters, put X +1/+1 counters on target creature, where X is that creature's power. + Ability ability = new EntersBattlefieldTriggeredAbility(new AddCountersTargetEffect(CounterType.P1P1.createInstance(), TargetPermanentPowerCount.instance) + .setText("put X +1/+1 counters on target creature, where X is that creature's power") + ); + ability.addTarget(new TargetCreaturePermanent()); + this.addAbility(ability); + + // At the beginning of your end step, draw two cards if you control the creature with the greatest power or tied for the greatest power. + this.addAbility(new BeginningOfEndStepTriggeredAbility(new ConditionalOneShotEffect( + new DrawCardSourceControllerEffect(2), ControlsCreatureGreatestPowerCondition.instance + ).withConditionTextAtEnd(true))); + } + + private ThickestInTheThicket(final ThickestInTheThicket card) { + super(card); + } + + @Override + public ThickestInTheThicket copy() { + return new ThickestInTheThicket(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/ThrabenFoulbloods.java b/Mage.Sets/src/mage/cards/t/ThrabenFoulbloods.java index 2b011cce9a5..5d358717bba 100644 --- a/Mage.Sets/src/mage/cards/t/ThrabenFoulbloods.java +++ b/Mage.Sets/src/mage/cards/t/ThrabenFoulbloods.java @@ -7,16 +7,15 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.MenaceAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @author LevelX2 @@ -35,7 +34,7 @@ public final class ThrabenFoulbloods extends CardImpl { Ability ability = new SimpleStaticAbility(effect); ability.addEffect(new ConditionalContinuousEffect(new GainAbilitySourceEffect(new MenaceAbility()), DeliriumCondition.instance, "and has menace as long as there are four or more card types among cards in your graveyard. (A creature with menace can't be blocked except by two or more creatures.)")); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/t/ThunderheadGunner.java b/Mage.Sets/src/mage/cards/t/ThunderheadGunner.java new file mode 100644 index 00000000000..dc52e87c7ea --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/ThunderheadGunner.java @@ -0,0 +1,46 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.common.LimitedTimesPerTurnActivatedAbility; +import mage.abilities.costs.common.DiscardCardCost; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.keyword.ReachAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TimingRule; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ThunderheadGunner extends CardImpl { + + public ThunderheadGunner(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{R}"); + + this.subtype.add(SubType.SHARK); + this.subtype.add(SubType.PIRATE); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // Reach + this.addAbility(ReachAbility.getInstance()); + + // Discard a card: Draw a card. Activate only as a sorcery and only once each turn. + this.addAbility(new LimitedTimesPerTurnActivatedAbility( + new DrawCardSourceControllerEffect(1), new DiscardCardCost() + ).setTiming(TimingRule.SORCERY)); + } + + private ThunderheadGunner(final ThunderheadGunner card) { + super(card); + } + + @Override + public ThunderheadGunner copy() { + return new ThunderheadGunner(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TicketTortoise.java b/Mage.Sets/src/mage/cards/t/TicketTortoise.java new file mode 100644 index 00000000000..8d9a58dac6a --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TicketTortoise.java @@ -0,0 +1,48 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.OpponentControlsMoreCondition; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.DefenderAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.game.permanent.token.TreasureToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TicketTortoise extends CardImpl { + + private static final Condition condition = new OpponentControlsMoreCondition(StaticFilters.FILTER_LANDS); + + public TicketTortoise(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{2}"); + + this.subtype.add(SubType.TURTLE); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + + // Defender + this.addAbility(DefenderAbility.getInstance()); + + // When this creature enters, if an opponent controls more lands than you, you create a Treasure token. + this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new TreasureToken()) + .setText("you create a Treasure token")).withInterveningIf(condition)); + } + + private TicketTortoise(final TicketTortoise card) { + super(card); + } + + @Override + public TicketTortoise copy() { + return new TicketTortoise(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/ToTheSlaughter.java b/Mage.Sets/src/mage/cards/t/ToTheSlaughter.java index 6cb638d7c2c..0b441cf269c 100644 --- a/Mage.Sets/src/mage/cards/t/ToTheSlaughter.java +++ b/Mage.Sets/src/mage/cards/t/ToTheSlaughter.java @@ -5,8 +5,8 @@ import java.util.UUID; import mage.abilities.condition.InvertCondition; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.SacrificeEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -37,7 +37,7 @@ public final class ToTheSlaughter extends CardImpl { new SacrificeEffect(StaticFilters.FILTER_PERMANENT_PLANESWALKER, 1, "Target player"), DeliriumCondition.instance, "and a planeswalker.")); this.getSpellAbility().addTarget(new TargetPlayer()); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private ToTheSlaughter(final ToTheSlaughter card) { diff --git a/Mage.Sets/src/mage/cards/t/Tombstalker.java b/Mage.Sets/src/mage/cards/t/Tombstalker.java index 138a32a73e3..7c415ad3bbb 100644 --- a/Mage.Sets/src/mage/cards/t/Tombstalker.java +++ b/Mage.Sets/src/mage/cards/t/Tombstalker.java @@ -26,10 +26,9 @@ public final class Tombstalker extends CardImpl { // Flying this.addAbility(FlyingAbility.getInstance()); + // Delve - Ability ability = new DelveAbility(); - ability.setRuleAtTheTop(false); - this.addAbility(ability); + this.addAbility(new DelveAbility(false)); } private Tombstalker(final Tombstalker card) { diff --git a/Mage.Sets/src/mage/cards/t/ToothCollector.java b/Mage.Sets/src/mage/cards/t/ToothCollector.java index d4b8893c581..6e032980e2c 100644 --- a/Mage.Sets/src/mage/cards/t/ToothCollector.java +++ b/Mage.Sets/src/mage/cards/t/ToothCollector.java @@ -8,8 +8,8 @@ import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -48,7 +48,7 @@ public final class ToothCollector extends CardImpl { DeliriumCondition.instance, "Delirium — At the beginning of each opponent's upkeep, if there are four or more card types among cards in your graveyard, " + "target creature that player controls gets -1/-1 until end of turn."); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/t/Topplegeist.java b/Mage.Sets/src/mage/cards/t/Topplegeist.java index 1d948ec2114..c7123946614 100644 --- a/Mage.Sets/src/mage/cards/t/Topplegeist.java +++ b/Mage.Sets/src/mage/cards/t/Topplegeist.java @@ -8,8 +8,8 @@ import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.TapTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -50,7 +50,7 @@ public final class Topplegeist extends CardImpl { DeliriumCondition.instance, "Delirium — At the beginning of each opponent's upkeep, if there are four or more card types among cards in your graveyard, " + "tap target creature that player controls."); - ability.addHint(CardTypesInGraveyardHint.YOU); + ability.addHint(CardTypesInGraveyardCount.YOU.getHint()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/t/TradeTheHelm.java b/Mage.Sets/src/mage/cards/t/TradeTheHelm.java new file mode 100644 index 00000000000..f660f0cf62a --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TradeTheHelm.java @@ -0,0 +1,43 @@ +package mage.cards.t; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.continuous.ExchangeControlTargetEffect; +import mage.abilities.keyword.CyclingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.filter.StaticFilters; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TradeTheHelm extends CardImpl { + + public TradeTheHelm(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{U}"); + + // Exchange control of target artifact or creature you control and target artifact or creature an opponent controls. + this.getSpellAbility().addEffect(new ExchangeControlTargetEffect( + Duration.EndOfGame, "exchange control of target artifact or creature you control " + + "and target artifact or creature an opponent controls", false, true + )); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT_OR_CREATURE)); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_ARTIFACT_OR_CREATURE)); + + // Cycling {2} + this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{2}"))); + } + + private TradeTheHelm(final TradeTheHelm card) { + super(card); + } + + @Override + public TradeTheHelm copy() { + return new TradeTheHelm(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TraverseTheUlvenwald.java b/Mage.Sets/src/mage/cards/t/TraverseTheUlvenwald.java index 3f1bf860b31..85af720195f 100644 --- a/Mage.Sets/src/mage/cards/t/TraverseTheUlvenwald.java +++ b/Mage.Sets/src/mage/cards/t/TraverseTheUlvenwald.java @@ -3,7 +3,6 @@ package mage.cards.t; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.AbilityWord; diff --git a/Mage.Sets/src/mage/cards/t/TreasureCruise.java b/Mage.Sets/src/mage/cards/t/TreasureCruise.java index 6adc2b216e9..c3ba68e8998 100644 --- a/Mage.Sets/src/mage/cards/t/TreasureCruise.java +++ b/Mage.Sets/src/mage/cards/t/TreasureCruise.java @@ -1,4 +1,3 @@ - package mage.cards.t; import java.util.UUID; @@ -17,9 +16,9 @@ public final class TreasureCruise extends CardImpl { public TreasureCruise(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{7}{U}"); - // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); + // Draw 3 Cards this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(3)); } diff --git a/Mage.Sets/src/mage/cards/t/TripUp.java b/Mage.Sets/src/mage/cards/t/TripUp.java new file mode 100644 index 00000000000..93fb7247fa4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TripUp.java @@ -0,0 +1,37 @@ +package mage.cards.t; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.PutOnTopOrBottomLibraryTargetEffect; +import mage.abilities.keyword.CyclingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.common.TargetNonlandPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TripUp extends CardImpl { + + public TripUp(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{U}"); + + // Target nonland permanent's owner puts it on their choice of the top or bottom of their library. + this.getSpellAbility().addEffect(new PutOnTopOrBottomLibraryTargetEffect(false)); + this.getSpellAbility().addTarget(new TargetNonlandPermanent()); + + // Cycling {2} + this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{2}"))); + } + + private TripUp(final TripUp card) { + super(card); + } + + @Override + public TripUp copy() { + return new TripUp(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TyvarThePummeler.java b/Mage.Sets/src/mage/cards/t/TyvarThePummeler.java index c1e11fd2571..0c8e8b9f6f0 100644 --- a/Mage.Sets/src/mage/cards/t/TyvarThePummeler.java +++ b/Mage.Sets/src/mage/cards/t/TyvarThePummeler.java @@ -58,7 +58,7 @@ public final class TyvarThePummeler extends CardImpl { this.addAbility(new SimpleActivatedAbility(new BoostControlledEffect( GreatestPowerAmongControlledCreaturesValue.instance, GreatestPowerAmongControlledCreaturesValue.instance, Duration.EndOfTurn - ), new ManaCostsImpl<>("{3}{G}{G}"))); + ), new ManaCostsImpl<>("{3}{G}{G}")).addHint(GreatestPowerAmongControlledCreaturesValue.getHint())); } private TyvarThePummeler(final TyvarThePummeler card) { diff --git a/Mage.Sets/src/mage/cards/u/UnholyHeat.java b/Mage.Sets/src/mage/cards/u/UnholyHeat.java index ae3f1920fd6..296fc21f8f8 100644 --- a/Mage.Sets/src/mage/cards/u/UnholyHeat.java +++ b/Mage.Sets/src/mage/cards/u/UnholyHeat.java @@ -2,8 +2,8 @@ package mage.cards.u; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.DamageTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -28,7 +28,7 @@ public final class UnholyHeat extends CardImpl { "if there are four or more card types among cards in your graveyard." )); this.getSpellAbility().addTarget(new TargetCreatureOrPlaneswalker()); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private UnholyHeat(final UnholyHeat card) { diff --git a/Mage.Sets/src/mage/cards/u/UrborgLhurgoyf.java b/Mage.Sets/src/mage/cards/u/UrborgLhurgoyf.java index d25cb2e8fb2..8c02220162f 100644 --- a/Mage.Sets/src/mage/cards/u/UrborgLhurgoyf.java +++ b/Mage.Sets/src/mage/cards/u/UrborgLhurgoyf.java @@ -9,6 +9,7 @@ import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; import mage.abilities.dynamicvalue.common.MultikickerCount; import mage.abilities.effects.common.MillCardsControllerEffect; import mage.abilities.effects.common.continuous.SetBasePowerToughnessPlusOneSourceEffect; +import mage.abilities.hint.ValueHint; import mage.abilities.keyword.KickerAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -43,7 +44,9 @@ public final class UrborgLhurgoyf extends CardImpl { this.addAbility(new AsEntersBattlefieldAbility(new MillCardsControllerEffect(millValue))); // Urborg Lhurgoyf's power is equal to the number of creature cards in your graveyard and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(powerValue) + ).addHint(new ValueHint("Creature cards in your graveyard", powerValue))); } private UrborgLhurgoyf(final UrborgLhurgoyf card) { diff --git a/Mage.Sets/src/mage/cards/u/UvildaDeanOfPerfection.java b/Mage.Sets/src/mage/cards/u/UvildaDeanOfPerfection.java index 47665872602..e12ddb9f71c 100644 --- a/Mage.Sets/src/mage/cards/u/UvildaDeanOfPerfection.java +++ b/Mage.Sets/src/mage/cards/u/UvildaDeanOfPerfection.java @@ -281,7 +281,7 @@ class NassariDeanOfExpressionEffect extends OneShotEffect { return false; } Cards cards = new CardsImpl(); - game.getOpponents(source.getControllerId()) + game.getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/v/VeteranBeastrider.java b/Mage.Sets/src/mage/cards/v/VeteranBeastrider.java index 03a6231687d..327092b813e 100644 --- a/Mage.Sets/src/mage/cards/v/VeteranBeastrider.java +++ b/Mage.Sets/src/mage/cards/v/VeteranBeastrider.java @@ -36,7 +36,7 @@ public final class VeteranBeastrider extends CardImpl { // {2}{G}{W}: Creatures you control get +1/+1 until end of turn. this.addAbility(new SimpleActivatedAbility(new BoostControlledEffect( - 1, 1, Duration.EndOfTurn, true + 1, 1, Duration.EndOfTurn, false ), new ManaCostsImpl<>("{2}{G}{W}"))); } diff --git a/Mage.Sets/src/mage/cards/v/ViolentUrge.java b/Mage.Sets/src/mage/cards/v/ViolentUrge.java index dda8d7c1cdf..c9a3b8ab777 100644 --- a/Mage.Sets/src/mage/cards/v/ViolentUrge.java +++ b/Mage.Sets/src/mage/cards/v/ViolentUrge.java @@ -2,10 +2,10 @@ package mage.cards.v; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.AddContinuousEffectToGame; import mage.abilities.effects.common.continuous.BoostTargetEffect; import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.DoubleStrikeAbility; import mage.abilities.keyword.FirstStrikeAbility; import mage.cards.CardImpl; @@ -40,7 +40,7 @@ public final class ViolentUrge extends CardImpl { DeliriumCondition.instance, AbilityWord.DELIRIUM.formatWord() + "If there are four or more " + "card types among cards in your graveyard, that creature gains double strike until end of turn" ).concatBy("
")); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private ViolentUrge(final ViolentUrge card) { diff --git a/Mage.Sets/src/mage/cards/v/VnwxtVerboseHost.java b/Mage.Sets/src/mage/cards/v/VnwxtVerboseHost.java new file mode 100644 index 00000000000..20fe9ade5ea --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VnwxtVerboseHost.java @@ -0,0 +1,89 @@ +package mage.cards.v; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.continuous.MaximumHandSizeControllerEffect; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class VnwxtVerboseHost extends CardImpl { + + public VnwxtVerboseHost(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HOMUNCULUS); + this.power = new MageInt(0); + this.toughness = new MageInt(4); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // You have no maximum hand size. + this.addAbility(new SimpleStaticAbility(new MaximumHandSizeControllerEffect( + Integer.MAX_VALUE, Duration.WhileOnBattlefield, + MaximumHandSizeControllerEffect.HandSizeModification.SET + ))); + + // Max speed -- If you would draw a card, draw two cards instead. + this.addAbility(new MaxSpeedAbility(new VnwxtVerboseHostEffect())); + } + + private VnwxtVerboseHost(final VnwxtVerboseHost card) { + super(card); + } + + @Override + public VnwxtVerboseHost copy() { + return new VnwxtVerboseHost(this); + } +} + +class VnwxtVerboseHostEffect extends ReplacementEffectImpl { + + VnwxtVerboseHostEffect() { + super(Duration.WhileOnBattlefield, Outcome.Neutral); + staticText = "if you would draw a card, draw two cards instead"; + } + + private VnwxtVerboseHostEffect(final VnwxtVerboseHostEffect effect) { + super(effect); + } + + @Override + public VnwxtVerboseHostEffect copy() { + return new VnwxtVerboseHostEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DRAW_CARD; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return event.getPlayerId().equals(source.getControllerId()); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player you = game.getPlayer(event.getPlayerId()); + if (you != null) { + you.drawCards(2, source, game, event); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/v/VoiceOfMany.java b/Mage.Sets/src/mage/cards/v/VoiceOfMany.java index 39be1262c85..a36bbb608e6 100644 --- a/Mage.Sets/src/mage/cards/v/VoiceOfMany.java +++ b/Mage.Sets/src/mage/cards/v/VoiceOfMany.java @@ -64,7 +64,7 @@ class VoiceOfManyEffect extends OneShotEffect { StaticFilters.FILTER_PERMANENT_CREATURE, source.getControllerId(), game ).size(); int toDraw = game - .getOpponents(source.getControllerId()) + .getOpponents(source.getControllerId(), true) .stream() .mapToInt(uuid -> game.getBattlefield().getAllActivePermanents( StaticFilters.FILTER_PERMANENT_CREATURE, uuid, game diff --git a/Mage.Sets/src/mage/cards/v/VoyagerGlidecar.java b/Mage.Sets/src/mage/cards/v/VoyagerGlidecar.java new file mode 100644 index 00000000000..5131996f87e --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VoyagerGlidecar.java @@ -0,0 +1,68 @@ +package mage.cards.v; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapTargetCost; +import mage.abilities.effects.common.continuous.AddCardTypeSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.keyword.ScryEffect; +import mage.abilities.keyword.CrewAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.target.common.TargetControlledPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class VoyagerGlidecar extends CardImpl { + + public VoyagerGlidecar(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{W}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // When this Vehicle enters, scry 1. + this.addAbility(new EntersBattlefieldTriggeredAbility(new ScryEffect(1))); + + // Tap three other untapped creatures you control: Until end of turn, this Vehicle becomes an artifact creature and gains flying. Put a +1/+1 counter on it. + Ability ability = new SimpleActivatedAbility( + new AddCardTypeSourceEffect( + Duration.EndOfTurn, CardType.ARTIFACT, CardType.CREATURE + ).setText("until end of turn, this Vehicle becomes an artifact creature"), + new TapTargetCost(new TargetControlledPermanent( + 3, StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURES + )) + ); + ability.addEffect(new GainAbilitySourceEffect( + FlyingAbility.getInstance(), Duration.EndOfTurn + ).setText("and gains flying")); + ability.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance()) + .setText("put a +1/+1 counter on it")); + this.addAbility(ability); + + // Crew 1 + this.addAbility(new CrewAbility(1)); + } + + private VoyagerGlidecar(final VoyagerGlidecar card) { + super(card); + } + + @Override + public VoyagerGlidecar copy() { + return new VoyagerGlidecar(this); + } +} diff --git a/Mage.Sets/src/mage/cards/w/WatcherForTomorrow.java b/Mage.Sets/src/mage/cards/w/WatcherForTomorrow.java index 3b3e5e2145b..57b123c4739 100644 --- a/Mage.Sets/src/mage/cards/w/WatcherForTomorrow.java +++ b/Mage.Sets/src/mage/cards/w/WatcherForTomorrow.java @@ -33,7 +33,7 @@ public final class WatcherForTomorrow extends CardImpl { this.toughness = new MageInt(1); // Hideaway - this.addAbility(new HideawayAbility(4)); + this.addAbility(new HideawayAbility(this, 4)); this.addAbility(new EntersBattlefieldTappedAbility()); // When Watcher for Tomorrow leaves the battlefield, put the exiled card into its owner's hand. diff --git a/Mage.Sets/src/mage/cards/w/WebstrikeElite.java b/Mage.Sets/src/mage/cards/w/WebstrikeElite.java new file mode 100644 index 00000000000..bfbe9a00a7b --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WebstrikeElite.java @@ -0,0 +1,100 @@ +package mage.cards.w; + +import mage.MageInt; +import mage.abilities.common.ZoneChangeTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.keyword.CyclingAbility; +import mage.abilities.keyword.ReachAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterArtifactOrEnchantmentPermanent; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.StackObject; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WebstrikeElite extends CardImpl { + + public WebstrikeElite(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{G}"); + + this.subtype.add(SubType.INSECT); + this.subtype.add(SubType.ARCHER); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Reach + this.addAbility(ReachAbility.getInstance()); + + // Cycling {X}{G}{G} + this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{X}{G}{G}"))); + + // When you cycle this card, destroy up to one target artifact or enchantment with mana value X. + this.addAbility(new WebstrikeEliteTriggeredAbility()); + } + + private WebstrikeElite(final WebstrikeElite card) { + super(card); + } + + @Override + public WebstrikeElite copy() { + return new WebstrikeElite(this); + } +} + +class WebstrikeEliteTriggeredAbility extends ZoneChangeTriggeredAbility { + + WebstrikeEliteTriggeredAbility() { + super(Zone.ALL, null, "", false); + } + + private WebstrikeEliteTriggeredAbility(final WebstrikeEliteTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ACTIVATED_ABILITY; + } + + @Override + public WebstrikeEliteTriggeredAbility copy() { + return new WebstrikeEliteTriggeredAbility(this); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!event.getSourceId().equals(this.getSourceId())) { + return false; + } + StackObject object = game.getStack().getStackObject(event.getSourceId()); + if (object == null || !(object.getStackAbility() instanceof CyclingAbility)) { + return false; + } + FilterPermanent filter = new FilterArtifactOrEnchantmentPermanent("artifact or enchantment with mana value X"); + filter.add(new ManaValuePredicate( + ComparisonType.EQUAL_TO, GetXValue.instance.calculate(game, object.getStackAbility(), null) + )); + this.getTargets().clear(); + this.addTarget(new TargetPermanent(0, 1, filter)); + return true; + } + + @Override + public String getRule() { + return "When you cycle this card, destroy up to one target artifact or enchantment with mana value X."; + } +} diff --git a/Mage.Sets/src/mage/cards/w/WhispersOfEmrakul.java b/Mage.Sets/src/mage/cards/w/WhispersOfEmrakul.java index c60aef42e84..e51b24000da 100644 --- a/Mage.Sets/src/mage/cards/w/WhispersOfEmrakul.java +++ b/Mage.Sets/src/mage/cards/w/WhispersOfEmrakul.java @@ -5,8 +5,8 @@ import java.util.UUID; import mage.abilities.condition.InvertCondition; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.discard.DiscardTargetEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -32,7 +32,7 @@ public final class WhispersOfEmrakul extends CardImpl { DeliriumCondition.instance, "
Delirium — If there are four or more card types among cards in your graveyard, that player discards two cards at random instead")); this.getSpellAbility().addTarget(new TargetOpponent()); - this.getSpellAbility().addHint(CardTypesInGraveyardHint.YOU); + this.getSpellAbility().addHint(CardTypesInGraveyardCount.YOU.getHint()); } private WhispersOfEmrakul(final WhispersOfEmrakul card) { diff --git a/Mage.Sets/src/mage/cards/w/WickerfolkThresher.java b/Mage.Sets/src/mage/cards/w/WickerfolkThresher.java index 9d405b5897f..c22f8d62ab0 100644 --- a/Mage.Sets/src/mage/cards/w/WickerfolkThresher.java +++ b/Mage.Sets/src/mage/cards/w/WickerfolkThresher.java @@ -5,8 +5,8 @@ import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.OneShotEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -34,7 +34,7 @@ public final class WickerfolkThresher extends CardImpl { DeliriumCondition.instance, "Whenever {this} attacks, if there are four or more card types " + "among cards in your graveyard, look at the top card of your library. If it's a land card, you may " + "put it onto the battlefield. If you don't put the card onto the battlefield, put it into your hand." - ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + ).setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private WickerfolkThresher(final WickerfolkThresher card) { diff --git a/Mage.Sets/src/mage/cards/w/WidespreadThieving.java b/Mage.Sets/src/mage/cards/w/WidespreadThieving.java index ea77d732fa7..42cf54aacce 100644 --- a/Mage.Sets/src/mage/cards/w/WidespreadThieving.java +++ b/Mage.Sets/src/mage/cards/w/WidespreadThieving.java @@ -31,7 +31,7 @@ public final class WidespreadThieving extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{R}"); // Hideaway 5 - this.addAbility(new HideawayAbility(5)); + this.addAbility(new HideawayAbility(this, 5)); // Whenever you cast a multicolored spell, create a Treasure token. Then you may pay {W}{U}{B}{R}{G}. If you do, you may play the exiled card without paying its mana cost. Ability ability = new SpellCastControllerTriggeredAbility( diff --git a/Mage.Sets/src/mage/cards/w/WildfireWickerfolk.java b/Mage.Sets/src/mage/cards/w/WildfireWickerfolk.java index f473e11cf01..2fed84eee1f 100644 --- a/Mage.Sets/src/mage/cards/w/WildfireWickerfolk.java +++ b/Mage.Sets/src/mage/cards/w/WildfireWickerfolk.java @@ -5,9 +5,9 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.DeliriumCondition; import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.HasteAbility; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; @@ -43,7 +43,7 @@ public final class WildfireWickerfolk extends CardImpl { new GainAbilitySourceEffect(TrampleAbility.getInstance()), DeliriumCondition.instance, "and has trample as long as there are four or more card types among cards in your graveyard" )); - this.addAbility(ability.setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardHint.YOU)); + this.addAbility(ability.setAbilityWord(AbilityWord.DELIRIUM).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private WildfireWickerfolk(final WildfireWickerfolk card) { diff --git a/Mage.Sets/src/mage/cards/w/WilfredMott.java b/Mage.Sets/src/mage/cards/w/WilfredMott.java new file mode 100644 index 00000000000..ca81091b7d4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WilfredMott.java @@ -0,0 +1,65 @@ +package mage.cards.w; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CountersSourceCount; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.PutCards; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.common.FilterNonlandCard; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.filter.predicate.mageobject.PermanentPredicate; + +/** + * + * @author padfoot + */ +public final class WilfredMott extends CardImpl { + + private static final FilterCard filter = new FilterNonlandCard("nonland permanent card with mana value 3 or less"); + private static final DynamicValue xValue = new CountersSourceCount(CounterType.TIME); + + static { + filter.add(PermanentPredicate.instance); + filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 4)); + } + + public WilfredMott(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Look to the Stars -- At the beginning of your upkeep, put a time counter on Wilfred Mott. Then look at the top X cards of your library, where X is the number of time counters on Wilfred Mott. You may put a nonland permanent card with mana value 3 or less from among them onto the battlefield. Put the rest on the bottom of your library in a random order. + Ability ability = new BeginningOfUpkeepTriggeredAbility(new AddCountersSourceEffect(CounterType.TIME.createInstance())).withFlavorWord("Look to the Stars"); + LookLibraryAndPickControllerEffect effect = new LookLibraryAndPickControllerEffect(xValue, 1, filter, PutCards.BATTLEFIELD, PutCards.BOTTOM_RANDOM); + effect.setText("Then look at the top X cards of your library, where X is the number of time counters on Wilfred Mott. " + + " You may put a nonland permanent card with mana value 3 or less from among them onto the battlefield. " + + " Put the rest on the bottom of your library in a random order."); + ability.addEffect(effect); + this.addAbility(ability); + } + + private WilfredMott(final WilfredMott card) { + super(card); + } + + @Override + public WilfredMott copy() { + return new WilfredMott(this); + } +} diff --git a/Mage.Sets/src/mage/cards/w/WillOfTheNaga.java b/Mage.Sets/src/mage/cards/w/WillOfTheNaga.java index dc6cd9d0141..64a61719676 100644 --- a/Mage.Sets/src/mage/cards/w/WillOfTheNaga.java +++ b/Mage.Sets/src/mage/cards/w/WillOfTheNaga.java @@ -1,4 +1,3 @@ - package mage.cards.w; import java.util.UUID; @@ -20,7 +19,8 @@ public final class WillOfTheNaga extends CardImpl { super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{4}{U}{U}"); // Delve - this.addAbility(new DelveAbility()); + this.addAbility(new DelveAbility(false)); + // Tap up to two target creatures. Those creatures don't untap during their controller's next untap step. this.getSpellAbility().addEffect(new TapTargetEffect()); this.getSpellAbility().addTarget(new TargetCreaturePermanent(0, 2)); diff --git a/Mage.Sets/src/mage/cards/w/WindbriskHeights.java b/Mage.Sets/src/mage/cards/w/WindbriskHeights.java index 17ed6c5a3aa..84e80166b41 100644 --- a/Mage.Sets/src/mage/cards/w/WindbriskHeights.java +++ b/Mage.Sets/src/mage/cards/w/WindbriskHeights.java @@ -27,7 +27,7 @@ public final class WindbriskHeights extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // Hideaway (This land enters the battlefield tapped. When it does, look at the top four cards of your library, exile one face down, then put the rest on the bottom of your library.) - this.addAbility(new HideawayAbility(4)); + this.addAbility(new HideawayAbility(this, 4)); this.addAbility(new EntersBattlefieldTappedAbility()); // {tap}: Add {W}. diff --git a/Mage.Sets/src/mage/cards/w/WinterMisanthropicGuide.java b/Mage.Sets/src/mage/cards/w/WinterMisanthropicGuide.java index aab1d89b7fe..323628285c4 100644 --- a/Mage.Sets/src/mage/cards/w/WinterMisanthropicGuide.java +++ b/Mage.Sets/src/mage/cards/w/WinterMisanthropicGuide.java @@ -8,7 +8,6 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.DrawCardAllEffect; -import mage.abilities.hint.common.CardTypesInGraveyardHint; import mage.abilities.keyword.WardAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -43,7 +42,7 @@ public final class WinterMisanthropicGuide extends CardImpl { // Delirium -- As long as there are four or more card types among cards in your graveyard, each opponent's maximum hand size is equal to seven minus the number of those card types. this.addAbility(new SimpleStaticAbility(new WinterMisanthropicGuideEffect()) .setAbilityWord(AbilityWord.DELIRIUM) - .addHint(CardTypesInGraveyardHint.YOU)); + .addHint(CardTypesInGraveyardCount.YOU.getHint())); } private WinterMisanthropicGuide(final WinterMisanthropicGuide card) { diff --git a/Mage.Sets/src/mage/cards/w/Wiretapping.java b/Mage.Sets/src/mage/cards/w/Wiretapping.java index 1b4a563f59f..afbb61fde73 100644 --- a/Mage.Sets/src/mage/cards/w/Wiretapping.java +++ b/Mage.Sets/src/mage/cards/w/Wiretapping.java @@ -28,7 +28,7 @@ public final class Wiretapping extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{U}"); // Hideaway 5 - this.addAbility(new HideawayAbility(5)); + this.addAbility(new HideawayAbility(this, 5)); // Whenever you draw your first card during each of your draw steps, draw a card. Then if you have nine or more cards in hand, you may play the exiled card without paying its mana cost. this.addAbility(new WiretappingTriggeredAbility()); diff --git a/Mage.Sets/src/mage/cards/w/WyllsReversal.java b/Mage.Sets/src/mage/cards/w/WyllsReversal.java index 06096468aca..3b46ec66e61 100644 --- a/Mage.Sets/src/mage/cards/w/WyllsReversal.java +++ b/Mage.Sets/src/mage/cards/w/WyllsReversal.java @@ -38,6 +38,7 @@ public final class WyllsReversal extends CardImpl { // 15+ | You may choose new targets for that spell or ability. Then copy it. You may choose new targets for the copy. this.getSpellAbility().addEffect(new WyllsReversalEffect()); this.getSpellAbility().addTarget(new TargetStackObject()); + this.getSpellAbility().addHint(GreatestPowerAmongControlledCreaturesValue.getHint()); } private WyllsReversal(final WyllsReversal card) { diff --git a/Mage.Sets/src/mage/cards/x/XandersPact.java b/Mage.Sets/src/mage/cards/x/XandersPact.java index 93c0f3377d6..1733c8a8590 100644 --- a/Mage.Sets/src/mage/cards/x/XandersPact.java +++ b/Mage.Sets/src/mage/cards/x/XandersPact.java @@ -75,7 +75,7 @@ class XandersPactExileEffect extends OneShotEffect { return false; } Set cards = game - .getOpponents(source.getControllerId()) + .getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/z/ZahurGlorysPast.java b/Mage.Sets/src/mage/cards/z/ZahurGlorysPast.java new file mode 100644 index 00000000000..eb230fa7177 --- /dev/null +++ b/Mage.Sets/src/mage/cards/z/ZahurGlorysPast.java @@ -0,0 +1,61 @@ +package mage.cards.z; + +import mage.MageInt; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.common.LimitedTimesPerTurnActivatedAbility; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.keyword.SurveilEffect; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.permanent.token.ZombieToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ZahurGlorysPast extends CardImpl { + + public ZahurGlorysPast(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.CAT); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // Sacrifice another creature: Surveil 1. Activate only once each turn. + this.addAbility(new LimitedTimesPerTurnActivatedAbility( + Zone.BATTLEFIELD, new SurveilEffect(1), + new SacrificeTargetCost(StaticFilters.FILTER_ANOTHER_CREATURE) + )); + + // Max speed -- Whenever a nontoken creature you control dies, create a tapped 2/2 black Zombie creature token. + this.addAbility(new MaxSpeedAbility(new DiesCreatureTriggeredAbility( + new CreateTokenEffect(new ZombieToken(), 1, true), + false, StaticFilters.FILTER_CONTROLLED_CREATURE_NON_TOKEN + ))); + } + + private ZahurGlorysPast(final ZahurGlorysPast card) { + super(card); + } + + @Override + public ZahurGlorysPast copy() { + return new ZahurGlorysPast(this); + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index 4bea67031a4..9e5c09d6335 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -4,15 +4,11 @@ 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 Aetherdrift extends ExpansionSet { - private static final List unfinished = Arrays.asList("Aether Syphon", "Amonkhet Raceway", "Avishkar Raceway", "Burnout Bashtronaut", "Embalmed Ascendant", "Endrider Catalyzer", "Endrider Spikespitter", "Far Fortune, End Boss", "Gas Guzzler", "Gastal Raider", "Gastal Thrillseeker", "Glitch Ghost Surveyor", "Goblin Surveyor", "Hazoret, Godseeker", "Hour of Victory", "Howlsquad Heavy", "Kickoff Celebrations", "Leonin Surveyor", "Lightwheel Enhancements", "Loxodon Surveyor", "Mendicant Core, Guidelight", "Momentum Breaker", "Muraganda Raceway", "Mutant Surveyor", "Nesting Bot", "Outpace Oblivion", "Perilous Snare", "Point the Way", "Pride of the Road", "Racers' Scoreboard", "Risen Necroregent", "Samut, the Driving Force", "Slick Imitator", "Starting Column", "Streaking Oilgorger", "Swiftwing Assailant", "The Speed Demon", "Vnwxt, Verbose Host", "Walking Sarcophagus", "Zahur, Glory's Past"); private static final Aetherdrift instance = new Aetherdrift(); public static Aetherdrift getInstance() { @@ -22,10 +18,11 @@ public final class Aetherdrift extends ExpansionSet { private Aetherdrift() { super("Aetherdrift", "DFT", ExpansionSet.buildDate(2025, 2, 14), SetType.EXPANSION); this.blockName = "Aetherdrift"; // for sorting in GUI - this.hasBasicLands = true; - this.hasBoosters = false; // temporary + + this.enablePlayBooster(Integer.MAX_VALUE); cards.add(new SetCardInfo("Aatchik, Emerald Radian", 187, Rarity.RARE, mage.cards.a.AatchikEmeraldRadian.class)); + cards.add(new SetCardInfo("Adrenaline Jockey", 112, Rarity.UNCOMMON, mage.cards.a.AdrenalineJockey.class)); cards.add(new SetCardInfo("Aether Syphon", 38, Rarity.UNCOMMON, mage.cards.a.AetherSyphon.class)); cards.add(new SetCardInfo("Aetherjacket", 230, Rarity.COMMON, mage.cards.a.Aetherjacket.class)); cards.add(new SetCardInfo("Afterburner Expert", 150, Rarity.RARE, mage.cards.a.AfterburnerExpert.class)); @@ -34,6 +31,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Alacrian Jaguar", 152, Rarity.COMMON, mage.cards.a.AlacrianJaguar.class)); cards.add(new SetCardInfo("Amonkhet Raceway", 248, Rarity.UNCOMMON, mage.cards.a.AmonkhetRaceway.class)); cards.add(new SetCardInfo("Apocalypse Runner", 188, Rarity.UNCOMMON, mage.cards.a.ApocalypseRunner.class)); + cards.add(new SetCardInfo("Autarch Mammoth", 153, Rarity.UNCOMMON, mage.cards.a.AutarchMammoth.class)); cards.add(new SetCardInfo("Avishkar Raceway", 249, Rarity.COMMON, mage.cards.a.AvishkarRaceway.class)); cards.add(new SetCardInfo("Back on Track", 76, Rarity.UNCOMMON, mage.cards.b.BackOnTrack.class)); cards.add(new SetCardInfo("Basri, Tomorrow's Champion", 3, Rarity.RARE, mage.cards.b.BasriTomorrowsChampion.class)); @@ -43,6 +41,10 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Bloodfell Caves", 251, Rarity.COMMON, mage.cards.b.BloodfellCaves.class)); cards.add(new SetCardInfo("Bloodghast", 77, Rarity.RARE, mage.cards.b.Bloodghast.class)); cards.add(new SetCardInfo("Blossoming Sands", 252, Rarity.COMMON, mage.cards.b.BlossomingSands.class)); + cards.add(new SetCardInfo("Boommobile", 113, Rarity.RARE, mage.cards.b.Boommobile.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Boommobile", 310, Rarity.RARE, mage.cards.b.Boommobile.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Boommobile", 454, Rarity.RARE, mage.cards.b.Boommobile.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Boommobile", 526, Rarity.RARE, mage.cards.b.Boommobile.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Boom Scholar", 189, Rarity.UNCOMMON, mage.cards.b.BoomScholar.class)); cards.add(new SetCardInfo("Boosted Sloop", 190, Rarity.UNCOMMON, mage.cards.b.BoostedSloop.class)); cards.add(new SetCardInfo("Bounce Off", 39, Rarity.COMMON, mage.cards.b.BounceOff.class)); @@ -52,17 +54,24 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Broadcast Rambler", 6, Rarity.COMMON, mage.cards.b.BroadcastRambler.class)); cards.add(new SetCardInfo("Broadside Barrage", 192, Rarity.UNCOMMON, mage.cards.b.BroadsideBarrage.class)); cards.add(new SetCardInfo("Broken Wings", 156, Rarity.COMMON, mage.cards.b.BrokenWings.class)); + cards.add(new SetCardInfo("Broodheart Engine", 193, Rarity.UNCOMMON, mage.cards.b.BroodheartEngine.class)); cards.add(new SetCardInfo("Bulwark Ox", 7, Rarity.RARE, mage.cards.b.BulwarkOx.class)); + cards.add(new SetCardInfo("Burner Rocket", 114, Rarity.COMMON, mage.cards.b.BurnerRocket.class)); cards.add(new SetCardInfo("Burnout Bashtronaut", 115, Rarity.RARE, mage.cards.b.BurnoutBashtronaut.class)); cards.add(new SetCardInfo("Caelorna, Coral Tyrant", 40, Rarity.UNCOMMON, mage.cards.c.CaelornaCoralTyrant.class)); cards.add(new SetCardInfo("Camera Launcher", 232, Rarity.COMMON, mage.cards.c.CameraLauncher.class)); cards.add(new SetCardInfo("Caradora, Heart of Alacria", 195, Rarity.RARE, mage.cards.c.CaradoraHeartOfAlacria.class)); + cards.add(new SetCardInfo("Carrion Cruiser", 78, Rarity.UNCOMMON, mage.cards.c.CarrionCruiser.class)); + cards.add(new SetCardInfo("Chandra, Spark Hunter", 116, Rarity.MYTHIC, mage.cards.c.ChandraSparkHunter.class)); + cards.add(new SetCardInfo("Chitin Gravestalker", 79, Rarity.COMMON, mage.cards.c.ChitinGravestalker.class)); cards.add(new SetCardInfo("Clamorous Ironclad", 117, Rarity.COMMON, mage.cards.c.ClamorousIronclad.class)); cards.add(new SetCardInfo("Cloudspire Captain", 9, Rarity.UNCOMMON, mage.cards.c.CloudspireCaptain.class)); + cards.add(new SetCardInfo("Cloudspire Coordinator", 196, Rarity.UNCOMMON, mage.cards.c.CloudspireCoordinator.class)); cards.add(new SetCardInfo("Collision Course", 10, Rarity.COMMON, mage.cards.c.CollisionCourse.class)); cards.add(new SetCardInfo("Count on Luck", 118, Rarity.RARE, mage.cards.c.CountOnLuck.class)); cards.add(new SetCardInfo("Country Roads", 253, Rarity.UNCOMMON, mage.cards.c.CountryRoads.class)); cards.add(new SetCardInfo("Crash and Burn", 119, Rarity.COMMON, mage.cards.c.CrashAndBurn.class)); + cards.add(new SetCardInfo("Cryptcaller Chariot", 80, Rarity.RARE, mage.cards.c.CryptcallerChariot.class)); cards.add(new SetCardInfo("Daretti, Rocketeer Engineer", 120, Rarity.RARE, mage.cards.d.DarettiRocketeerEngineer.class)); cards.add(new SetCardInfo("Daring Mechanic", 11, Rarity.COMMON, mage.cards.d.DaringMechanic.class)); cards.add(new SetCardInfo("Deathless Pilot", 82, Rarity.COMMON, mage.cards.d.DeathlessPilot.class)); @@ -81,13 +90,19 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Endrider Catalyzer", 124, Rarity.COMMON, mage.cards.e.EndriderCatalyzer.class)); cards.add(new SetCardInfo("Endrider Spikespitter", 125, Rarity.UNCOMMON, mage.cards.e.EndriderSpikespitter.class)); cards.add(new SetCardInfo("Engine Rat", 84, Rarity.COMMON, mage.cards.e.EngineRat.class)); + cards.add(new SetCardInfo("Fang Guardian", 162, Rarity.UNCOMMON, mage.cards.f.FangGuardian.class)); + cards.add(new SetCardInfo("Far Fortune, End Boss", 203, Rarity.RARE, mage.cards.f.FarFortuneEndBoss.class)); cards.add(new SetCardInfo("Fearless Swashbuckler", 204, Rarity.RARE, mage.cards.f.FearlessSwashbuckler.class)); + cards.add(new SetCardInfo("Flood the Engine", 42, Rarity.COMMON, mage.cards.f.FloodTheEngine.class)); cards.add(new SetCardInfo("Forest", 289, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Foul Roads", 255, Rarity.UNCOMMON, mage.cards.f.FoulRoads.class)); cards.add(new SetCardInfo("Fuel the Flames", 126, Rarity.UNCOMMON, mage.cards.f.FuelTheFlames.class)); cards.add(new SetCardInfo("Gallant Strike", 13, Rarity.UNCOMMON, mage.cards.g.GallantStrike.class)); cards.add(new SetCardInfo("Gas Guzzler", 85, Rarity.RARE, mage.cards.g.GasGuzzler.class)); + cards.add(new SetCardInfo("Gastal Blockbuster", 128, Rarity.COMMON, mage.cards.g.GastalBlockbuster.class)); + cards.add(new SetCardInfo("Gastal Raider", 86, Rarity.UNCOMMON, mage.cards.g.GastalRaider.class)); cards.add(new SetCardInfo("Gastal Thrillroller", 129, Rarity.RARE, mage.cards.g.GastalThrillroller.class)); + cards.add(new SetCardInfo("Gastal Thrillseeker", 205, Rarity.UNCOMMON, mage.cards.g.GastalThrillseeker.class)); cards.add(new SetCardInfo("Gearseeker Serpent", 43, Rarity.COMMON, mage.cards.g.GearseekerSerpent.class)); cards.add(new SetCardInfo("Gilded Ghoda", 130, Rarity.COMMON, mage.cards.g.GildedGhoda.class)); cards.add(new SetCardInfo("Glitch Ghost Surveyor", 44, Rarity.COMMON, mage.cards.g.GlitchGhostSurveyor.class)); @@ -96,12 +111,17 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Greasewrench Goblin", 132, Rarity.UNCOMMON, mage.cards.g.GreasewrenchGoblin.class)); cards.add(new SetCardInfo("Greenbelt Guardian", 164, Rarity.UNCOMMON, mage.cards.g.GreenbeltGuardian.class)); cards.add(new SetCardInfo("Grim Bauble", 88, Rarity.COMMON, mage.cards.g.GrimBauble.class)); + cards.add(new SetCardInfo("Grim Javelineer", 89, Rarity.COMMON, mage.cards.g.GrimJavelineer.class)); cards.add(new SetCardInfo("Guardian Sunmare", 15, Rarity.RARE, mage.cards.g.GuardianSunmare.class)); cards.add(new SetCardInfo("Guidelight Pathmaker", 206, Rarity.UNCOMMON, mage.cards.g.GuidelightPathmaker.class)); + cards.add(new SetCardInfo("Guidelight Synergist", 16, Rarity.UNCOMMON, mage.cards.g.GuidelightSynergist.class)); + cards.add(new SetCardInfo("Haunt the Network", 207, Rarity.UNCOMMON, mage.cards.h.HauntTheNetwork.class)); cards.add(new SetCardInfo("Haunted Hellride", 208, Rarity.UNCOMMON, mage.cards.h.HauntedHellride.class)); cards.add(new SetCardInfo("Hazard of the Dunes", 165, Rarity.COMMON, mage.cards.h.HazardOfTheDunes.class)); cards.add(new SetCardInfo("Hazoret, Godseeker", 133, Rarity.MYTHIC, mage.cards.h.HazoretGodseeker.class)); + cards.add(new SetCardInfo("Hour of Victory", 91, Rarity.UNCOMMON, mage.cards.h.HourOfVictory.class)); cards.add(new SetCardInfo("Howler's Heavy", 46, Rarity.COMMON, mage.cards.h.HowlersHeavy.class)); + cards.add(new SetCardInfo("Howlsquad Heavy", 134, Rarity.RARE, mage.cards.h.HowlsquadHeavy.class)); cards.add(new SetCardInfo("Hulldrifter", 47, Rarity.COMMON, mage.cards.h.Hulldrifter.class)); cards.add(new SetCardInfo("Interface Ace", 17, Rarity.COMMON, mage.cards.i.InterfaceAce.class)); cards.add(new SetCardInfo("Intimidation Tactics", 92, Rarity.UNCOMMON, mage.cards.i.IntimidationTactics.class)); @@ -110,12 +130,19 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Jungle Hollow", 256, Rarity.COMMON, mage.cards.j.JungleHollow.class)); cards.add(new SetCardInfo("Kalakscion, Hunger Tyrant", 93, Rarity.UNCOMMON, mage.cards.k.KalakscionHungerTyrant.class)); cards.add(new SetCardInfo("Keen Buccaneer", 48, Rarity.COMMON, mage.cards.k.KeenBuccaneer.class)); + cards.add(new SetCardInfo("Kickoff Celebrations", 135, Rarity.COMMON, mage.cards.k.KickoffCelebrations.class)); cards.add(new SetCardInfo("Lagorin, Soul of Alacria", 211, Rarity.UNCOMMON, mage.cards.l.LagorinSoulOfAlacria.class)); cards.add(new SetCardInfo("Leonin Surveyor", 18, Rarity.COMMON, mage.cards.l.LeoninSurveyor.class)); + cards.add(new SetCardInfo("Lifecraft Engine", 234, Rarity.RARE, mage.cards.l.LifecraftEngine.class)); cards.add(new SetCardInfo("Lightning Strike", 136, Rarity.COMMON, mage.cards.l.LightningStrike.class)); cards.add(new SetCardInfo("Lightshield Parry", 19, Rarity.COMMON, mage.cards.l.LightshieldParry.class)); + cards.add(new SetCardInfo("Lightwheel Enhancements", 20, Rarity.COMMON, mage.cards.l.LightwheelEnhancements.class)); cards.add(new SetCardInfo("Locust Spray", 95, Rarity.UNCOMMON, mage.cards.l.LocustSpray.class)); + cards.add(new SetCardInfo("Lotusguard Disciple", 21, Rarity.COMMON, mage.cards.l.LotusguardDisciple.class)); cards.add(new SetCardInfo("Loxodon Surveyor", 167, Rarity.COMMON, mage.cards.l.LoxodonSurveyor.class)); + cards.add(new SetCardInfo("Lumbering Worldwagon", 168, Rarity.RARE, mage.cards.l.LumberingWorldwagon.class)); + cards.add(new SetCardInfo("Magmakin Artillerist", 137, Rarity.COMMON, mage.cards.m.MagmakinArtillerist.class)); + cards.add(new SetCardInfo("Marauding Mako", 138, Rarity.UNCOMMON, mage.cards.m.MaraudingMako.class)); cards.add(new SetCardInfo("Marketback Walker", 235, Rarity.RARE, mage.cards.m.MarketbackWalker.class)); cards.add(new SetCardInfo("Marshals' Pathcruiser", 236, Rarity.UNCOMMON, mage.cards.m.MarshalsPathcruiser.class)); cards.add(new SetCardInfo("Maximum Overdrive", 96, Rarity.COMMON, mage.cards.m.MaximumOverdrive.class)); @@ -126,16 +153,25 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Molt Tender", 171, Rarity.UNCOMMON, mage.cards.m.MoltTender.class)); cards.add(new SetCardInfo("Monument to Endurance", 237, Rarity.RARE, mage.cards.m.MonumentToEndurance.class)); cards.add(new SetCardInfo("Mountain", 286, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mu Yanling, Wind Rider", 52, Rarity.MYTHIC, mage.cards.m.MuYanlingWindRider.class)); cards.add(new SetCardInfo("Muraganda Raceway", 257, Rarity.RARE, mage.cards.m.MuragandaRaceway.class)); cards.add(new SetCardInfo("Mutant Surveyor", 98, Rarity.COMMON, mage.cards.m.MutantSurveyor.class)); cards.add(new SetCardInfo("Nesting Bot", 22, Rarity.UNCOMMON, mage.cards.n.NestingBot.class)); cards.add(new SetCardInfo("Night Market", 258, Rarity.COMMON, mage.cards.n.NightMarket.class)); cards.add(new SetCardInfo("Nimble Thopterist", 53, Rarity.COMMON, mage.cards.n.NimbleThopterist.class)); + cards.add(new SetCardInfo("Ooze Patrol", 172, Rarity.UNCOMMON, mage.cards.o.OozePatrol.class)); + cards.add(new SetCardInfo("Outpace Oblivion", 139, Rarity.UNCOMMON, mage.cards.o.OutpaceOblivion.class)); + cards.add(new SetCardInfo("Oviya, Automech Artisan", 173, Rarity.RARE, mage.cards.o.OviyaAutomechArtisan.class)); cards.add(new SetCardInfo("Pacesetter Paragon", 140, Rarity.UNCOMMON, mage.cards.p.PacesetterParagon.class)); + cards.add(new SetCardInfo("Pactdoll Terror", 99, Rarity.COMMON, mage.cards.p.PactdollTerror.class)); cards.add(new SetCardInfo("Pedal to the Metal", 141, Rarity.COMMON, mage.cards.p.PedalToTheMetal.class)); + cards.add(new SetCardInfo("Perilous Snare", 23, Rarity.RARE, mage.cards.p.PerilousSnare.class)); + cards.add(new SetCardInfo("Pit Automaton", 238, Rarity.UNCOMMON, mage.cards.p.PitAutomaton.class)); cards.add(new SetCardInfo("Plains", 272, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Plow Through", 174, Rarity.UNCOMMON, mage.cards.p.PlowThrough.class)); + cards.add(new SetCardInfo("Point the Way", 175, Rarity.UNCOMMON, mage.cards.p.PointTheWay.class)); cards.add(new SetCardInfo("Possession Engine", 54, Rarity.RARE, mage.cards.p.PossessionEngine.class)); + cards.add(new SetCardInfo("Pothole Mole", 176, Rarity.COMMON, mage.cards.p.PotholeMole.class)); cards.add(new SetCardInfo("Pride of the Road", 24, Rarity.UNCOMMON, mage.cards.p.PrideOfTheRoad.class)); cards.add(new SetCardInfo("Prowcatcher Specialist", 142, Rarity.COMMON, mage.cards.p.ProwcatcherSpecialist.class)); cards.add(new SetCardInfo("Pyrewood Gearhulk", 216, Rarity.MYTHIC, mage.cards.p.PyrewoodGearhulk.class)); @@ -143,31 +179,44 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Racers' Scoreboard", 239, Rarity.UNCOMMON, mage.cards.r.RacersScoreboard.class)); cards.add(new SetCardInfo("Rangers' Aetherhive", 217, Rarity.UNCOMMON, mage.cards.r.RangersAetherhive.class)); cards.add(new SetCardInfo("Rangers' Refueler", 55, Rarity.UNCOMMON, mage.cards.r.RangersRefueler.class)); + cards.add(new SetCardInfo("Reckless Velocitaur", 144, Rarity.UNCOMMON, mage.cards.r.RecklessVelocitaur.class)); + cards.add(new SetCardInfo("Redshift, Rocketeer Chief", 218, Rarity.RARE, mage.cards.r.RedshiftRocketeerChief.class)); cards.add(new SetCardInfo("Reef Roads", 259, Rarity.UNCOMMON, mage.cards.r.ReefRoads.class)); cards.add(new SetCardInfo("Regal Imperiosaur", 177, Rarity.RARE, mage.cards.r.RegalImperiosaur.class)); + cards.add(new SetCardInfo("Repurposing Bay", 56, Rarity.RARE, mage.cards.r.RepurposingBay.class)); cards.add(new SetCardInfo("Ride's End", 25, Rarity.COMMON, mage.cards.r.RidesEnd.class)); cards.add(new SetCardInfo("Ripclaw Wrangler", 101, Rarity.COMMON, mage.cards.r.RipclawWrangler.class)); + cards.add(new SetCardInfo("Rise from the Wreck", 178, Rarity.UNCOMMON, mage.cards.r.RiseFromTheWreck.class)); cards.add(new SetCardInfo("Risen Necroregent", 102, Rarity.UNCOMMON, mage.cards.r.RisenNecroregent.class)); cards.add(new SetCardInfo("Risky Shortcut", 103, Rarity.COMMON, mage.cards.r.RiskyShortcut.class)); cards.add(new SetCardInfo("Riverpyre Verge", 260, Rarity.RARE, mage.cards.r.RiverpyreVerge.class)); + cards.add(new SetCardInfo("Road Rage", 145, Rarity.UNCOMMON, mage.cards.r.RoadRage.class)); cards.add(new SetCardInfo("Roadside Assistance", 26, Rarity.UNCOMMON, mage.cards.r.RoadsideAssistance.class)); cards.add(new SetCardInfo("Roadside Blowout", 58, Rarity.UNCOMMON, mage.cards.r.RoadsideBlowout.class)); cards.add(new SetCardInfo("Rocketeer Boostbuggy", 220, Rarity.UNCOMMON, mage.cards.r.RocketeerBoostbuggy.class)); cards.add(new SetCardInfo("Rocky Roads", 261, Rarity.UNCOMMON, mage.cards.r.RockyRoads.class)); cards.add(new SetCardInfo("Rover Blades", 241, Rarity.UNCOMMON, mage.cards.r.RoverBlades.class)); cards.add(new SetCardInfo("Rugged Highlands", 262, Rarity.COMMON, mage.cards.r.RuggedHighlands.class)); + cards.add(new SetCardInfo("Run Over", 179, Rarity.COMMON, mage.cards.r.RunOver.class)); cards.add(new SetCardInfo("Sab-Sunen, Luxa Embodied", 221, Rarity.MYTHIC, mage.cards.s.SabSunenLuxaEmbodied.class)); + cards.add(new SetCardInfo("Sabotage Strategist", 59, Rarity.UNCOMMON, mage.cards.s.SabotageStrategist.class)); cards.add(new SetCardInfo("Salvation Engine", 27, Rarity.MYTHIC, mage.cards.s.SalvationEngine.class)); + cards.add(new SetCardInfo("Samut, the Driving Force", 222, Rarity.RARE, mage.cards.s.SamutTheDrivingForce.class)); cards.add(new SetCardInfo("Scoured Barrens", 263, Rarity.COMMON, mage.cards.s.ScouredBarrens.class)); + cards.add(new SetCardInfo("Scrap Compactor", 242, Rarity.COMMON, mage.cards.s.ScrapCompactor.class)); + cards.add(new SetCardInfo("Scrounging Skyray", 60, Rarity.UNCOMMON, mage.cards.s.ScroungingSkyray.class)); cards.add(new SetCardInfo("Shefet Archfiend", 104, Rarity.UNCOMMON, mage.cards.s.ShefetArchfiend.class)); cards.add(new SetCardInfo("Silken Strength", 180, Rarity.COMMON, mage.cards.s.SilkenStrength.class)); cards.add(new SetCardInfo("Skybox Ferry", 243, Rarity.COMMON, mage.cards.s.SkyboxFerry.class)); cards.add(new SetCardInfo("Skycrash", 146, Rarity.UNCOMMON, mage.cards.s.Skycrash.class)); cards.add(new SetCardInfo("Skystreak Engineer", 61, Rarity.COMMON, mage.cards.s.SkystreakEngineer.class)); + cards.add(new SetCardInfo("Slick Imitator", 62, Rarity.UNCOMMON, mage.cards.s.SlickImitator.class)); cards.add(new SetCardInfo("Spectral Interference", 63, Rarity.COMMON, mage.cards.s.SpectralInterference.class)); cards.add(new SetCardInfo("Spell Pierce", 64, Rarity.UNCOMMON, mage.cards.s.SpellPierce.class)); + cards.add(new SetCardInfo("Spikeshell Harrier", 65, Rarity.UNCOMMON, mage.cards.s.SpikeshellHarrier.class)); cards.add(new SetCardInfo("Spin Out", 106, Rarity.COMMON, mage.cards.s.SpinOut.class)); cards.add(new SetCardInfo("Spotcycle Scouter", 30, Rarity.COMMON, mage.cards.s.SpotcycleScouter.class)); + cards.add(new SetCardInfo("Stall Out", 66, Rarity.COMMON, mage.cards.s.StallOut.class)); cards.add(new SetCardInfo("Stampeding Scurryfoot", 181, Rarity.COMMON, mage.cards.s.StampedingScurryfoot.class)); cards.add(new SetCardInfo("Starting Column", 244, Rarity.COMMON, mage.cards.s.StartingColumn.class)); cards.add(new SetCardInfo("Stock Up", 67, Rarity.UNCOMMON, mage.cards.s.StockUp.class)); @@ -179,13 +228,18 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Swiftwing Assailant", 32, Rarity.COMMON, mage.cards.s.SwiftwingAssailant.class)); cards.add(new SetCardInfo("Syphon Fuel", 108, Rarity.COMMON, mage.cards.s.SyphonFuel.class)); cards.add(new SetCardInfo("Terrian, World Tyrant", 182, Rarity.UNCOMMON, mage.cards.t.TerrianWorldTyrant.class)); + cards.add(new SetCardInfo("The Aetherspark", 231, Rarity.MYTHIC, mage.cards.t.TheAetherspark.class)); cards.add(new SetCardInfo("The Last Ride", 94, Rarity.MYTHIC, mage.cards.t.TheLastRide.class)); cards.add(new SetCardInfo("The Speed Demon", 105, Rarity.MYTHIC, mage.cards.t.TheSpeedDemon.class)); cards.add(new SetCardInfo("Thopter Fabricator", 68, Rarity.RARE, mage.cards.t.ThopterFabricator.class)); cards.add(new SetCardInfo("Thornwood Falls", 266, Rarity.COMMON, mage.cards.t.ThornwoodFalls.class)); + cards.add(new SetCardInfo("Thunderhead Gunner", 148, Rarity.COMMON, mage.cards.t.ThunderheadGunner.class)); cards.add(new SetCardInfo("Thundering Broodwagon", 225, Rarity.UNCOMMON, mage.cards.t.ThunderingBroodwagon.class)); + cards.add(new SetCardInfo("Ticket Tortoise", 245, Rarity.COMMON, mage.cards.t.TicketTortoise.class)); + cards.add(new SetCardInfo("Trade the Helm", 69, Rarity.UNCOMMON, mage.cards.t.TradeTheHelm.class)); cards.add(new SetCardInfo("Tranquil Cove", 267, Rarity.COMMON, mage.cards.t.TranquilCove.class)); cards.add(new SetCardInfo("Transit Mage", 70, Rarity.UNCOMMON, mage.cards.t.TransitMage.class)); + cards.add(new SetCardInfo("Trip Up", 71, Rarity.COMMON, mage.cards.t.TripUp.class)); cards.add(new SetCardInfo("Tyrox, Saurid Tyrant", 149, Rarity.UNCOMMON, mage.cards.t.TyroxSauridTyrant.class)); cards.add(new SetCardInfo("Unstoppable Plan", 72, Rarity.RARE, mage.cards.u.UnstoppablePlan.class)); cards.add(new SetCardInfo("Unswerving Sloth", 34, Rarity.UNCOMMON, mage.cards.u.UnswervingSloth.class)); @@ -193,18 +247,20 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Veloheart Bike", 184, Rarity.COMMON, mage.cards.v.VeloheartBike.class)); cards.add(new SetCardInfo("Venomsac Lagac", 185, Rarity.COMMON, mage.cards.v.VenomsacLagac.class)); cards.add(new SetCardInfo("Veteran Beastrider", 226, Rarity.UNCOMMON, mage.cards.v.VeteranBeastrider.class)); + cards.add(new SetCardInfo("Vnwxt, Verbose Host", 73, Rarity.RARE, mage.cards.v.VnwxtVerboseHost.class)); cards.add(new SetCardInfo("Voyage Home", 227, Rarity.UNCOMMON, mage.cards.v.VoyageHome.class)); + cards.add(new SetCardInfo("Voyager Glidecar", 36, Rarity.RARE, mage.cards.v.VoyagerGlidecar.class)); cards.add(new SetCardInfo("Voyager Quickwelder", 37, Rarity.COMMON, mage.cards.v.VoyagerQuickwelder.class)); cards.add(new SetCardInfo("Walking Sarcophagus", 246, Rarity.COMMON, mage.cards.w.WalkingSarcophagus.class)); cards.add(new SetCardInfo("Wastewood Verge", 268, Rarity.RARE, mage.cards.w.WastewoodVerge.class)); cards.add(new SetCardInfo("Waxen Shapethief", 74, Rarity.RARE, mage.cards.w.WaxenShapethief.class)); + cards.add(new SetCardInfo("Webstrike Elite", 186, Rarity.RARE, mage.cards.w.WebstrikeElite.class)); cards.add(new SetCardInfo("Wild Roads", 269, Rarity.UNCOMMON, mage.cards.w.WildRoads.class)); cards.add(new SetCardInfo("Willowrush Verge", 270, Rarity.RARE, mage.cards.w.WillowrushVerge.class)); cards.add(new SetCardInfo("Wind-Scarred Crag", 271, Rarity.COMMON, mage.cards.w.WindScarredCrag.class)); cards.add(new SetCardInfo("Wreck Remover", 247, Rarity.COMMON, mage.cards.w.WreckRemover.class)); cards.add(new SetCardInfo("Wreckage Wickerfolk", 110, Rarity.COMMON, mage.cards.w.WreckageWickerfolk.class)); cards.add(new SetCardInfo("Wretched Doll", 111, Rarity.UNCOMMON, mage.cards.w.WretchedDoll.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); + cards.add(new SetCardInfo("Zahur, Glory's Past", 229, Rarity.RARE, mage.cards.z.ZahurGlorysPast.class)); } } diff --git a/Mage.Sets/src/mage/sets/AetherdriftCommander.java b/Mage.Sets/src/mage/sets/AetherdriftCommander.java index 3444bbd6e67..24dcf213775 100644 --- a/Mage.Sets/src/mage/sets/AetherdriftCommander.java +++ b/Mage.Sets/src/mage/sets/AetherdriftCommander.java @@ -20,8 +20,12 @@ public final class AetherdriftCommander extends ExpansionSet { this.hasBasicLands = false; cards.add(new SetCardInfo("Academy Ruins", 58, Rarity.RARE, mage.cards.a.AcademyRuins.class)); + cards.add(new SetCardInfo("Accursed Duneyard", 20, Rarity.RARE, mage.cards.a.AccursedDuneyard.class)); + cards.add(new SetCardInfo("Adaptive Omnitool", 16, Rarity.RARE, mage.cards.a.AdaptiveOmnitool.class)); cards.add(new SetCardInfo("Adarkar Wastes", 144, Rarity.RARE, mage.cards.a.AdarkarWastes.class)); cards.add(new SetCardInfo("Aether Hub", 145, Rarity.UNCOMMON, mage.cards.a.AetherHub.class)); + cards.add(new SetCardInfo("Aetherflux Conduit", 17, Rarity.RARE, mage.cards.a.AetherfluxConduit.class)); + cards.add(new SetCardInfo("Aetheric Amplifier", 18, Rarity.RARE, mage.cards.a.AethericAmplifier.class)); cards.add(new SetCardInfo("Aethersquall Ancient", 68, Rarity.RARE, mage.cards.a.AethersquallAncient.class)); cards.add(new SetCardInfo("Aethertide Whale", 69, Rarity.RARE, mage.cards.a.AethertideWhale.class)); cards.add(new SetCardInfo("Aetherwind Basker", 107, Rarity.MYTHIC, mage.cards.a.AetherwindBasker.class)); @@ -115,6 +119,7 @@ public final class AetherdriftCommander extends ExpansionSet { cards.add(new SetCardInfo("Panharmonicon", 135, Rarity.RARE, mage.cards.p.Panharmonicon.class)); cards.add(new SetCardInfo("Path of Ancestry", 61, Rarity.COMMON, mage.cards.p.PathOfAncestry.class)); cards.add(new SetCardInfo("Peema Aether-Seer", 113, Rarity.UNCOMMON, mage.cards.p.PeemaAetherSeer.class)); + cards.add(new SetCardInfo("Peema Trailblazer", 14, Rarity.RARE, mage.cards.p.PeemaTrailblazer.class)); cards.add(new SetCardInfo("Pia and Kiran Nalaar", 105, Rarity.RARE, mage.cards.p.PiaAndKiranNalaar.class)); cards.add(new SetCardInfo("Plague Belcher", 97, Rarity.RARE, mage.cards.p.PlagueBelcher.class)); cards.add(new SetCardInfo("Prairie Stream", 167, Rarity.RARE, mage.cards.p.PrairieStream.class)); @@ -123,6 +128,7 @@ public final class AetherdriftCommander extends ExpansionSet { cards.add(new SetCardInfo("Reality Shift", 39, Rarity.UNCOMMON, mage.cards.r.RealityShift.class)); cards.add(new SetCardInfo("Reckless Fireweaver", 106, Rarity.COMMON, mage.cards.r.RecklessFireweaver.class)); cards.add(new SetCardInfo("Retrofitter Foundry", 136, Rarity.RARE, mage.cards.r.RetrofitterFoundry.class)); + cards.add(new SetCardInfo("Rhet-Tomb Mystic", 10, Rarity.RARE, mage.cards.r.RhetTombMystic.class)); cards.add(new SetCardInfo("Rogue Refiner", 118, Rarity.UNCOMMON, mage.cards.r.RogueRefiner.class)); cards.add(new SetCardInfo("Rootbound Crag", 168, Rarity.RARE, mage.cards.r.RootboundCrag.class)); cards.add(new SetCardInfo("Rot Hulk", 98, Rarity.MYTHIC, mage.cards.r.RotHulk.class)); diff --git a/Mage.Sets/src/mage/sets/BloomburrowCommander.java b/Mage.Sets/src/mage/sets/BloomburrowCommander.java index daf45fc4390..cd669f6f14a 100644 --- a/Mage.Sets/src/mage/sets/BloomburrowCommander.java +++ b/Mage.Sets/src/mage/sets/BloomburrowCommander.java @@ -300,6 +300,8 @@ public final class BloomburrowCommander extends ExpansionSet { cards.add(new SetCardInfo("Tetsuko Umezawa, Fugitive", 177, Rarity.UNCOMMON, mage.cards.t.TetsukoUmezawaFugitive.class)); cards.add(new SetCardInfo("The Gitrog Monster", 88, Rarity.MYTHIC, mage.cards.t.TheGitrogMonster.class)); cards.add(new SetCardInfo("The Odd Acorn Gang", 7, Rarity.MYTHIC, mage.cards.t.TheOddAcornGang.class)); + cards.add(new SetCardInfo("Thickest in the Thicket", 34, Rarity.RARE, mage.cards.t.ThickestInTheThicket.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Thickest in the Thicket", 67, Rarity.RARE, mage.cards.t.ThickestInTheThicket.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Thopter Engineer", 204, Rarity.UNCOMMON, mage.cards.t.ThopterEngineer.class)); cards.add(new SetCardInfo("Thought Vessel", 289, Rarity.COMMON, mage.cards.t.ThoughtVessel.class)); cards.add(new SetCardInfo("Thran Dynamo", 290, Rarity.UNCOMMON, mage.cards.t.ThranDynamo.class)); diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index 2c095c0831a..a9ff63f658b 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -920,13 +920,13 @@ public final class DoctorWho extends ExpansionSet { cards.add(new SetCardInfo("The Fifth Doctor", 413, Rarity.RARE, mage.cards.t.TheFifthDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Fifth Doctor", 556, Rarity.RARE, mage.cards.t.TheFifthDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Fifth Doctor", 732, Rarity.RARE, mage.cards.t.TheFifthDoctor.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The First Doctor", "552z", Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The First Doctor", 1005, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The First Doctor", 1143, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The First Doctor", 128, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The First Doctor", 414, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The First Doctor", 552, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The First Doctor", 733, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The First Doctor", "552z", Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The First Doctor", 1005, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The First Doctor", 1143, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The First Doctor", 128, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The First Doctor", 414, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The First Doctor", 552, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The First Doctor", 733, Rarity.RARE, mage.cards.t.TheFirstDoctor.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("The Five Doctors", 101, Rarity.RARE, mage.cards.t.TheFiveDoctors.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("The Five Doctors", 394, Rarity.RARE, mage.cards.t.TheFiveDoctors.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("The Five Doctors", 706, Rarity.RARE, mage.cards.t.TheFiveDoctors.class, NON_FULL_USE_VARIOUS)); @@ -1180,10 +1180,10 @@ public final class DoctorWho extends ExpansionSet { cards.add(new SetCardInfo("Weeping Angel", 773, Rarity.RARE, mage.cards.w.WeepingAngel.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Wibbly-wobbly, Timey-wimey", 62, Rarity.COMMON, mage.cards.w.WibblyWobblyTimeyWimey.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Wibbly-wobbly, Timey-wimey", 667, Rarity.COMMON, mage.cards.w.WibblyWobblyTimeyWimey.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Wilfred Mott", 32, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Wilfred Mott", 350, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Wilfred Mott", 637, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Wilfred Mott", 941, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Wilfred Mott", 32, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Wilfred Mott", 350, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Wilfred Mott", 637, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Wilfred Mott", 941, Rarity.RARE, mage.cards.w.WilfredMott.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Wound Reflection", 1062, Rarity.RARE, mage.cards.w.WoundReflection.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Wound Reflection", 223, Rarity.RARE, mage.cards.w.WoundReflection.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Wound Reflection", 471, Rarity.RARE, mage.cards.w.WoundReflection.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java index d1b61be1f1e..1e66450e904 100644 --- a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java +++ b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java @@ -180,8 +180,8 @@ public final class DuskmournHouseOfHorror extends ExpansionSet { cards.add(new SetCardInfo("Hand That Feeds", 139, Rarity.COMMON, mage.cards.h.HandThatFeeds.class)); cards.add(new SetCardInfo("Hardened Escort", 16, Rarity.COMMON, mage.cards.h.HardenedEscort.class)); cards.add(new SetCardInfo("Haunted Screen", 250, Rarity.UNCOMMON, mage.cards.h.HauntedScreen.class)); - //cards.add(new SetCardInfo("Hauntwoods Shrieker", 182, Rarity.MYTHIC, mage.cards.h.HauntwoodsShrieker.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Hauntwoods Shrieker", 349, Rarity.MYTHIC, mage.cards.h.HauntwoodsShrieker.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Hauntwoods Shrieker", 182, Rarity.MYTHIC, mage.cards.h.HauntwoodsShrieker.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Hauntwoods Shrieker", 349, Rarity.MYTHIC, mage.cards.h.HauntwoodsShrieker.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Hedge Shredder", 183, Rarity.RARE, mage.cards.h.HedgeShredder.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Hedge Shredder", 320, Rarity.RARE, mage.cards.h.HedgeShredder.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Horrid Vigor", 184, Rarity.COMMON, mage.cards.h.HorridVigor.class)); diff --git a/Mage.Sets/src/mage/sets/Fallout.java b/Mage.Sets/src/mage/sets/Fallout.java index ac626b3e735..8a977e570c8 100644 --- a/Mage.Sets/src/mage/sets/Fallout.java +++ b/Mage.Sets/src/mage/sets/Fallout.java @@ -839,10 +839,10 @@ public final class Fallout extends ExpansionSet { //cards.add(new SetCardInfo("Strong, the Brutish Thespian", 612, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Strong, the Brutish Thespian", 84, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Strong, the Brutish Thespian", 931, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Struggle for Project Purity", 380, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Struggle for Project Purity", 39, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Struggle for Project Purity", 567, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Struggle for Project Purity", 908, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Struggle for Project Purity", 380, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Struggle for Project Purity", 39, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Struggle for Project Purity", 567, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Struggle for Project Purity", 908, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sulfur Falls", 1040, Rarity.RARE, mage.cards.s.SulfurFalls.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sulfur Falls", 294, Rarity.RARE, mage.cards.s.SulfurFalls.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sulfur Falls", 512, Rarity.RARE, mage.cards.s.SulfurFalls.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Sets/src/mage/sets/GameDayPromos.java b/Mage.Sets/src/mage/sets/GameDayPromos.java new file mode 100644 index 00000000000..6792560c183 --- /dev/null +++ b/Mage.Sets/src/mage/sets/GameDayPromos.java @@ -0,0 +1,33 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/gdy + */ +public final class GameDayPromos extends ExpansionSet { + + private static final GameDayPromos instance = new GameDayPromos(); + + public static GameDayPromos getInstance() { + return instance; + } + + private GameDayPromos() { + super("Game Day Promos", "GDY", ExpansionSet.buildDate(2022, 4, 8), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("All-Seeing Arbiter", 3, Rarity.MYTHIC, mage.cards.a.AllSeeingArbiter.class)); + cards.add(new SetCardInfo("Braids, Arisen Nightmare", 8, Rarity.RARE, mage.cards.b.BraidsArisenNightmare.class)); + cards.add(new SetCardInfo("Power Word Kill", 1, Rarity.RARE, mage.cards.p.PowerWordKill.class)); + cards.add(new SetCardInfo("Recruitment Officer", 7, Rarity.RARE, mage.cards.r.RecruitmentOfficer.class)); + cards.add(new SetCardInfo("Shivan Devastator", 6, Rarity.MYTHIC, mage.cards.s.ShivanDevastator.class)); + cards.add(new SetCardInfo("Skyclave Apparition", 2, Rarity.RARE, mage.cards.s.SkyclaveApparition.class)); + cards.add(new SetCardInfo("Surge Engine", 9, Rarity.MYTHIC, mage.cards.s.SurgeEngine.class)); + cards.add(new SetCardInfo("Touch the Spirit Realm", 4, Rarity.RARE, mage.cards.t.TouchTheSpiritRealm.class)); + cards.add(new SetCardInfo("Workshop Warchief", 5, Rarity.RARE, mage.cards.w.WorkshopWarchief.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/JudgeGiftCards2020.java b/Mage.Sets/src/mage/sets/JudgeGiftCards2020.java index d409a1c4709..1227997eb3f 100644 --- a/Mage.Sets/src/mage/sets/JudgeGiftCards2020.java +++ b/Mage.Sets/src/mage/sets/JudgeGiftCards2020.java @@ -21,10 +21,14 @@ public class JudgeGiftCards2020 extends ExpansionSet { this.hasBasicLands = false; cards.add(new SetCardInfo("Arena Rector", 1, Rarity.RARE, mage.cards.a.ArenaRector.class)); + cards.add(new SetCardInfo("Birthing Pod", 7, Rarity.RARE, mage.cards.b.BirthingPod.class)); cards.add(new SetCardInfo("Demonic Tutor", 4, Rarity.RARE, mage.cards.d.DemonicTutor.class)); cards.add(new SetCardInfo("Enlightened Tutor", 2, Rarity.RARE, mage.cards.e.EnlightenedTutor.class)); + cards.add(new SetCardInfo("Eye of Ugin", 10, Rarity.RARE, mage.cards.e.EyeOfUgin.class)); cards.add(new SetCardInfo("Gamble", 6, Rarity.RARE, mage.cards.g.Gamble.class)); + cards.add(new SetCardInfo("Infernal Tutor", 5, Rarity.RARE, mage.cards.i.InfernalTutor.class)); cards.add(new SetCardInfo("Spellseeker", 3, Rarity.RARE, mage.cards.s.Spellseeker.class)); + cards.add(new SetCardInfo("Sterling Grove", 9, Rarity.RARE, mage.cards.s.SterlingGrove.class)); cards.add(new SetCardInfo("Sylvan Tutor", 8, Rarity.RARE, mage.cards.s.SylvanTutor.class)); } } diff --git a/Mage.Sets/src/mage/sets/JudgeGiftCards2021.java b/Mage.Sets/src/mage/sets/JudgeGiftCards2021.java new file mode 100644 index 00000000000..efe4cd6675d --- /dev/null +++ b/Mage.Sets/src/mage/sets/JudgeGiftCards2021.java @@ -0,0 +1,35 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/pj21 + */ +public class JudgeGiftCards2021 extends ExpansionSet { + + private static final JudgeGiftCards2021 instance = new JudgeGiftCards2021(); + + public static JudgeGiftCards2021 getInstance() { + return instance; + } + + private JudgeGiftCards2021() { + super("Judge Gift Cards 2021", "PJ21", ExpansionSet.buildDate(2021, 1, 1), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Edgar Markov", 3, Rarity.MYTHIC, mage.cards.e.EdgarMarkov.class)); + cards.add(new SetCardInfo("Ezuri, Claw of Progress", 4, Rarity.MYTHIC, mage.cards.e.EzuriClawOfProgress.class)); + cards.add(new SetCardInfo("Grand Arbiter Augustin IV", 6, Rarity.RARE, mage.cards.g.GrandArbiterAugustinIV.class)); + cards.add(new SetCardInfo("Karlov of the Ghost Council", 7, Rarity.MYTHIC, mage.cards.k.KarlovOfTheGhostCouncil.class)); + cards.add(new SetCardInfo("K'rrik, Son of Yawgmoth", 2, Rarity.RARE, mage.cards.k.KrrikSonOfYawgmoth.class)); + cards.add(new SetCardInfo("Mizzix of the Izmagnus", 8, Rarity.MYTHIC, mage.cards.m.MizzixOfTheIzmagnus.class)); + cards.add(new SetCardInfo("Morophon, the Boundless", 1, Rarity.MYTHIC, mage.cards.m.MorophonTheBoundless.class)); + cards.add(new SetCardInfo("Nicol Bolas, the Arisen", 9, Rarity.MYTHIC, mage.cards.n.NicolBolasTheArisen.class)); + cards.add(new SetCardInfo("Nicol Bolas, the Ravager", 9, Rarity.MYTHIC, mage.cards.n.NicolBolasTheRavager.class)); + cards.add(new SetCardInfo("The Gitrog Monster", 5, Rarity.MYTHIC, mage.cards.t.TheGitrogMonster.class)); + cards.add(new SetCardInfo("Zacama, Primal Calamity", 10, Rarity.MYTHIC, mage.cards.z.ZacamaPrimalCalamity.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/JudgeGiftCards2022.java b/Mage.Sets/src/mage/sets/JudgeGiftCards2022.java new file mode 100644 index 00000000000..75275088946 --- /dev/null +++ b/Mage.Sets/src/mage/sets/JudgeGiftCards2022.java @@ -0,0 +1,35 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/p22 + */ +public class JudgeGiftCards2022 extends ExpansionSet { + + private static final JudgeGiftCards2022 instance = new JudgeGiftCards2022(); + + public static JudgeGiftCards2022 getInstance() { + return instance; + } + + private JudgeGiftCards2022() { + super("Judge Gift Cards 2022", "P22", ExpansionSet.buildDate(2022, 1, 1), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Animate Dead", 7, Rarity.RARE, mage.cards.a.AnimateDead.class)); + cards.add(new SetCardInfo("Greater Auramancy", 1, Rarity.RARE, mage.cards.g.GreaterAuramancy.class)); + cards.add(new SetCardInfo("Growing Rites of Itlimoc", 10, Rarity.RARE, mage.cards.g.GrowingRitesOfItlimoc.class)); + cards.add(new SetCardInfo("Itlimoc, Cradle of the Sun", 10, Rarity.RARE, mage.cards.i.ItlimocCradleOfTheSun.class)); + cards.add(new SetCardInfo("No Mercy", 9, Rarity.RARE, mage.cards.n.NoMercy.class)); + cards.add(new SetCardInfo("Omniscience", 2, Rarity.RARE, mage.cards.o.Omniscience.class)); + cards.add(new SetCardInfo("Parallel Lives", 3, Rarity.RARE, mage.cards.p.ParallelLives.class)); + cards.add(new SetCardInfo("Purphoros, God of the Forge", 8, Rarity.RARE, mage.cards.p.PurphorosGodOfTheForge.class)); + cards.add(new SetCardInfo("Smothering Tithe", 5, Rarity.RARE, mage.cards.s.SmotheringTithe.class)); + cards.add(new SetCardInfo("Stranglehold", 4, Rarity.RARE, mage.cards.s.Stranglehold.class)); + cards.add(new SetCardInfo("Training Grounds", 6, Rarity.RARE, mage.cards.t.TrainingGrounds.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/JudgeGiftCards2023.java b/Mage.Sets/src/mage/sets/JudgeGiftCards2023.java new file mode 100644 index 00000000000..133474c56f6 --- /dev/null +++ b/Mage.Sets/src/mage/sets/JudgeGiftCards2023.java @@ -0,0 +1,33 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/p23 + */ +public class JudgeGiftCards2023 extends ExpansionSet { + + private static final JudgeGiftCards2023 instance = new JudgeGiftCards2023(); + + public static JudgeGiftCards2023 getInstance() { + return instance; + } + + private JudgeGiftCards2023() { + super("Judge Gift Cards 2023", "P23", ExpansionSet.buildDate(2023, 1, 1), SetType.PROMOTIONAL); + this.hasBoosters = false; + + cards.add(new SetCardInfo("Forest", 10, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Grindstone", 2, Rarity.RARE, mage.cards.g.Grindstone.class)); + cards.add(new SetCardInfo("Island", 7, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 9, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Mycosynth Lattice", 3, Rarity.RARE, mage.cards.m.MycosynthLattice.class)); + cards.add(new SetCardInfo("Painter's Servant", 1, Rarity.RARE, mage.cards.p.PaintersServant.class)); + cards.add(new SetCardInfo("Plains", 6, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Retrofitter Foundry", 4, Rarity.RARE, mage.cards.r.RetrofitterFoundry.class)); + cards.add(new SetCardInfo("Swamp", 8, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Sword of War and Peace", 5, Rarity.MYTHIC, mage.cards.s.SwordOfWarAndPeace.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/SpecialGuests.java b/Mage.Sets/src/mage/sets/SpecialGuests.java index 55dbe9fbb6c..fd069857867 100644 --- a/Mage.Sets/src/mage/sets/SpecialGuests.java +++ b/Mage.Sets/src/mage/sets/SpecialGuests.java @@ -21,35 +21,58 @@ public final class SpecialGuests extends ExpansionSet { this.hasBoosters = false; this.hasBasicLands = false; + cards.add(new SetCardInfo("Akroma's Memorial", 81, Rarity.MYTHIC, mage.cards.a.AkromasMemorial.class)); + cards.add(new SetCardInfo("Bloom Tender", 79, Rarity.MYTHIC, mage.cards.b.BloomTender.class)); + cards.add(new SetCardInfo("Bone Miser", 87, Rarity.MYTHIC, mage.cards.b.BoneMiser.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Bone Miser", 97, Rarity.MYTHIC, mage.cards.b.BoneMiser.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Brazen Borrower", 30, Rarity.MYTHIC, mage.cards.b.BrazenBorrower.class)); cards.add(new SetCardInfo("Breeches, Brazen Plunderer", 6, Rarity.UNCOMMON, mage.cards.b.BreechesBrazenPlunderer.class)); cards.add(new SetCardInfo("Bridge from Below", 3, Rarity.RARE, mage.cards.b.BridgeFromBelow.class)); cards.add(new SetCardInfo("Carnage Tyrant", 10, Rarity.MYTHIC, mage.cards.c.CarnageTyrant.class)); + cards.add(new SetCardInfo("Cavalier of Dawn", 84, Rarity.MYTHIC, mage.cards.c.CavalierOfDawn.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Cavalier of Dawn", 94, Rarity.MYTHIC, mage.cards.c.CavalierOfDawn.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Chandra's Ignition", 89, Rarity.MYTHIC, mage.cards.c.ChandrasIgnition.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Chandra's Ignition", 99, Rarity.MYTHIC, mage.cards.c.ChandrasIgnition.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Chrome Mox", 102, Rarity.MYTHIC, mage.cards.c.ChromeMox.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Chrome Mox", 92, Rarity.MYTHIC, mage.cards.c.ChromeMox.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Collected Company", 72, Rarity.MYTHIC, mage.cards.c.CollectedCompany.class)); + cards.add(new SetCardInfo("Condemn", 74, Rarity.MYTHIC, mage.cards.c.Condemn.class)); cards.add(new SetCardInfo("Crashing Footfalls", 25, Rarity.MYTHIC, mage.cards.c.CrashingFootfalls.class)); + cards.add(new SetCardInfo("Damnation", 68, Rarity.MYTHIC, mage.cards.d.Damnation.class)); cards.add(new SetCardInfo("Dargo, the Shipwrecker", 7, Rarity.UNCOMMON, mage.cards.d.DargoTheShipwrecker.class)); cards.add(new SetCardInfo("Desert", 37, Rarity.MYTHIC, mage.cards.d.Desert.class)); cards.add(new SetCardInfo("Desertion", 31, Rarity.MYTHIC, mage.cards.d.Desertion.class)); cards.add(new SetCardInfo("Dismember", 41, Rarity.MYTHIC, mage.cards.d.Dismember.class)); cards.add(new SetCardInfo("Drown in the Loch", 27, Rarity.MYTHIC, mage.cards.d.DrownInTheLoch.class)); + cards.add(new SetCardInfo("Embercleave", 77, Rarity.MYTHIC, mage.cards.e.Embercleave.class)); cards.add(new SetCardInfo("Endurance", 48, Rarity.MYTHIC, mage.cards.e.Endurance.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Endurance", 53, Rarity.MYTHIC, mage.cards.e.Endurance.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Expressive Iteration", 43, Rarity.MYTHIC, mage.cards.e.ExpressiveIteration.class)); + cards.add(new SetCardInfo("Expropriate", 66, Rarity.MYTHIC, mage.cards.e.Expropriate.class)); cards.add(new SetCardInfo("Fabricate", 20, Rarity.MYTHIC, mage.cards.f.Fabricate.class)); cards.add(new SetCardInfo("Field of the Dead", 28, Rarity.MYTHIC, mage.cards.f.FieldOfTheDead.class)); + cards.add(new SetCardInfo("Fiend Artisan", 83, Rarity.MYTHIC, mage.cards.f.FiendArtisan.class)); cards.add(new SetCardInfo("Frogmite", 61, Rarity.MYTHIC, mage.cards.f.Frogmite.class)); cards.add(new SetCardInfo("Fury", 47, Rarity.MYTHIC, mage.cards.f.Fury.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Fury", 52, Rarity.MYTHIC, mage.cards.f.Fury.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Galvanic Blast", 100, Rarity.MYTHIC, mage.cards.g.GalvanicBlast.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Galvanic Blast", 90, Rarity.MYTHIC, mage.cards.g.GalvanicBlast.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Gamble", 24, Rarity.MYTHIC, mage.cards.g.Gamble.class)); cards.add(new SetCardInfo("Ghalta, Primal Hunger", 11, Rarity.RARE, mage.cards.g.GhaltaPrimalHunger.class)); cards.add(new SetCardInfo("Ghostly Prison", 19, Rarity.MYTHIC, mage.cards.g.GhostlyPrison.class)); + cards.add(new SetCardInfo("Goblin Bushwhacker", 78, Rarity.MYTHIC, mage.cards.g.GoblinBushwhacker.class)); cards.add(new SetCardInfo("Grief", 46, Rarity.MYTHIC, mage.cards.g.Grief.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Grief", 51, Rarity.MYTHIC, mage.cards.g.Grief.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Grim Tutor", 76, Rarity.MYTHIC, mage.cards.g.GrimTutor.class)); + cards.add(new SetCardInfo("Hallowed Haunting", 64, Rarity.MYTHIC, mage.cards.h.HallowedHaunting.class)); cards.add(new SetCardInfo("Kalamax, the Stormsire", 13, Rarity.MYTHIC, mage.cards.k.KalamaxTheStormsire.class)); cards.add(new SetCardInfo("Kindred Charge", 58, Rarity.MYTHIC, mage.cards.k.KindredCharge.class)); cards.add(new SetCardInfo("Ledger Shredder", 55, Rarity.MYTHIC, mage.cards.l.LedgerShredder.class)); cards.add(new SetCardInfo("Lord Windgrace", 14, Rarity.MYTHIC, mage.cards.l.LordWindgrace.class)); cards.add(new SetCardInfo("Lord of Atlantis", 1, Rarity.RARE, mage.cards.l.LordOfAtlantis.class)); + cards.add(new SetCardInfo("Lord of the Undead", 88, Rarity.MYTHIC, mage.cards.l.LordOfTheUndead.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Lord of the Undead", 98, Rarity.MYTHIC, mage.cards.l.LordOfTheUndead.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Maddening Hex", 70, Rarity.MYTHIC, mage.cards.m.MaddeningHex.class)); cards.add(new SetCardInfo("Malcolm, Keen-Eyed Navigator", 2, Rarity.UNCOMMON, mage.cards.m.MalcolmKeenEyedNavigator.class)); cards.add(new SetCardInfo("Mana Crypt", "17a", Rarity.MYTHIC, mage.cards.m.ManaCrypt.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Mana Crypt", "17b", Rarity.MYTHIC, mage.cards.m.ManaCrypt.class, NON_FULL_USE_VARIOUS)); @@ -63,7 +86,12 @@ public final class SpecialGuests extends ExpansionSet { cards.add(new SetCardInfo("Morbid Opportunist", 32, Rarity.MYTHIC, mage.cards.m.MorbidOpportunist.class)); cards.add(new SetCardInfo("Mystic Snake", 35, Rarity.MYTHIC, mage.cards.m.MysticSnake.class)); cards.add(new SetCardInfo("Notion Thief", 36, Rarity.MYTHIC, mage.cards.n.NotionThief.class)); + cards.add(new SetCardInfo("Noxious Revival", 73, Rarity.MYTHIC, mage.cards.n.NoxiousRevival.class)); + cards.add(new SetCardInfo("Paradise Druid", 80, Rarity.MYTHIC, mage.cards.p.ParadiseDruid.class)); + cards.add(new SetCardInfo("Pathbreaker Ibex", 101, Rarity.MYTHIC, mage.cards.p.PathbreakerIbex.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Pathbreaker Ibex", 91, Rarity.MYTHIC, mage.cards.p.PathbreakerIbex.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Persist", 42, Rarity.MYTHIC, mage.cards.p.Persist.class)); + cards.add(new SetCardInfo("Phantasmal Image", 67, Rarity.MYTHIC, mage.cards.p.PhantasmalImage.class)); cards.add(new SetCardInfo("Pitiless Plunderer", 5, Rarity.UNCOMMON, mage.cards.p.PitilessPlunderer.class)); cards.add(new SetCardInfo("Polyraptor", 12, Rarity.MYTHIC, mage.cards.p.Polyraptor.class)); cards.add(new SetCardInfo("Port Razer", 33, Rarity.MYTHIC, mage.cards.p.PortRazer.class)); @@ -72,11 +100,16 @@ public final class SpecialGuests extends ExpansionSet { cards.add(new SetCardInfo("Rampaging Ferocidon", 8, Rarity.RARE, mage.cards.r.RampagingFerocidon.class)); cards.add(new SetCardInfo("Rat Colony", 56, Rarity.MYTHIC, mage.cards.r.RatColony.class)); cards.add(new SetCardInfo("Relentless Rats", 57, Rarity.MYTHIC, mage.cards.r.RelentlessRats.class)); + cards.add(new SetCardInfo("Sacrifice", 69, Rarity.MYTHIC, mage.cards.s.Sacrifice.class)); cards.add(new SetCardInfo("Scapeshift", 34, Rarity.MYTHIC, mage.cards.s.Scapeshift.class)); cards.add(new SetCardInfo("Secluded Courtyard", 63, Rarity.MYTHIC, mage.cards.s.SecludedCourtyard.class)); cards.add(new SetCardInfo("Show and Tell", 21, Rarity.MYTHIC, mage.cards.s.ShowAndTell.class)); + cards.add(new SetCardInfo("Skysovereign, Consul Flagship", 103, Rarity.MYTHIC, mage.cards.s.SkysovereignConsulFlagship.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Skysovereign, Consul Flagship", 93, Rarity.MYTHIC, mage.cards.s.SkysovereignConsulFlagship.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Solitude", 44, Rarity.MYTHIC, mage.cards.s.Solitude.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Solitude", 49, Rarity.MYTHIC, mage.cards.s.Solitude.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Soul Warden", 65, Rarity.MYTHIC, mage.cards.s.SoulWarden.class)); + cards.add(new SetCardInfo("Sphinx's Tutelage", 75, Rarity.MYTHIC, mage.cards.s.SphinxsTutelage.class)); cards.add(new SetCardInfo("Star Compass", 18, Rarity.UNCOMMON, mage.cards.s.StarCompass.class)); cards.add(new SetCardInfo("Stoneforge Mystic", 29, Rarity.MYTHIC, mage.cards.s.StoneforgeMystic.class)); cards.add(new SetCardInfo("Subtlety", 45, Rarity.MYTHIC, mage.cards.s.Subtlety.class, NON_FULL_USE_VARIOUS)); @@ -84,12 +117,18 @@ public final class SpecialGuests extends ExpansionSet { cards.add(new SetCardInfo("Sword of Fire and Ice", 62, Rarity.MYTHIC, mage.cards.s.SwordOfFireAndIce.class)); cards.add(new SetCardInfo("Swords to Plowshares", 54, Rarity.MYTHIC, mage.cards.s.SwordsToPlowshares.class)); cards.add(new SetCardInfo("Sylvan Tutor", 59, Rarity.MYTHIC, mage.cards.s.SylvanTutor.class)); + cards.add(new SetCardInfo("Temporal Manipulation", 82, Rarity.MYTHIC, mage.cards.t.TemporalManipulation.class)); cards.add(new SetCardInfo("Thought-Knot Seer", 39, Rarity.MYTHIC, mage.cards.t.ThoughtKnotSeer.class)); + cards.add(new SetCardInfo("Thoughtcast", 85, Rarity.MYTHIC, mage.cards.t.Thoughtcast.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Thoughtcast", 95, Rarity.MYTHIC, mage.cards.t.Thoughtcast.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Thrasios, Triton Hero", 16, Rarity.RARE, mage.cards.t.ThrasiosTritonHero.class)); cards.add(new SetCardInfo("Tireless Tracker", 26, Rarity.MYTHIC, mage.cards.t.TirelessTracker.class)); cards.add(new SetCardInfo("Toski, Bearer of Secrets", 60, Rarity.MYTHIC, mage.cards.t.ToskiBearerOfSecrets.class)); cards.add(new SetCardInfo("Tragic Slip", 22, Rarity.MYTHIC, mage.cards.t.TragicSlip.class)); cards.add(new SetCardInfo("Underworld Breach", 9, Rarity.RARE, mage.cards.u.UnderworldBreach.class)); + cards.add(new SetCardInfo("Unholy Heat", 71, Rarity.MYTHIC, mage.cards.u.UnholyHeat.class)); cards.add(new SetCardInfo("Victimize", 23, Rarity.MYTHIC, mage.cards.v.Victimize.class)); + cards.add(new SetCardInfo("Whir of Invention", 86, Rarity.MYTHIC, mage.cards.w.WhirOfInvention.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Whir of Invention", 96, Rarity.MYTHIC, mage.cards.w.WhirOfInvention.class, NON_FULL_USE_VARIOUS)); } } diff --git a/Mage.Sets/src/mage/sets/Unfinity.java b/Mage.Sets/src/mage/sets/Unfinity.java index d64beec852e..48366459648 100644 --- a/Mage.Sets/src/mage/sets/Unfinity.java +++ b/Mage.Sets/src/mage/sets/Unfinity.java @@ -16,10 +16,14 @@ public final class Unfinity extends ExpansionSet { } private Unfinity() { - super("Unfinity", "UNF", ExpansionSet.buildDate(2022, 4, 1), SetType.JOKE_SET); + super("Unfinity", "UNF", ExpansionSet.buildDate(2022, 4, 1), SetType.SUPPLEMENTAL); this.hasBasicLands = true; this.hasBoosters = false; // un-set, low implemented cards + // set contains both legal and joke cards, so must use SetType.SUPPLEMENTAL: + // https://mtg.fandom.com/wiki/Unfinity + // The set is the first Un-set to include a mix of eternal-legal cards and acorn cards. + cards.add(new SetCardInfo("\"Name Sticker\" Goblin", "107m", Rarity.COMMON, mage.cards.n.NameStickerGoblin.class)); cards.add(new SetCardInfo("Atomwheel Acrobats", 130, Rarity.COMMON, mage.cards.a.AtomwheelAcrobats.class)); cards.add(new SetCardInfo("Attempted Murder", 66, Rarity.UNCOMMON, mage.cards.a.AttemptedMurder.class)); @@ -50,6 +54,7 @@ public final class Unfinity extends ExpansionSet { cards.add(new SetCardInfo("Pair o' Dice Lost", 149, Rarity.UNCOMMON, mage.cards.p.PairODiceLost.class)); cards.add(new SetCardInfo("Plains", 235, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_UST_VARIOUS)); cards.add(new SetCardInfo("Plains", 240, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_UST_VARIOUS)); + cards.add(new SetCardInfo("Priority Boarding", 119, Rarity.UNCOMMON, mage.cards.p.PriorityBoarding.class)); cards.add(new SetCardInfo("Sacred Foundry", 285, Rarity.RARE, mage.cards.s.SacredFoundry.class)); cards.add(new SetCardInfo("Saw in Half", 88, Rarity.RARE, mage.cards.s.SawInHalf.class)); cards.add(new SetCardInfo("Six-Sided Die", 92, Rarity.COMMON, mage.cards.s.SixSidedDie.class)); diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2021.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2021.java new file mode 100644 index 00000000000..485b3300e04 --- /dev/null +++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2021.java @@ -0,0 +1,30 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/pw21 + */ +public class WizardsPlayNetwork2021 extends ExpansionSet { + + private static final WizardsPlayNetwork2021 instance = new WizardsPlayNetwork2021(); + + public static WizardsPlayNetwork2021 getInstance() { + return instance; + } + + private WizardsPlayNetwork2021() { + super("Wizards Play Network 2021", "PW21", ExpansionSet.buildDate(2021, 6, 18), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Arbor Elf", 1, Rarity.RARE, mage.cards.a.ArborElf.class)); + cards.add(new SetCardInfo("Collected Company", 2, Rarity.RARE, mage.cards.c.CollectedCompany.class)); + cards.add(new SetCardInfo("Conjurer's Closet", 6, Rarity.RARE, mage.cards.c.ConjurersCloset.class)); + cards.add(new SetCardInfo("Fabled Passage", 4, Rarity.RARE, mage.cards.f.FabledPassage.class)); + cards.add(new SetCardInfo("Mind Stone", 5, Rarity.RARE, mage.cards.m.MindStone.class)); + cards.add(new SetCardInfo("Wurmcoil Engine", 3, Rarity.MYTHIC, mage.cards.w.WurmcoilEngine.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2022.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2022.java new file mode 100644 index 00000000000..30ef4953e93 --- /dev/null +++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2022.java @@ -0,0 +1,30 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/pw22 + */ +public class WizardsPlayNetwork2022 extends ExpansionSet { + + private static final WizardsPlayNetwork2022 instance = new WizardsPlayNetwork2022(); + + public static WizardsPlayNetwork2022 getInstance() { + return instance; + } + + private WizardsPlayNetwork2022() { + super("Wizards Play Network 2022", "PW22", ExpansionSet.buildDate(2022, 3, 5), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Atsushi, the Blazing Sky", 3, Rarity.MYTHIC, mage.cards.a.AtsushiTheBlazingSky.class)); + cards.add(new SetCardInfo("Consider", 1, Rarity.RARE, mage.cards.c.Consider.class)); + cards.add(new SetCardInfo("Dismember", 5, Rarity.RARE, mage.cards.d.Dismember.class)); + cards.add(new SetCardInfo("Fateful Absence", 2, Rarity.RARE, mage.cards.f.FatefulAbsence.class)); + cards.add(new SetCardInfo("Psychosis Crawler", 6, Rarity.RARE, mage.cards.p.PsychosisCrawler.class)); + cards.add(new SetCardInfo("Swiftfoot Boots", 4, Rarity.RARE, mage.cards.s.SwiftfootBoots.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2023.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2023.java new file mode 100644 index 00000000000..58ff15b30b0 --- /dev/null +++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2023.java @@ -0,0 +1,35 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/pw23 + */ +public class WizardsPlayNetwork2023 extends ExpansionSet { + + private static final WizardsPlayNetwork2023 instance = new WizardsPlayNetwork2023(); + + public static WizardsPlayNetwork2023 getInstance() { + return instance; + } + + private WizardsPlayNetwork2023() { + super("Wizards Play Network 2023", "PW23", ExpansionSet.buildDate(2023, 1, 1), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Beast Within", 3, Rarity.RARE, mage.cards.b.BeastWithin.class)); + cards.add(new SetCardInfo("Cultivate", 6, Rarity.RARE, mage.cards.c.Cultivate.class)); + cards.add(new SetCardInfo("Drown in the Loch", 4, Rarity.RARE, mage.cards.d.DrownInTheLoch.class)); + cards.add(new SetCardInfo("Ice Out", 7, Rarity.RARE, mage.cards.i.IceOut.class)); + cards.add(new SetCardInfo("Lifecrafter's Bestiary", 2, Rarity.RARE, mage.cards.l.LifecraftersBestiary.class)); + cards.add(new SetCardInfo("Norn's Annex", 1, Rarity.RARE, mage.cards.n.NornsAnnex.class)); + cards.add(new SetCardInfo("Pyroblast", 8, Rarity.RARE, mage.cards.p.Pyroblast.class)); + cards.add(new SetCardInfo("Rampant Growth", 9, Rarity.RARE, mage.cards.r.RampantGrowth.class)); + cards.add(new SetCardInfo("Ravenous Chupacabra", 10, Rarity.RARE, mage.cards.r.RavenousChupacabra.class)); + cards.add(new SetCardInfo("Syr Konrad, the Grim", 5, Rarity.RARE, mage.cards.s.SyrKonradTheGrim.class)); + cards.add(new SetCardInfo("Unclaimed Territory", 11, Rarity.RARE, mage.cards.u.UnclaimedTerritory.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2024.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2024.java new file mode 100644 index 00000000000..0d89ceb1de5 --- /dev/null +++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2024.java @@ -0,0 +1,42 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/pw24 + */ +public class WizardsPlayNetwork2024 extends ExpansionSet { + + private static final WizardsPlayNetwork2024 instance = new WizardsPlayNetwork2024(); + + public static WizardsPlayNetwork2024 getInstance() { + return instance; + } + + private WizardsPlayNetwork2024() { + super("Wizards Play Network 2024", "PW24", ExpansionSet.buildDate(2024, 1, 1), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Chaos Warp", 7, Rarity.RARE, mage.cards.c.ChaosWarp.class)); + cards.add(new SetCardInfo("Commander's Sphere", 8, Rarity.RARE, mage.cards.c.CommandersSphere.class)); + cards.add(new SetCardInfo("Costly Plunder", 14, Rarity.RARE, mage.cards.c.CostlyPlunder.class)); + cards.add(new SetCardInfo("Crippling Fear", 17, Rarity.RARE, mage.cards.c.CripplingFear.class)); + cards.add(new SetCardInfo("Darksteel Colossus", 19, Rarity.MYTHIC, mage.cards.d.DarksteelColossus.class)); + cards.add(new SetCardInfo("Diabolic Tutor", 13, Rarity.RARE, mage.cards.d.DiabolicTutor.class)); + cards.add(new SetCardInfo("Gaea's Liege", 5, Rarity.RARE, mage.cards.g.GaeasLiege.class)); + cards.add(new SetCardInfo("Goblin King", 4, Rarity.RARE, mage.cards.g.GoblinKing.class)); + cards.add(new SetCardInfo("Heirloom Blade", 16, Rarity.RARE, mage.cards.h.HeirloomBlade.class)); + cards.add(new SetCardInfo("Lord of Atlantis", 2, Rarity.RARE, mage.cards.l.LordOfAtlantis.class)); + cards.add(new SetCardInfo("Night's Whisper", 18, Rarity.RARE, mage.cards.n.NightsWhisper.class)); + cards.add(new SetCardInfo("Oltec Matterweaver", 12, Rarity.MYTHIC, mage.cards.o.OltecMatterweaver.class)); + cards.add(new SetCardInfo("Ravenous Squirrel", 15, Rarity.RARE, mage.cards.r.RavenousSquirrel.class)); + cards.add(new SetCardInfo("Rogue's Passage", 10, Rarity.UNCOMMON, mage.cards.r.RoguesPassage.class)); + cards.add(new SetCardInfo("Serra Angel", 1, Rarity.RARE, mage.cards.s.SerraAngel.class)); + cards.add(new SetCardInfo("Transmutation Font", 11, Rarity.MYTHIC, mage.cards.t.TransmutationFont.class)); + cards.add(new SetCardInfo("Underworld Connections", 9, Rarity.RARE, mage.cards.u.UnderworldConnections.class)); + cards.add(new SetCardInfo("Zombie Master", 3, Rarity.RARE, mage.cards.z.ZombieMaster.class)); + } +} diff --git a/Mage.Sets/src/mage/sets/WizardsPlayNetwork2025.java b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2025.java new file mode 100644 index 00000000000..5d762e01f6c --- /dev/null +++ b/Mage.Sets/src/mage/sets/WizardsPlayNetwork2025.java @@ -0,0 +1,25 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * https://scryfall.com/sets/pw25 + */ +public class WizardsPlayNetwork2025 extends ExpansionSet { + + private static final WizardsPlayNetwork2025 instance = new WizardsPlayNetwork2025(); + + public static WizardsPlayNetwork2025 getInstance() { + return instance; + } + + private WizardsPlayNetwork2025() { + super("Wizards Play Network 2025", "PW25", ExpansionSet.buildDate(2025, 1, 1), SetType.PROMOTIONAL); + this.hasBoosters = false; + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Rishkar's Expertise", 1, Rarity.RARE, mage.cards.r.RishkarsExpertise.class)); + } +} diff --git a/Mage.Tests/pom.xml b/Mage.Tests/pom.xml index 76334a3fa9e..feba3cf97ca 100644 --- a/Mage.Tests/pom.xml +++ b/Mage.Tests/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage-tests diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java index 624a26a3341..af1e99a689e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java @@ -6,6 +6,22 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; /** + * Combat's blocking tests + *

+ * TODO: add tests with multi blocker requirement effects + *

+ * Supported abilities: + * - DeathtouchAbility - supported, has tests + * - FirstStrikeAbility - supported, has tests + * - DoubleStrikeAbility - ? + * - IndestructibleAbility - supported, need tests + * - FlyingAbility - ? + * - ReachAbility - ? + * - ExaltedAbility - ? + * - Trample + Deathtouch + * - combat damage and die triggers - need to implement full combat simulation with stack resolve, see CombatUtil->willItSurviveSimple + * - other use cases, see https://github.com/magefree/mage/issues/4485 + * * @author JayDi85 */ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { @@ -52,7 +68,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { } @Test - public void test_Block_1_small_attacker_vs_1_small_blocker() { + public void test_Block_1_small_attacker_vs_1_small_blocker_same() { addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1 @@ -72,6 +88,54 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { assertGraveyardCount(playerB, "Arbor Elf", 1); } + @Test + public void test_Block_1_small_attacker_vs_1_small_blocker_better() { + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 + //addCard(Zone.BATTLEFIELD, playerB, "Elvish Archers"); // 2/1 first strike + addCard(Zone.BATTLEFIELD, playerB, "Dregscape Zombie", 1); // 2/1 + + attack(1, playerA, "Arbor Elf"); + + // ai must ignore block to keep better creature alive + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("no blockers", 1, playerB, ""); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 1); + assertPermanentCount(playerA, "Arbor Elf", 1); + assertPermanentCount(playerB, "Dregscape Zombie", 1); + } + + @Test + public void test_Block_1_small_attacker_vs_1_small_blocker_better_but_player_die() { + addCustomEffect_TargetDamage(playerA, 19); + + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Dregscape Zombie", 1); // 2/1 + + // prepare 1 life + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target damage 19", playerB); + + attack(1, playerA, "Arbor Elf"); + + // ai must keep better blocker in normal case, but now it must protect from lose and sacrifice it + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("x1 blocker", 1, playerB, "Dregscape Zombie"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 1); + assertGraveyardCount(playerA, "Arbor Elf", 1); + assertGraveyardCount(playerB, "Dregscape Zombie", 1); + } + @Test public void test_Block_1_big_attacker_vs_1_small_blocker() { addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 @@ -194,12 +258,55 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { assertDamageReceived(playerB, "Spectral Bears", 2); } - // TODO: add tests with multi blocker requirement effects - // TODO: add tests for DeathtouchAbility - // TODO: add tests for FirstStrikeAbility - // TODO: add tests for DoubleStrikeAbility - // TODO: add tests for IndestructibleAbility - // TODO: add tests for FlyingAbility - // TODO: add tests for ReachAbility - // TODO: add tests for ExaltedAbility??? + @Test + public void test_Block_1_attacker_vs_first_strike() { + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + // + addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "White Knight", 1); // 2/2 with first strike + addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3 + addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5 + addCard(Zone.BATTLEFIELD, playerB, "Colossal Dreadmaw", 1); // 6/6 + + attack(1, playerA, "Balduvian Bears"); + + // ai must use smaller blocker and survive (2/2 with first strike must block 2/2) + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("x1 optimal blocker", 1, playerB, "White Knight"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Balduvian Bears", 1); + assertDamageReceived(playerB, "White Knight", 0); // due first strike + } + + @Test + public void test_Block_1_attacker_vs_deathtouch() { + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + // + addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Arashin Cleric", 1); // 1/3 + addCard(Zone.BATTLEFIELD, playerB, "Graveblade Marauder", 1); // 1/4 with deathtouch + addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5 + addCard(Zone.BATTLEFIELD, playerB, "Colossal Dreadmaw", 1); // 6/6 + + attack(1, playerA, "Balduvian Bears"); + + // ai must use smaller blocker to kill and survive (1/4 with deathtouch must block 2/2 -- not a smaller 1/3) + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("x1 optimal blocker", 1, playerB, "Graveblade Marauder"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Balduvian Bears", 1); + assertDamageReceived(playerB, "Graveblade Marauder", 2); + } } 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 45db260750a..c8ce253a707 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 @@ -1,8 +1,12 @@ package org.mage.test.AI.basic; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.game.permanent.Permanent; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBaseAI; @@ -10,6 +14,15 @@ import java.util.Arrays; import java.util.List; /** + * Possible problems: + * - big memory consumption on sims prepare (memory overflow) + * - too many sims to calculate (AI fail on time out and do nothing) + *

+ * 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 simulations for target options in triggered + * * @author JayDi85 */ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI { @@ -20,7 +33,7 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI { } @Test - public void test_AIvsAI_Simple() { + public void test_Simple_ShortGame() { // both must kill x2 bears by x2 bolts addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 2); @@ -38,7 +51,7 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI { } @Test - public void test_AIvsAI_LongGame() { + public void test_Simple_LongGame() { // many bears and bolts must help to end game fast int maxTurn = 50; removeAllCardsFromLibrary(playerA); @@ -61,4 +74,143 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI { Assert.assertTrue("One of player must won a game before turn " + maxTurn + ", but it ends on " + currentGame, currentGame.hasEnded()); } + + private void runManyTargetOptionsInTrigger(String info, int totalCreatures, int needDiedCreatures, boolean isDamageRandomCreature, int needPlayerLife) { + // When Bogardan Hellkite enters, it deals 5 damage divided as you choose among any number of targets. + addCard(Zone.HAND, playerA, "Bogardan Hellkite", 1); // {6}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + // + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", totalCreatures); + + if (isDamageRandomCreature) { + runCode("damage creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (s, player, game) -> { + Permanent creature = game.getBattlefield().getAllPermanents().stream() + .filter(p -> p.getName().equals("Balduvian Bears")) + .findAny() + .orElse(null); + Assert.assertNotNull(creature); + Ability fakeAbility = new SimpleStaticAbility(null); + fakeAbility.setControllerId(player.getId()); + fakeAbility.setSourceId(creature.getId()); + creature.damage(1, fakeAbility, game); + }); + } + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Bogardan Hellkite", 1); // if fail then AI stops before all sims ends + assertGraveyardCount(playerB, "Balduvian Bears", needDiedCreatures); + assertLife(playerB, needPlayerLife); + } + + @Test + @Ignore // enable after triggered supported or need performance test + public void test_ManyTargetOptions_Triggered_Single() { + // 2 damage to bear and 3 damage to player B + runManyTargetOptionsInTrigger("1 target creature", 1, 1, false, 20 - 3); + } + + @Test + @Ignore // enable after triggered supported or need performance test + public void test_ManyTargetOptions_Triggered_Few() { + // 4 damage to x2 bears and 1 damage to player B + runManyTargetOptionsInTrigger("2 target creatures", 2, 2, false, 20 - 1); + } + + @Test + @Ignore // enable after triggered supported or need performance test + public void test_ManyTargetOptions_Triggered_Many() { + // 4 damage to x2 bears and 1 damage to player B + runManyTargetOptionsInTrigger("5 target creatures", 5, 2, false, 20 - 1); + } + + @Test + @Ignore // enable after triggered supported or need performance test + public void test_ManyTargetOptions_Triggered_TooMuch() { + // warning, can be slow + + // make sure targets optimization works + // (must ignore same targets for faster calc) + + // 4 damage to x2 bears and 1 damage to player B + runManyTargetOptionsInTrigger("50 target creatures", 50, 2, false, 20 - 1); + } + + @Test + @Ignore // enable after triggered supported or need performance test + public void test_ManyTargetOptions_Triggered_TargetGroups() { + // make sure targets optimization can find unique creatures, e.g. damaged + + // 4 damage to x2 bears and 1 damage to damaged bear + runManyTargetOptionsInTrigger("5 target creatures with one damaged", 5, 3, true, 20); + } + + private void runManyTargetOptionsInActivate(String info, int totalCreatures, int needDiedCreatures, boolean isDamageRandomCreature, int needPlayerLife) { + // Boulderfall deals 5 damage divided as you choose among any number of targets. + addCard(Zone.HAND, playerA, "Boulderfall", 1); // {6}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + // + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", totalCreatures); + + if (isDamageRandomCreature) { + runCode("damage creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (s, player, game) -> { + Permanent creature = game.getBattlefield().getAllPermanents().stream() + .filter(p -> p.getName().equals("Balduvian Bears")) + .findAny() + .orElse(null); + Assert.assertNotNull(creature); + Ability fakeAbility = new SimpleStaticAbility(null); + fakeAbility.setControllerId(player.getId()); + fakeAbility.setSourceId(creature.getId()); + creature.damage(1, fakeAbility, game); + }); + } + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Boulderfall", 1); // if fail then AI stops before all sims ends + assertGraveyardCount(playerB, "Balduvian Bears", needDiedCreatures); + assertLife(playerB, needPlayerLife); + } + + @Test + public void test_ManyTargetOptions_Activated_Single() { + // 2 damage to bear and 3 damage to player B + runManyTargetOptionsInActivate("1 target creature", 1, 1, false, 20 - 3); + } + + @Test + public void test_ManyTargetOptions_Activated_Few() { + // 4 damage to x2 bears and 1 damage to player B + runManyTargetOptionsInActivate("2 target creatures", 2, 2, false, 20 - 1); + } + + @Test + public void test_ManyTargetOptions_Activated_Many() { + // 4 damage to x2 bears and 1 damage to player B + runManyTargetOptionsInActivate("5 target creatures", 5, 2, false, 20 - 1); + } + + @Test + public void test_ManyTargetOptions_Activated_TooMuch() { + // warning, can be slow + + // make sure targets optimization works + // (must ignore same targets for faster calc) + + // 4 damage to x2 bears and 1 damage to player B + runManyTargetOptionsInActivate("50 target creatures", 50, 2, false, 20 - 1); + } + + @Test + public void test_ManyTargetOptions_Activated_TargetGroups() { + // make sure targets optimization can find unique creatures, e.g. damaged + + // 4 damage to x2 bears and 1 damage to damaged bear + runManyTargetOptionsInActivate("5 target creatures with one damaged", 5, 3, true, 20); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java index cb24db3af3d..51b5efea6c6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java @@ -31,6 +31,8 @@ public class TargetAmountAITest extends CardTestPlayerBaseWithAIHelps { @Test public void test_AI_SimulateTargets() { + // warning, test depends on targets list optimization by AI + // Distribute four +1/+1 counters among any number of target creatures. addCard(Zone.HAND, playerA, "Blessings of Nature", 1); // {4}{G} addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/designations/StartYourEnginesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/designations/StartYourEnginesTest.java new file mode 100644 index 00000000000..2c4f6caf823 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/designations/StartYourEnginesTest.java @@ -0,0 +1,202 @@ +package org.mage.test.cards.designations; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.players.Player; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class StartYourEnginesTest extends CardTestPlayerBase { + + private static final String sarcophagus = "Walking Sarcophagus"; + + private void assertSpeed(Player player, int speed) { + Assert.assertEquals(player.getName() + " speed should be " + speed, speed, player.getSpeed()); + } + + @Test + public void testRegular() { + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 0); + assertSpeed(playerB, 0); + } + + @Test + public void testSpeed1() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 1); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2, 1); + } + + private static final String goblet = "Onyx Goblet"; + + @Test + public void testSpeed2() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerA, goblet); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 2); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2, 1); + } + + @Test + public void testSpeed1OppTurn() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerA, goblet); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 1); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2, 1); + } + + @Test + public void testSpeed3() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerA, goblet); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 3); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2, 1); + } + + @Test + public void testSpeed4() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerA, goblet); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(5, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 4); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2 + 1, 1 + 2); + } + + @Test + public void testSpeed5() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerA, goblet); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(5, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(7, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + setStrictChooseMode(true); + setStopAt(7, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 4); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2 + 1, 1 + 2); + } + + private static final String surveyor = "Loxodon Surveyor"; + + @Test + public void testSpeed4Graveyard() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 3); + addCard(Zone.GRAVEYARD, playerA, surveyor); + + runCode("Increase player speed", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + player.initSpeed(game); + player.increaseSpeed(game); + player.increaseSpeed(game); + player.increaseSpeed(game); + }); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{3}"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 4); + assertSpeed(playerB, 0); + assertGraveyardCount(playerA, surveyor, 0); + assertExileCount(playerA, surveyor, 1); + } + + private static final String mindControl = "Mind Control"; + + @Test + public void testSpeedChangeControl() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerB, "Island", 5); + addCard(Zone.HAND, playerA, sarcophagus); + addCard(Zone.HAND, playerB, mindControl); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, mindControl, sarcophagus); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 1); + assertSpeed(playerB, 1); + assertPowerToughness(playerB, sarcophagus, 2, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java index 52f24ce1acf..b8cb151d5e8 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/conditional/CrypticTrilobiteTest.java @@ -20,31 +20,31 @@ public class CrypticTrilobiteTest extends CardTestPlayerBase { @Test public void testAvailableManaCalculation(){ setStrictChooseMode(true); - + // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} - + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); setChoice(playerA, "X=5"); - + setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); assertPermanentCount(playerA, "Cryptic Trilobite", 1); - + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); - assertManaOptions("{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}[{CrypticTrilobiteManaCondition}]", manaOptions); + assertManaOptions("{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}[{ActivatedAbilityManaCondition}]", manaOptions); } - + @Test public void testUse(){ setStrictChooseMode(true); - + // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. @@ -54,54 +54,54 @@ public class CrypticTrilobiteTest extends CardTestPlayerBase { // Soulshift 1 (When this creature dies, you may return target Spirit card with converted mana cost 1 or less from your graveyard to your hand.) addCard(Zone.BATTLEFIELD, playerA, "Deathknell Kami"); // Creature (0/1) addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); - - + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite", true); setChoice(playerA, "X=5"); - + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}:"); - + setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); assertPermanentCount(playerA, "Cryptic Trilobite", 1); assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 3); - + assertPowerToughness(playerA, "Deathknell Kami", 2, 3); - + ManaOptions manaOptions = playerA.getAvailableManaTest(currentGame); Assert.assertEquals("mana variations don't fit", 1, manaOptions.size()); - assertManaOptions("{C}{C}{C}{C}{C}{C}[{CrypticTrilobiteManaCondition}]", manaOptions); - } - + assertManaOptions("{C}{C}{C}{C}{C}{C}[{ActivatedAbilityManaCondition}]", manaOptions); + } + @Test public void testCantUse(){ setStrictChooseMode(true); - + // Cryptic Trilobite enters the battlefield with X +1/+1 counters on it. // Remove a +1/+1 counter from Cryptic Trilobite: Add {C}{C}. Spend this mana only to activate abilities. // {1}, {T}: Put a +1/+1 counter on Cryptic Trilobite. addCard(Zone.HAND, playerA, "Cryptic Trilobite"); // Creature {X}{X} - + // {4}{W}: Return another target creature you control to its owner's hand. addCard(Zone.HAND, playerA, "Aegis Automaton"); // Creature {2} (0/2) addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10); - - + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cryptic Trilobite"); setChoice(playerA, "X=5"); - + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); - + checkPlayableAbility("can't play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Aegis Automaton", false); - + setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); assertPermanentCount(playerA, "Cryptic Trilobite", 1); - assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 5); - } - - -} \ No newline at end of file + assertCounterCount(playerA, "Cryptic Trilobite", CounterType.P1P1, 5); + } + + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/ShiftingWoodlandTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/ShiftingWoodlandTest.java index 90b80d04a13..39c9bb6886a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/ShiftingWoodlandTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/ShiftingWoodlandTest.java @@ -151,4 +151,24 @@ public class ShiftingWoodlandTest extends CardTestPlayerBase { assertLife(playerB, 20 - 3 - 1); assertGraveyardCount(playerA, woodland, 1); } + + @Test + public void test_Copy_MDFC() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Yavimaya Coast", 4); // to be sure not to activate Woodland + addCard(Zone.BATTLEFIELD, playerA, woodland); + addCard(Zone.GRAVEYARD, playerA, "Drowner of Truth"); + addCard(Zone.GRAVEYARD, playerA, "Plains"); + addCard(Zone.GRAVEYARD, playerA, "Memnite"); + addCard(Zone.GRAVEYARD, playerA, "Divination"); + + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}. {this} deals 1 damage to you.", 4); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Delirium — {2}{G}{G}:", "Drowner of Truth"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Drowner of Truth", 1); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/StruggleForProjectPurityTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/StruggleForProjectPurityTest.java new file mode 100644 index 00000000000..115fcb99474 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/StruggleForProjectPurityTest.java @@ -0,0 +1,127 @@ +package org.mage.test.cards.single.pip; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +/** + * @author JayDi85 + */ +public class StruggleForProjectPurityTest extends CardTestCommander4Players { + + /** + * {@link mage.cards.s.StruggleForProjectPurity Struggle for Project Purity} + * {3}{U} + * Enchantment + * As Struggle for Project Purity enters, choose Brotherhood or Enclave. + * • Brotherhood — At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way. + * • Enclave — Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters. + */ + private static final String struggle = "Struggle for Project Purity"; + + private void checkRadCounters(String info, int needA, int needB, int needC, int needD) { + Assert.assertEquals(info + ", rad counter on playerA", needA, playerA.getCountersCount(CounterType.RAD)); + Assert.assertEquals(info + ", rad counter on playerB", needB, playerB.getCountersCount(CounterType.RAD)); + Assert.assertEquals(info + ", rad counter on playerC", needC, playerC.getCountersCount(CounterType.RAD)); + Assert.assertEquals(info + ", rad counter on playerD", needD, playerD.getCountersCount(CounterType.RAD)); + } + + @Test + public void test_Brotherhood() { + // Player order: A -> D -> C -> B + + // Brotherhood — At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way. + addCard(Zone.HAND, playerA, struggle); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + checkHandCount("before cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // struggle + starting draw + + // turn 1 - A - prepare brotherhood, no triggers + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, struggle); + setChoice(playerA, "Brotherhood"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // turn 2 - D - no triggers + checkHandCount("no draws on turn 2", 2, PhaseStep.PRECOMBAT_MAIN, playerA, 1); + + // turn 3 - C - no triggers + checkHandCount("no draws on turn 3", 3, PhaseStep.PRECOMBAT_MAIN, playerA, 1); + + // turn 4 - B - no triggers + checkHandCount("no draws on turn 4", 4, PhaseStep.PRECOMBAT_MAIN, playerA, 1); + + // turn 5 - A - trigger + // opponent draw: +1 + // you draw: +3 + checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerA, 1 + 1 + 3); // draw 1 + draw 5 + draw trigger + checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerB, 2); // opponent turn + trigger + checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerC, 2); // opponent turn + trigger + checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerD, 2); // opponent turn + trigger + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, struggle, 1); + } + + @Test + public void test_Enclave() { + // Player order: A -> D -> C -> B + + // Enclave — Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters. + addCard(Zone.HAND, playerA, struggle); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + // + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 2); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 2); + addCard(Zone.BATTLEFIELD, playerC, "Grizzly Bears", 2); + addCard(Zone.BATTLEFIELD, playerD, "Grizzly Bears", 2); + + checkHandCount("before cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // struggle + starting draw + runCode("rad count playerA turn 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 0)); + + // turn 1 + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, struggle); + setChoice(playerA, "Enclave"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // turn 1 + attack(1, playerA, "Grizzly Bears", playerD); + attack(1, playerA, "Grizzly Bears", playerD); + runCode("A attacked D on turn 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 0)); + + // turn 2 + attack(2, playerD, "Grizzly Bears", playerA); // <<< trigger for D + attack(2, playerD, "Grizzly Bears", playerA); + runCode("D attacked A on turn 2", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 2 * 2)); + + // turn 3 + attack(3, playerC, "Grizzly Bears", playerB); + attack(3, playerC, "Grizzly Bears", playerB); + runCode("B attacked B on turn 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 2 * 2)); + + // turn 4 + attack(4, playerB, "Grizzly Bears", playerA); // <<< trigger for B + attack(4, playerB, "Grizzly Bears", playerA); + runCode("B attacked A on turn 4", 4, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 2 * 2, 0, 2 * 2)); + + // turn 5 + attack(5, playerA, "Grizzly Bears", playerD); + attack(5, playerA, "Grizzly Bears", playerD); + runCode("A attacked D on turn 5", 5, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 2 * 2, 0, 2 * 2)); + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, struggle, 1); + assertLife(playerA, 20 - 2 * 2 - 2 * 2); // from D and B + assertLife(playerB, 20 - 2 * 2); // from C + assertLife(playerC, 20); // no attackers + assertLife(playerD, 20 - 2 * 2 - 2 * 2); // from A and A + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java b/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java index e60aa31d41c..a43818f1c90 100644 --- a/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java @@ -7,7 +7,7 @@ import mage.game.permanent.Permanent; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -17,7 +17,7 @@ import static org.junit.Assert.fail; * * @author noxx, JayDi85 */ -public class AttackBlockRestrictionsTest extends CardTestPlayerBase { +public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps { @Test public void testFlyingVsNonFlying() { @@ -571,7 +571,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { } @Test - public void test_MustBeBlocked_nothing() { + public void test_MustBeBlocked_nothing_human() { // Fear of Being Hunted must be blocked if able. addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2 @@ -587,12 +587,30 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { } @Test - public void test_MustBeBlocked_1_blocker() { + public void test_MustBeBlocked_nothing_AI() { + // Fear of Being Hunted must be blocked if able. + addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2 + + attack(1, playerA, "Fear of Being Hunted"); + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted"); + checkBlockers("no blocker", 1, playerB, ""); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 4); + } + + @Test + public void test_MustBeBlocked_1_blocker_human() { // Fear of Being Hunted must be blocked if able. addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2 // addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1 + // auto-choose blocker attack(1, playerA, "Fear of Being Hunted"); checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted"); checkBlockers("forced x1 blocker", 1, playerB, "Alpha Myr"); @@ -606,15 +624,36 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { } @Test - public void test_MustBeBlocked_many_blockers_good() { + public void test_MustBeBlocked_1_blocker_AI() { + // Fear of Being Hunted must be blocked if able. + addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2 + // + addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1 + + // auto-choose blocker with AI + attack(1, playerA, "Fear of Being Hunted"); + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted"); + checkBlockers("forced x1 blocker", 1, playerB, "Alpha Myr"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Fear of Being Hunted", 1); + } + + @Test + public void test_MustBeBlocked_many_blockers_good_AI() { // Fear of Being Hunted must be blocked if able. addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2 // addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 10); // 3/3 - // TODO: human logic can't be tested (until isHuman replaced by ~isComputer), so current use case will - // take first available blocker + // ai must choose any bear attack(1, playerA, "Fear of Being Hunted"); + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted"); checkBlockers("x1 optimal blocker", 1, playerB, "Spectral Bears"); @@ -627,15 +666,15 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { } @Test - public void test_MustBeBlocked_many_blockers_bad() { + public void test_MustBeBlocked_many_blockers_bad_AI() { // Fear of Being Hunted must be blocked if able. addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2 // addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1 - // TODO: human logic can't be tested (until isHuman replaced by ~isComputer), so current use case will - // take first available blocker + // ai don't want but must choose any bad memnite attack(1, playerA, "Fear of Being Hunted"); + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted"); checkBlockers("x1 optimal blocker", 1, playerB, "Memnite"); @@ -645,12 +684,11 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { assertLife(playerB, 20); assertPermanentCount(playerA, "Fear of Being Hunted", 1); + assertGraveyardCount(playerB, "Memnite", 1); // x1 blocker die } @Test - @Ignore - // TODO: enable and duplicate for AI -- after implement choose blocker logic and isHuman replace by ~isComputer - public void test_MustBeBlocked_many_blockers_optimal() { + public void test_MustBeBlocked_many_blockers_optimal_AI() { // Fear of Being Hunted must be blocked if able. addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2 // @@ -659,7 +697,9 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3 addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5 + // ai must choose optimal creature to kill but survive attack(1, playerA, "Fear of Being Hunted"); + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted"); checkBlockers("x1 optimal blocker", 1, playerB, "Deadbridge Goliath"); @@ -669,6 +709,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { assertLife(playerB, 20); assertGraveyardCount(playerA, "Fear of Being Hunted", 1); + assertGraveyardCount(playerB, 0); } @Test @@ -697,7 +738,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { } @Test - public void test_MustBlocking_full_blockers() { + public void test_MustBlocking_full_blockers_human() { // All creatures able to block target creature this turn do so. addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G} addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G} @@ -725,7 +766,37 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { } @Test - public void test_MustBlocking_many_blockers() { + public void test_MustBlocking_full_blockers_AI() { + // All creatures able to block target creature this turn do so. + addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G} + // + // Menace + // Each creature you control with menace can't be blocked except by three or more creatures. + addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2 + // + addCard(Zone.BATTLEFIELD, playerB, "Memnite", 3); // 1/1 + + // prepare + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder"); + + // ai must choose all blockers anyway + attack(1, playerA, "Sonorous Howlbonder"); + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites + checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder"); + checkBlockers("x3 blockers", 1, playerB, "Memnite", "Memnite", "Memnite"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Sonorous Howlbonder", 1); + } + + @Test + public void test_MustBlocking_many_blockers_human() { // possible bug: AI's blockers auto-fix assign too many blockers (e.g. x10 instead x3 by required effect) // All creatures able to block target creature this turn do so. @@ -754,6 +825,38 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { assertGraveyardCount(playerA, "Sonorous Howlbonder", 1); } + @Test + public void test_MustBlocking_many_blockers_AI() { + // possible bug: AI's blockers auto-fix assign too many blockers (e.g. x10 instead x3 by required effect) + + // All creatures able to block target creature this turn do so. + addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G} + // + // Menace + // Each creature you control with menace can't be blocked except by three or more creatures. + addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2 + // + addCard(Zone.BATTLEFIELD, playerB, "Memnite", 5); // 1/1 + + // prepare + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder"); + + // ai must choose all blockers + attack(1, playerA, "Sonorous Howlbonder"); + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites + checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder"); + checkBlockers("all blockers", 1, playerB, "Memnite", "Memnite", "Memnite", "Memnite", "Memnite"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Sonorous Howlbonder", 1); + } + @Test @Ignore // TODO: need exception fix - java.lang.UnsupportedOperationException: Sonorous Howlbonder is blocked by 1 creature(s). It has to be blocked by 3 or more. @@ -814,6 +917,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { @Test @Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1) + // looks like it's impossible for auto-fix (it's can remove blocker, but not add) public void test_MustBeBlockedWithMenace_all_blockers() { // At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s // power until end of turn. That creature must be blocked this combat if able. @@ -844,7 +948,8 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { @Test @Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1) - public void test_MustBeBlockedWithMenace_many_blockers() { + // looks like it's impossible for auto-fix (it's can remove blocker, but not add) + public void test_MustBeBlockedWithMenace_many_low_blockers_human() { // At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s // power until end of turn. That creature must be blocked this combat if able. addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3 @@ -872,6 +977,42 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { assertGraveyardCount(playerA, "Alley Strangler", 0); } + @Test + @Ignore // TODO: blockWithGoodTrade2 must support additional restrictions + public void test_MustBeBlockedWithMenace_many_low_blockers_AI() { + // At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s + // power until end of turn. That creature must be blocked this combat if able. + addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3 + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + // + // Menace + addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3 + // + addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1 + + // If the target creature has menace, two creatures must block it if able. + // (2020-06-23) + + // AI must be forced to choose min x2 low blockers (it's possible to fail here after AI logic improve someday) + + addTarget(playerA, "Alley Strangler"); // boost target + setChoice(playerA, true); // boost target + attack(1, playerA, "Alley Strangler"); + setChoiceAmount(playerA, 1); // assign damage to 1 of 2 blocking memnites + setChoiceAmount(playerA, 1); // assign damage to 2 of 2 blocking memnites + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkAttackers("x1 attacker", 1, playerA, "Alley Strangler"); + checkBlockers("x2 blockers", 1, playerB, "Memnite", "Memnite"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Alley Strangler", 0); + assertGraveyardCount(playerB, "Memnite", 2); // x2 blockers must die + } + @Test public void test_MustBeBlockedWithMenace_low_blockers_auto() { // At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s @@ -946,4 +1087,80 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase { assertLife(playerB, 20 - 4); assertGraveyardCount(playerA, "Alley Strangler", 0); } + + @Test + public void test_MustBeBlockedWithMenace_low_small_blockers_AI() { + // At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s + // power until end of turn. That creature must be blocked this combat if able. + addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3 + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + // + // Menace + addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3 + // + addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); // 1/1 + + // If the target creature has menace, two creatures must block it if able. + // (2020-06-23) + // + // If a creature is required to block a creature with menace, another creature must also block that creature + // if able. If none can, the creature that’s required to block can block another creature or not block at all. + // (2020-04-17) + + // auto-fix block config inside + // AI must ignore such use case + + addTarget(playerA, "Alley Strangler"); // boost target + setChoice(playerA, true); // boost target + attack(1, playerA, "Alley Strangler"); + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkAttackers("x1 attacker", 1, playerA, "Alley Strangler"); + checkBlockers("no blockers", 1, playerB, ""); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 4); + assertGraveyardCount(playerA, "Alley Strangler", 0); + } + + @Test + public void test_MustBeBlockedWithMenace_low_big_blockers_AI() { + // bug: #13290, AI can try to use bigger creature to block + + // At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s + // power until end of turn. That creature must be blocked this combat if able. + addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3 + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + // + // Menace + addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3 + // + addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5 + + // If the target creature has menace, two creatures must block it if able. + // (2020-06-23) + // + // If a creature is required to block a creature with menace, another creature must also block that creature + // if able. If none can, the creature that’s required to block can block another creature or not block at all. + // (2020-04-17) + + // auto-fix block config inside + // AI must ignore BIG creature to wrongly block + + addTarget(playerA, "Alley Strangler"); // boost target + setChoice(playerA, true); // boost target + attack(1, playerA, "Alley Strangler"); + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkAttackers("x1 attacker", 1, playerA, "Alley Strangler"); + checkBlockers("no blockers", 1, playerB, ""); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 4); + assertGraveyardCount(playerA, "Alley Strangler", 0); + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java b/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java index e69ee664e21..35046809512 100644 --- a/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java +++ b/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java @@ -7,6 +7,7 @@ import mage.remote.Session; import mage.view.*; import org.apache.log4j.Logger; import org.jsoup.Jsoup; +import org.junit.Assert; import java.util.List; import java.util.UUID; @@ -33,6 +34,7 @@ public class LoadCallbackClient implements CallbackClient { private final String logsPrefix; private final Boolean showLogsAsHtml; // original game logs in HTML, but it can be converted to txt for more readable console + private String globalProgress = ""; // example: progress 33% [20.cd, 21.__, 17.__], AI game #09: --- public LoadCallbackClient(boolean joinGameChat, String logsPrefix, Boolean showLogsAsHtml) { this.joinGameChat = joinGameChat; @@ -40,6 +42,10 @@ public class LoadCallbackClient implements CallbackClient { this.showLogsAsHtml = showLogsAsHtml; } + protected void updateGlobalProgress(String globalProgress) { + this.globalProgress = globalProgress; + } + @Override public void onNewConnection() { // nothing to do, only one time connection for LoadClient @@ -69,6 +75,12 @@ public class LoadCallbackClient implements CallbackClient { } break; + case GAME_UPDATE: + GameView newGameView = (GameView) callback.getData(); + Assert.assertNotNull("game update event must return game view object", newGameView); + this.gameView = newGameView; + break; + case CHATMESSAGE: { ChatMessage message = (ChatMessage) callback.getData(); String mes = this.showLogsAsHtml ? message.getMessage() : Jsoup.parse(message.getMessage()).text(); @@ -89,7 +101,7 @@ public class LoadCallbackClient implements CallbackClient { case GAME_UPDATE_AND_INFORM: case GAME_INFORM_PERSONAL: { GameClientMessage message = (GameClientMessage) callback.getData(); - gameView = message.getGameView(); + this.gameView = message.getGameView(); // ignore play priority log break; } @@ -169,7 +181,6 @@ public class LoadCallbackClient implements CallbackClient { break; // skip callbacks (no need to react) - case GAME_UPDATE: case JOINED_TABLE: break; @@ -199,7 +210,7 @@ public class LoadCallbackClient implements CallbackClient { mes += "T" + this.gameView.getTurn() + "-" + this.gameView.getStep().getIndex() + ", L:" + p.getLibraryCount() + ", H:" + getPlayer().getHandCount() + ": "; } - return logsPrefix + ": " + mes; + return globalProgress + ", " + logsPrefix + ": " + mes; } public void setSession(Session session) { diff --git a/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java b/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java index 9319ab03b16..8fad6bac7ef 100644 --- a/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java @@ -51,7 +51,7 @@ public class LoadTest { private static final Boolean TEST_SHOW_GAME_LOGS_AS_HTML = false; // html is original format with full data, but can be too bloated private static final String TEST_AI_GAME_MODE = "Freeform Commander Free For All"; private static final String TEST_AI_DECK_TYPE = "Variant Magic - Freeform Commander"; - private static final String TEST_AI_RANDOM_DECK_SETS = "LCI,LCC,WHO"; // set for random generated decks (empty for all sets usage, PELP for lands only - communication test) + private static final String TEST_AI_RANDOM_DECK_SETS = ""; // sets list for random generated decks (GRN,ACR for specific sets, empty for all sets, PELP for lands only - communication test) private static final String TEST_AI_RANDOM_DECK_COLORS_FOR_EMPTY_GAME = "GR"; // colors list for deck generation, empty for all colors private static final String TEST_AI_RANDOM_DECK_COLORS_FOR_AI_GAME = "WUBRG"; private static final String TEST_AI_CUSTOM_DECK_PATH_1 = ""; // custom deck file instead random for player 1 (empty for random) @@ -217,7 +217,6 @@ public class LoadTest { public void playTwoAIGame(String gameName, Integer taskNumber, TasksProgress tasksProgress, long randomSeed, String deckColors, String deckAllowedSets, LoadTestGameResult gameResult) { Assert.assertFalse("need deck colors", deckColors.isEmpty()); - Assert.assertFalse("need allowed sets", deckAllowedSets.isEmpty()); // monitor and game source LoadPlayer monitor = new LoadPlayer("mon", true, gameName + ", mon"); @@ -250,8 +249,13 @@ public class LoadTest { TableView checkGame = monitor.getTable(tableId).orElse(null); TableState state = (checkGame == null ? null : checkGame.getTableState()); - tasksProgress.update(taskNumber, state == TableState.FINISHED, gameView == null ? 0 : gameView.getTurn()); + String finishInfo = ""; + if (state == TableState.FINISHED) { + finishInfo = gameView == null ? "??" : gameView.getStep().getStepShortText().toLowerCase(Locale.ENGLISH); + } + tasksProgress.update(taskNumber, finishInfo, gameView == null ? 0 : gameView.getTurn()); String globalProgress = tasksProgress.getInfo(); + monitor.client.updateGlobalProgress(globalProgress); if (gameView != null && checkGame != null) { logger.info(globalProgress + ", " + checkGame.getTableName() + ": ---"); @@ -283,12 +287,13 @@ public class LoadTest { if (Objects.equals(gameView.getActivePlayerId(), p.getPlayerId())) { activeInfo = " (active)"; } - logger.info(String.format("%s, %s, status: %s - Life=%d; Lib=%d;%s", + logger.info(String.format("%s, %s, status: %s - Life=%d; Lib=%d; CRs=%d%s;", globalProgress, checkGame.getTableName(), p.getName(), p.getLife(), p.getLibraryCount(), + p.getBattlefield().values().stream().filter(CardView::isCreature).mapToInt(x -> 1).sum(), activeInfo )); }); @@ -319,7 +324,7 @@ public class LoadTest { long randomSeed = RandomUtil.nextInt(); LoadTestGameResult gameResult = gameResults.createGame(0, "test game", randomSeed); TasksProgress tasksProgress = new TasksProgress(); - tasksProgress.update(1, true, 0); + tasksProgress.update(1, "", 0); playTwoAIGame("Single AI game", 1, tasksProgress, randomSeed, "WGUBR", TEST_AI_RANDOM_DECK_SETS, gameResult); printGameResults(gameResults); @@ -331,12 +336,13 @@ public class LoadTest { // play multiple AI games with CLIENT side code (catch every GameView changes from the server) int singleGameSID = 0; // set sid for same deck games, set 0 for random decks - int gamesAmount = 10; // games per run - boolean isRunParallel = true; // can generate too much logs in test run, so search server logs for possible errors + + int runTotalGames = 10; + int runMaxParallelGames = 5; // use 1 to run one by one (warning, it's limited by COMPUTER_MAX_THREADS_FOR_SIMULATIONS) ExecutorService executerService; - if (isRunParallel) { - executerService = Executors.newFixedThreadPool(gamesAmount, new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES)); + if (runMaxParallelGames > 1) { + executerService = Executors.newFixedThreadPool(runMaxParallelGames, new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES)); } else { executerService = Executors.newSingleThreadExecutor(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES)); } @@ -344,11 +350,11 @@ public class LoadTest { // save random seeds for repeated results (in decks generating) List seedsList = new ArrayList<>(); if (singleGameSID != 0) { - for (int i = 1; i <= gamesAmount; i++) { + for (int i = 1; i <= runTotalGames; i++) { seedsList.add(singleGameSID); } } else { - for (int i = 1; i <= gamesAmount; i++) { + for (int i = 1; i <= runTotalGames; i++) { seedsList.add(RandomUtil.nextInt()); } } @@ -356,26 +362,42 @@ public class LoadTest { LoadTestGameResultsList gameResults = new LoadTestGameResultsList(); try { TasksProgress tasksProgress = new TasksProgress(); + List gameTasks = new ArrayList<>(); for (int i = 0; i < seedsList.size(); i++) { int gameIndex = i; - tasksProgress.update(gameIndex + 1, true, 0); + tasksProgress.update(gameIndex + 1, "", 0); long randomSeed = seedsList.get(i); logger.info("Game " + (i + 1) + " of " + seedsList.size() + ", RANDOM seed: " + randomSeed); Future gameTask = executerService.submit(() -> { - String gameName = "AI game #" + (gameIndex + 1); + String gameName = String.format("AI game #%02d", gameIndex + 1); LoadTestGameResult gameResult = gameResults.createGame(gameIndex + 1, gameName, randomSeed); playTwoAIGame(gameName, gameIndex + 1, tasksProgress, randomSeed, TEST_AI_RANDOM_DECK_COLORS_FOR_AI_GAME, TEST_AI_RANDOM_DECK_SETS, gameResult); }); + gameTasks.add(gameTask); - if (!isRunParallel) { + if (runMaxParallelGames <= 1) { // run one by one gameTask.get(); } } - if (isRunParallel) { + if (runMaxParallelGames > 1) { // run parallel executerService.shutdown(); - Assert.assertTrue(executerService.awaitTermination(1, TimeUnit.HOURS)); + Assert.assertTrue("running too long", executerService.awaitTermination(1, TimeUnit.HOURS)); + } + + // check errors + int errorsCount = 0; + for (Future task : gameTasks) { + try { + task.get(); + } catch (InterruptedException | ExecutionException e) { + errorsCount++; + logger.error(e, e); + } + } + if (errorsCount > 0) { + Assert.fail(String.format("Found %d critical errors in running games, see logs above", errorsCount)); } } catch (InterruptedException | ExecutionException e) { logger.error(e, e); @@ -583,11 +605,11 @@ public class LoadTest { private static class TasksProgress { private String info; - private final Map finishes = new LinkedHashMap<>(); - private final Map turns = new LinkedHashMap<>(); + private final Map finishes = new LinkedHashMap<>(); // game number, finish on step + private final Map turns = new LinkedHashMap<>(); // game number, current turn - synchronized public void update(Integer taskNumber, boolean newFinish, Integer newTurn) { - Boolean oldFinish = this.finishes.getOrDefault(taskNumber, false); + synchronized public void update(Integer taskNumber, String newFinish, Integer newTurn) { + String oldFinish = this.finishes.getOrDefault(taskNumber, ""); Integer oldTurn = this.turns.getOrDefault(taskNumber, 0); if (!this.finishes.containsKey(taskNumber) || !Objects.equals(oldFinish, newFinish) @@ -599,14 +621,25 @@ public class LoadTest { } private void updateInfo() { - // example: progress [=00, +01, +01, =12, =15, =01, +61] + // example: progress 33% [20.cd, 21.__, 17.__], AI game #09: --- + + int completed = this.finishes.values().stream().mapToInt(x -> x.isEmpty() ? 0 : 1).sum(); + int completedPercent = this.finishes.size() == 0 ? 0 : completed * 100 / this.finishes.size(); + String res = this.finishes.keySet().stream() - .map(taskNumber -> String.format("%s%02d", - this.finishes.getOrDefault(taskNumber, false) ? "=" : "+", - this.turns.getOrDefault(taskNumber, 0) - )) + .map(taskNumber -> { + String turn = String.format("%02d", this.turns.getOrDefault(taskNumber, 0)); + String finishInfo = this.finishes.getOrDefault(taskNumber, ""); + if (finishInfo.isEmpty()) { + // active + return turn + ".__"; + } else { + // done + return turn + "." + finishInfo; + } + }) .collect(Collectors.joining(", ")); - this.info = String.format("progress [%s]", res); + this.info = String.format("progress %d%% [%s]", completedPercent, res); } public String getInfo() { @@ -848,10 +881,32 @@ public class LoadTest { return finalGameView == null ? 0 : this.finalGameView.getPlayers().get(1).getLife(); } + public int getCreaturesCount1() { + return finalGameView == null ? 0 : this.finalGameView.getPlayers().get(0).getBattlefield().values() + .stream() + .filter(CardView::isCreature) + .mapToInt(x -> 1) + .sum(); + } + + public int getCreaturesCount2() { + return finalGameView == null ? 0 : this.finalGameView.getPlayers().get(1).getBattlefield().values() + .stream() + .filter(CardView::isCreature) + .mapToInt(x -> 1) + .sum(); + } + public int getTurn() { return finalGameView == null ? 0 : this.finalGameView.getTurn(); } + public String getTurnInfo() { + int turn = finalGameView == null ? 0 : this.finalGameView.getTurn(); + String stepInfo = finalGameView == null ? "??" : this.finalGameView.getStep().getStepShortText().toLowerCase(Locale.ENGLISH); + return String.format("%02d.%s", turn, stepInfo); + } + public int getDurationMs() { return finalGameView == null ? 0 : ((int) ((this.timeEnded.getTime() - this.timeStarted.getTime()))); } @@ -859,12 +914,16 @@ public class LoadTest { public int getTotalErrorsCount() { return finalGameView == null ? 0 : this.finalGameView.getTotalErrorsCount(); } + + public int getTotalEffectsCount() { + return finalGameView == null ? 0 : this.finalGameView.getTotalEffectsCount(); + } } private static class LoadTestGameResultsList extends HashMap { - private static final String tableFormatHeader = "|%-10s|%-15s|%-20s|%-10s|%-10s|%-15s|%-15s|%-10s|%-20s|%n"; - private static final String tableFormatData = "|%-10s|%15s|%20s|%10s|%10s|%15s|%15s|%10s|%20s|%n"; + private static final String tableFormatHeader = "|%-10s|%-15s|%-20s|%-10s|%-10s|%-10s|%-10s|%-10s|%-15s|%-15s|%-10s|%n"; + private static final String tableFormatData = "|%-10s|%15s|%20s|%10s|%10s|%10s|%10s|%10s|%15s|%15s|%10s|%n"; public LoadTestGameResult createGame(int index, String name, long randomSeed) { if (this.containsKey(index)) { @@ -881,9 +940,12 @@ public class LoadTest { "name", "random sid", "errors", + "effects", "turn", - "player 1", - "player 2", + "life p1", + "life p2", + "creatures p1", + "creatures p2", "time, sec", "time per turn, sec" ); @@ -900,9 +962,12 @@ public class LoadTest { gameResult.name, //"name", String.valueOf(gameResult.randomSeed), // "random sid", String.valueOf(gameResult.getTotalErrorsCount()), // "errors", - String.valueOf(gameResult.getTurn()), //"turn", - String.valueOf(gameResult.getLife1()), //"player 1", - String.valueOf(gameResult.getLife2()), //"player 2", + String.valueOf(gameResult.getTotalEffectsCount()), // "effects", + gameResult.getTurnInfo(), //"turn", + String.valueOf(gameResult.getLife1()), //"life p1", + String.valueOf(gameResult.getLife2()), //"life p2", + String.valueOf(gameResult.getCreaturesCount1()), //"creatures p1", + String.valueOf(gameResult.getCreaturesCount2()), //"creatures p2", String.format("%.3f", (float) gameResult.getDurationMs() / 1000), //"time, sec", String.format("%.3f", ((float) gameResult.getDurationMs() / 1000) / gameResult.getTurn()) //"per turn, sec" ); @@ -915,9 +980,12 @@ public class LoadTest { String.valueOf(this.size()), //"name", "total, secs: " + String.format("%.3f", (float) this.getTotalDurationMs() / 1000), // "random sid", String.valueOf(this.getTotalErrorsCount()), // errors + String.valueOf(this.getAvgEffectsCount()), // effects String.valueOf(this.getAvgTurn()), // turn - String.valueOf(this.getAvgLife1()), // player 1 - String.valueOf(this.getAvgLife2()), // player 2 + String.valueOf(this.getAvgLife1()), // life p1 + String.valueOf(this.getAvgLife2()), // life p2 + String.valueOf(this.getAvgCreaturesCount1()), // creatures p1 + String.valueOf(this.getAvgCreaturesCount2()), // creatures p2 String.valueOf(String.format("%.3f", (float) this.getAvgDurationMs() / 1000)), // time, sec String.valueOf(String.format("%.3f", (float) this.getAvgDurationPerTurnMs() / 1000)) // time per turn, sec ); @@ -928,6 +996,10 @@ public class LoadTest { return this.values().stream().mapToInt(LoadTestGameResult::getTotalErrorsCount).sum(); } + private int getAvgEffectsCount() { + return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getTotalEffectsCount).sum() / this.size(); + } + private int getAvgTurn() { return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getTurn).sum() / this.size(); } @@ -940,6 +1012,14 @@ public class LoadTest { return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getLife2).sum() / this.size(); } + private int getAvgCreaturesCount1() { + return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getCreaturesCount1).sum() / this.size(); + } + + private int getAvgCreaturesCount2() { + return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getCreaturesCount2).sum() / this.size(); + } + private int getTotalDurationMs() { return this.values().stream().mapToInt(LoadTestGameResult::getDurationMs).sum(); } diff --git a/Mage.Tests/src/test/java/org/mage/test/load/SimpleMageClient.java b/Mage.Tests/src/test/java/org/mage/test/load/SimpleMageClient.java index e49c03a0a4b..0ed68a22f05 100644 --- a/Mage.Tests/src/test/java/org/mage/test/load/SimpleMageClient.java +++ b/Mage.Tests/src/test/java/org/mage/test/load/SimpleMageClient.java @@ -28,6 +28,10 @@ public class SimpleMageClient implements MageClient { callbackClient = new LoadCallbackClient(joinGameChat, logsPrefix, showLogsAsHtml); } + protected void updateGlobalProgress(String globalProgress) { + callbackClient.updateGlobalProgress(globalProgress); + } + @Override public MageVersion getVersion() { return version; 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 cef50ca4a8e..15dfcd42146 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 @@ -1996,7 +1996,7 @@ public class TestPlayer implements Player { } } } - checkMultipleBlockers(game, blockedCreaturesList); + checkMultipleBlockers(game, blockedCreaturesList); // search wrong block commands // AI FULL play if no actions available if (!mustBlockByAction && (this.AIPlayer || this.AIRealGameSimulation)) { @@ -3977,6 +3977,26 @@ public class TestPlayer implements Player { return computerPlayer.isDrawsOnOpponentsTurn(); } + @Override + public int getSpeed() { + return computerPlayer.getSpeed(); + } + + @Override + public void initSpeed(Game game) { + computerPlayer.initSpeed(game); + } + + @Override + public void increaseSpeed(Game game) { + computerPlayer.increaseSpeed(game); + } + + @Override + public void decreaseSpeed(Game game) { + computerPlayer.decreaseSpeed(game); + } + @Override public void setPayManaMode(boolean payManaMode) { computerPlayer.setPayManaMode(payManaMode); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckAutoLandsTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckAutoLandsTest.java index 35c08188042..f7180431d1c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckAutoLandsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckAutoLandsTest.java @@ -148,12 +148,15 @@ public class DeckAutoLandsTest extends MageTestPlayerBase { Deck deck = prepareDeck(Arrays.asList( new DeckCardInfo("Amulet of Kroog", "36", "ATQ", 1) // ATQ without lands )); - // must find 2 random sets - List possibleSets1 = TournamentUtil.getLandSetCodeForDeckSets(deck.getExpansionSetCodes()).stream().sorted().collect(Collectors.toList()); - List possibleSets2 = TournamentUtil.getLandSetCodeForDeckSets(deck.getExpansionSetCodes()).stream().sorted().collect(Collectors.toList()); - Assert.assertEquals("must find 1 random set, try 1", 1, possibleSets1.size()); - Assert.assertEquals("must find 1 random set, try 2", 1, possibleSets2.size()); - Assert.assertNotEquals("must find random sets, try 3", possibleSets1.get(0), possibleSets2.get(0)); + + // must find random sets + int tries = 3; + List possibleSets = new ArrayList<>(); + for (int i = 0; i < tries; ++i) { + possibleSets.addAll(TournamentUtil.getLandSetCodeForDeckSets(deck.getExpansionSetCodes()).stream().sorted().collect(Collectors.toList())); + } + Assert.assertEquals("must find 1 set per request, but get " + possibleSets, tries, possibleSets.size()); + Assert.assertNotEquals("must find different random sets, but get " + possibleSets, 1, possibleSets.stream().distinct().count()); } private void assertPossibleSets( diff --git a/Mage.Verify/pom.xml b/Mage.Verify/pom.xml index ac62b70e9db..31b9493ede0 100644 --- a/Mage.Verify/pom.xml +++ b/Mage.Verify/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage-verify diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index 1da2835fdfc..892bfadafbe 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -70,7 +70,7 @@ public class VerifyCardDataTest { private static final Logger logger = Logger.getLogger(VerifyCardDataTest.class); - private static final String FULL_ABILITIES_CHECK_SET_CODES = "MH3;M3C"; // check ability text due mtgjson, can use multiple sets like MAT;CMD or * for all + private static final String FULL_ABILITIES_CHECK_SET_CODES = "DFT"; // check ability text due mtgjson, can use multiple sets like MAT;CMD or * for all private static final boolean CHECK_ONLY_ABILITIES_TEXT = false; // use when checking text locally, suppresses unnecessary checks and output messages private static final boolean CHECK_COPYABLE_FIELDS = true; // disable for better verify test performance @@ -102,7 +102,8 @@ public class VerifyCardDataTest { "shroud", "banding", "flanking", "horsemanship", "legendary landwalk" ); - private static final List doubleWords = new ArrayList<>(); + private static final List doubleWords = new ArrayList<>(); // for inner calc + private static final List etbTriggerPhrases = new ArrayList<>(); // for inner calc static { // numbers @@ -143,14 +144,12 @@ public class VerifyCardDataTest { skipListAddName(SKIP_LIST_TYPE, "UNH", "Old Fogey"); // uses summon word as a joke card skipListAddName(SKIP_LIST_TYPE, "UND", "Old Fogey"); skipListAddName(SKIP_LIST_TYPE, "UST", "capital offense"); // uses "instant" instead "Instant" as a joke card - skipListAddName(SKIP_LIST_TYPE, "DFT", "Venomsac Lagac"); // temporary // subtype // skipListAddName(SKIP_LIST_SUBTYPE, set, cardName); skipListAddName(SKIP_LIST_SUBTYPE, "UGL", "Miss Demeanor"); // uses multiple types as a joke card: Lady, of, Proper, Etiquette skipListAddName(SKIP_LIST_SUBTYPE, "UGL", "Elvish Impersonators"); // subtype is "Elves" pun skipListAddName(SKIP_LIST_SUBTYPE, "UND", "Elvish Impersonators"); - skipListAddName(SKIP_LIST_SUBTYPE, "DFT", "Venomsac Lagac"); // temporary // number // skipListAddName(SKIP_LIST_NUMBER, set, cardName); @@ -1991,6 +1990,7 @@ public class VerifyCardDataTest { } String refLowerText = ref.text.toLowerCase(Locale.ENGLISH); + String cardLowerText = String.join("\n", card.getRules()).toLowerCase(Locale.ENGLISH); // special check: kicker ability must be in rules if (card.getAbilities().containsClass(MultikickerAbility.class) && card.getRules().stream().noneMatch(rule -> rule.contains("Multikicker"))) { @@ -2050,6 +2050,21 @@ public class VerifyCardDataTest { fail(card, "abilities", "mutate cards aren't implemented and shouldn't be available"); } + // special check: some new creature's ETB must use When this creature enters instead When {this} enters + if (EntersBattlefieldTriggeredAbility.ENABLE_TRIGGER_PHRASE_AUTO_FIX) { + if (etbTriggerPhrases.isEmpty()) { + etbTriggerPhrases.addAll(EntersBattlefieldTriggeredAbility.getPossibleTriggerPhrases()); + Assert.assertTrue(etbTriggerPhrases.get(0).startsWith("when")); + } + if (refLowerText.contains("when")) { + for (String needTriggerPhrase : etbTriggerPhrases) { + if (refLowerText.contains(needTriggerPhrase) && !cardLowerText.contains(needTriggerPhrase)) { + fail(card, "abilities", "wrong creature's ETB trigger phrase, must use: " + needTriggerPhrase); + } + } + } + } + // special check: wrong dies triggers (there are also a runtime check on wrong usage, see isInUseableZoneDiesTrigger) Set ignoredCards = new HashSet<>(); ignoredCards.add("Caller of the Claw"); @@ -2096,7 +2111,7 @@ public class VerifyCardDataTest { continue; } boolean isDiesAbility = rules.contains("die ") - || rules.contains("dies ") + || (rules.contains("dies ") && !rules.contains("dies this turn")) || rules.contains("die,") || rules.contains("dies,"); boolean isPutToGraveAbility = rules.contains("put into") diff --git a/Mage/pom.xml b/Mage/pom.xml index e5c1184ee14..6713c55dccb 100644 --- a/Mage/pom.xml +++ b/Mage/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 mage diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbility.java b/Mage/src/main/java/mage/abilities/TriggeredAbility.java index f948f9ffe4b..24266aeaaad 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbility.java @@ -104,4 +104,6 @@ public interface TriggeredAbility extends Ability { GameEvent getTriggerEvent(); TriggeredAbility setTriggerPhrase(String triggerPhrase); + + String getTriggerPhrase(); } diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java index cb87938acdc..0daef751810 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java @@ -132,6 +132,11 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge return this; } + @Override + public String getTriggerPhrase() { + return this.triggerPhrase; + } + @Override public void setTriggerEvent(GameEvent triggerEvent) { this.triggerEvent = triggerEvent; diff --git a/Mage/src/main/java/mage/abilities/common/ActivateAsSorceryActivatedAbility.java b/Mage/src/main/java/mage/abilities/common/ActivateAsSorceryActivatedAbility.java index e53490ee343..4eb998547f3 100644 --- a/Mage/src/main/java/mage/abilities/common/ActivateAsSorceryActivatedAbility.java +++ b/Mage/src/main/java/mage/abilities/common/ActivateAsSorceryActivatedAbility.java @@ -9,6 +9,8 @@ import mage.constants.Zone; public class ActivateAsSorceryActivatedAbility extends ActivatedAbilityImpl { + private boolean showActivateText = true; + public ActivateAsSorceryActivatedAbility(Effect effect, Cost cost) { this(Zone.BATTLEFIELD, effect, cost); } @@ -20,6 +22,7 @@ public class ActivateAsSorceryActivatedAbility extends ActivatedAbilityImpl { protected ActivateAsSorceryActivatedAbility(final ActivateAsSorceryActivatedAbility ability) { super(ability); + this.showActivateText = ability.showActivateText; } @Override @@ -27,9 +30,18 @@ public class ActivateAsSorceryActivatedAbility extends ActivatedAbilityImpl { return new ActivateAsSorceryActivatedAbility(this); } + public ActivateAsSorceryActivatedAbility withShowActivateText(boolean showActivateText) { + this.showActivateText = showActivateText; + return this; + } + @Override public String getRule() { String superRule = super.getRule(); + if (!showActivateText) { + return superRule; + } + String newText = (mayActivate == TargetController.OPPONENT ? " Only your opponents may activate this ability and only as a sorcery." : " Activate only as a sorcery."); diff --git a/Mage/src/main/java/mage/abilities/common/EntersBattlefieldTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/EntersBattlefieldTriggeredAbility.java index 1ebaf677b9d..0a8aeb373d6 100644 --- a/Mage/src/main/java/mage/abilities/common/EntersBattlefieldTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/EntersBattlefieldTriggeredAbility.java @@ -2,22 +2,33 @@ package mage.abilities.common; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; +import mage.cards.Card; +import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; +import java.util.Arrays; +import java.util.List; + /** * @author BetaSteward_at_googlemail.com */ public class EntersBattlefieldTriggeredAbility extends TriggeredAbilityImpl { + static public boolean ENABLE_TRIGGER_PHRASE_AUTO_FIX = false; + public EntersBattlefieldTriggeredAbility(Effect effect) { this(effect, false); } public EntersBattlefieldTriggeredAbility(Effect effect, boolean optional) { super(Zone.ALL, effect, optional); // Zone.All because a creature with trigger can be put into play and be sacrificed during the resolution of an effect (discard Obstinate Baloth with Smallpox) - this.withRuleTextReplacement(true); // default true to replace "{this}" with "it" + this.withRuleTextReplacement(true); // default true to replace "{this}" with "it" or "this creature" + + // warning, it's impossible to add text auto-replacement for creatures here (When this creature enters), + // so it was implemented in CardImpl.addAbility instead + // see https://github.com/magefree/mage/issues/12791 setTriggerPhrase("When {this} enters, "); } @@ -43,4 +54,59 @@ public class EntersBattlefieldTriggeredAbility extends TriggeredAbilityImpl { public EntersBattlefieldTriggeredAbility copy() { return new EntersBattlefieldTriggeredAbility(this); } + + @Override + public EntersBattlefieldTriggeredAbility setTriggerPhrase(String triggerPhrase) { + super.setTriggerPhrase(triggerPhrase); + return this; + } + + /** + * Find description of "{this}" like "this creature" + */ + static public String getThisObjectDescription(Card card) { + // prepare {this} description + + // short names like Aatchik for Aatchik, Emerald Radian + // except: Mu Yanling, Wind Rider (maybe related to spaces in name) + List parts = Arrays.asList(card.getName().split(",")); + if (parts.size() > 1 && !parts.get(0).contains(" ")) { + return parts.get(0); + } + + // some types have priority, e.g. Vehicle instead artifact, example: Boommobile + if (card.getSubtype().contains(SubType.VEHICLE)) { + return "this Vehicle"; + } + if (card.getSubtype().contains(SubType.AURA)) { + return "this Aura"; + } + + // by priority + if (card.isCreature()) { + return "this creature"; + } else if (card.isPlaneswalker()) { + return "this planeswalker"; + } else if (card.isLand()) { + return "this land"; + } else if (card.isEnchantment()) { + return "this enchantment"; + } else if (card.isArtifact()) { + return "this artifact"; + } else { + return "this permanent"; + } + } + + public static List getPossibleTriggerPhrases() { + // for verify tests - must be same list as above (only {this} relates phrases) + return Arrays.asList( + "when this creature enters", + "when this planeswalker enters", + "when this land enters", + "when this enchantment enters", + "when this artifact enters", + "when this permanent enters" + ); + } } diff --git a/Mage/src/main/java/mage/abilities/common/LimitedTimesPerTurnActivatedAbility.java b/Mage/src/main/java/mage/abilities/common/LimitedTimesPerTurnActivatedAbility.java index aaebd872795..9f25acf57b4 100644 --- a/Mage/src/main/java/mage/abilities/common/LimitedTimesPerTurnActivatedAbility.java +++ b/Mage/src/main/java/mage/abilities/common/LimitedTimesPerTurnActivatedAbility.java @@ -13,6 +13,10 @@ import mage.util.CardUtil; */ public class LimitedTimesPerTurnActivatedAbility extends ActivatedAbilityImpl { + public LimitedTimesPerTurnActivatedAbility(Effect effect, Cost cost) { + this(Zone.BATTLEFIELD, effect, cost); + } + public LimitedTimesPerTurnActivatedAbility(Zone zone, Effect effect, Cost cost) { this(zone, effect, cost, 1); } diff --git a/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java b/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java index 4a66080a057..d97c23e9c16 100644 --- a/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java +++ b/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java @@ -10,6 +10,7 @@ import mage.cards.Card; import mage.constants.*; import mage.game.Game; import mage.game.permanent.Permanent; +import mage.util.CardUtil; /** * @author TheElk801 @@ -38,9 +39,21 @@ class MaxSpeedAbilityEffect extends ContinuousEffectImpl { private final Ability ability; + private static Duration getDuration(Ability ability) { + switch (ability.getZone()) { + case BATTLEFIELD: + return Duration.WhileOnBattlefield; + case GRAVEYARD: + return Duration.WhileInGraveyard; + default: + return Duration.Custom; + } + } + MaxSpeedAbilityEffect(Ability ability) { - super(Duration.Custom, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + super(getDuration(ability), Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); this.ability = ability; + this.ability.setRuleVisible(false); } private MaxSpeedAbilityEffect(final MaxSpeedAbilityEffect effect) { @@ -73,6 +86,6 @@ class MaxSpeedAbilityEffect extends ContinuousEffectImpl { @Override public String getText(Mode mode) { - return "Max speed — " + ability.getRule(); + return "Max speed — " + CardUtil.getTextWithFirstCharUpperCase(ability.getRule()); } } diff --git a/Mage/src/main/java/mage/abilities/common/MayCastFromGraveyardSourceAbility.java b/Mage/src/main/java/mage/abilities/common/MayCastFromGraveyardSourceAbility.java index a86ac17e6c6..00f77b14586 100644 --- a/Mage/src/main/java/mage/abilities/common/MayCastFromGraveyardSourceAbility.java +++ b/Mage/src/main/java/mage/abilities/common/MayCastFromGraveyardSourceAbility.java @@ -35,7 +35,7 @@ class MayCastFromGraveyardEffect extends AsThoughEffectImpl { MayCastFromGraveyardEffect() { super(AsThoughEffectType.CAST_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.PutCreatureInPlay); - staticText = "you may cast {this} from your graveyard"; + staticText = "you may cast this card from your graveyard"; } private MayCastFromGraveyardEffect(final MayCastFromGraveyardEffect effect) { 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 0a518e03f9f..e3b234cb4e2 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/ExileFromGraveCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/ExileFromGraveCost.java @@ -18,7 +18,6 @@ import mage.util.CardUtil; import java.util.ArrayList; import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; /** * @author nantuko @@ -27,6 +26,7 @@ public class ExileFromGraveCost extends CostImpl { private final List exiledCards = new ArrayList<>(); private boolean setTargetPointer = false; + private boolean useSourceExileZone = true; public ExileFromGraveCost(TargetCardInYourGraveyard target) { target.withNotTarget(true); @@ -73,6 +73,7 @@ public class ExileFromGraveCost extends CostImpl { super(cost); this.exiledCards.addAll(cost.getExiledCards()); this.setTargetPointer = cost.setTargetPointer; + this.useSourceExileZone = cost.useSourceExileZone; } @Override @@ -90,11 +91,23 @@ public class ExileFromGraveCost extends CostImpl { } Cards cardsToExile = new CardsImpl(); cardsToExile.addAllCards(exiledCards); + + + UUID exileZoneId = null; + String exileZoneName = ""; + if (useSourceExileZone) { + exileZoneId = CardUtil.getExileZoneId(game, source); + exileZoneName = CardUtil.getSourceName(game, source); + } controller.moveCardsToExile( - cardsToExile.getCards(game), source, game, true, - CardUtil.getExileZoneId(game, source), - CardUtil.getSourceName(game, source) + cardsToExile.getCards(game), + source, + game, + true, + exileZoneId, + exileZoneName ); + if (setTargetPointer) { source.getEffects().setTargetPointer(new FixedTargets(cardsToExile.getCards(game), game)); } @@ -118,4 +131,12 @@ public class ExileFromGraveCost extends CostImpl { public List getExiledCards() { return exiledCards; } + + /** + * Put exiled cards to source zone, so next linked ability can find it + */ + public ExileFromGraveCost withSourceExileZone(boolean useSourceExileZone) { + this.useSourceExileZone = useSourceExileZone; + return this; + } } diff --git a/Mage/src/main/java/mage/abilities/costs/common/ExileSourceFromGraveCost.java b/Mage/src/main/java/mage/abilities/costs/common/ExileSourceFromGraveCost.java index 51fad57ba09..4b1ad0b2610 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/ExileSourceFromGraveCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/ExileSourceFromGraveCost.java @@ -16,7 +16,7 @@ import java.util.UUID; public class ExileSourceFromGraveCost extends CostImpl { public ExileSourceFromGraveCost() { - this.text = "exile {this} from your graveyard"; + this.text = "exile this card from your graveyard"; } private ExileSourceFromGraveCost(final ExileSourceFromGraveCost cost) { diff --git a/Mage/src/main/java/mage/abilities/costs/common/ExileSourceWithTimeCountersCost.java b/Mage/src/main/java/mage/abilities/costs/common/ExileSourceWithTimeCountersCost.java new file mode 100644 index 00000000000..4ce88003b7a --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/common/ExileSourceWithTimeCountersCost.java @@ -0,0 +1,88 @@ +package mage.abilities.costs.common; + +import java.util.Locale; +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.costs.Cost; +import mage.abilities.costs.CostImpl; +import mage.abilities.effects.common.continuous.GainSuspendEffect; +import mage.abilities.keyword.SuspendAbility; +import mage.cards.Card; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.Game; +import mage.MageObjectReference; +import mage.players.Player; + + +/** + * @author padfoot + */ +public class ExileSourceWithTimeCountersCost extends CostImpl { + + private final int counters; + private final boolean checksSuspend; + private final boolean givesSuspend; + private final Zone fromZone; + + public ExileSourceWithTimeCountersCost(int counters) { + this (counters, true, false, null); + } + + public ExileSourceWithTimeCountersCost(int counters, boolean givesSuspend, boolean checksSuspend, Zone fromZone) { + this.counters = counters; + this.givesSuspend = givesSuspend; + this.checksSuspend = checksSuspend; + this.fromZone = fromZone; + this.text = "exile {this} " + + ((fromZone != null) ? " from your " + fromZone.toString().toLowerCase(Locale.ENGLISH) : "") + + " and put " + counters + " time counters on it" + + (givesSuspend ? ". It gains suspend" : "") + + (checksSuspend ? ". If it doesn't have suspend, it gains suspend" : ""); + } + + private ExileSourceWithTimeCountersCost(final ExileSourceWithTimeCountersCost cost) { + super(cost); + this.counters = cost.counters; + this.givesSuspend = cost.givesSuspend; + this.checksSuspend = cost.checksSuspend; + this.fromZone = cost.fromZone; + } + + @Override + public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { + Player controller = game.getPlayer(controllerId); + if (controller == null) { + return paid; + } + Card card = game.getCard(source.getSourceId()); + boolean hasSuspend = card.getAbilities(game).containsClass(SuspendAbility.class); + if (card != null && (fromZone == null || fromZone == game.getState().getZone(source.getSourceId()))) { + UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game); + if (controller.moveCardsToExile(card, source, game, true, exileId, "Suspended cards of " + controller.getName())) { + card.addCounters(CounterType.TIME.createInstance(counters), controller.getId(), source, game); + game.informPlayers(controller.getLogName() + " exiles " + card.getLogName() + ((fromZone != null) ? " from their " + fromZone.toString().toLowerCase(Locale.ENGLISH) : "") + " with " + counters + " time counters on it."); + if (givesSuspend || (checksSuspend && !hasSuspend)) { + game.addEffect(new GainSuspendEffect(new MageObjectReference(card, game)), source); + } + } + // 117.11. The actions performed when paying a cost may be modified by effects. + // Even if they are, meaning the actions that are performed don't match the actions + // that are called for, the cost has still been paid. + // so return state here is not important because the user indended to exile the target anyway + paid = true; + } + return paid; + } + + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + return (game.getCard(source.getSourceId()) != null && (fromZone == null || fromZone == game.getState().getZone(source.getSourceId()))); + } + + @Override + public ExileSourceWithTimeCountersCost copy() { + return new ExileSourceWithTimeCountersCost(this); + } +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardTypesInGraveyardCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardTypesInGraveyardCount.java index c307f98adf9..07f2116470e 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardTypesInGraveyardCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardTypesInGraveyardCount.java @@ -3,14 +3,16 @@ package mage.abilities.dynamicvalue.common; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; import mage.cards.Card; +import mage.constants.CardType; import mage.game.Game; import mage.game.permanent.PermanentToken; import mage.players.Player; -import java.util.Collection; -import java.util.Objects; -import java.util.UUID; +import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -20,16 +22,18 @@ public enum CardTypesInGraveyardCount implements DynamicValue { YOU("your graveyard"), ALL("all graveyards"), OPPONENTS("your opponents' graveyards"); + private final String message; + private final CardTypesInGraveyardHint hint; CardTypesInGraveyardCount(String message) { this.message = "the number of card types among cards in " + message; + this.hint = new CardTypesInGraveyardHint(this); } @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - return getStream(game, sourceAbility) - .filter(card -> !card.isCopy() && !(card instanceof PermanentToken)) + return getGraveyardCards(game, sourceAbility) .map(card -> card.getCardType(game)) .flatMap(Collection::stream) .distinct() @@ -52,16 +56,16 @@ public enum CardTypesInGraveyardCount implements DynamicValue { return message; } - private final Stream getStream(Game game, Ability ability) { + public Hint getHint() { + return hint; + } + + public Stream getGraveyardCards(Game game, Ability ability) { Collection playerIds; switch (this) { case YOU: - Player player = game.getPlayer(ability.getControllerId()); - return player == null - ? null : player - .getGraveyard() - .getCards(game) - .stream(); + playerIds = Collections.singletonList(ability.getControllerId()); + break; case OPPONENTS: playerIds = game.getOpponents(ability.getControllerId()); break; @@ -69,13 +73,47 @@ public enum CardTypesInGraveyardCount implements DynamicValue { playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game); break; default: - return null; + throw new IllegalArgumentException("Wrong code usage: miss implementation for " + this); } return playerIds.stream() .map(game::getPlayer) .filter(Objects::nonNull) .map(Player::getGraveyard) .map(graveyard -> graveyard.getCards(game)) - .flatMap(Collection::stream); + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .filter(card -> !card.isCopy() && !(card instanceof PermanentToken)); } } + +class CardTypesInGraveyardHint implements Hint { + + CardTypesInGraveyardCount value; + + CardTypesInGraveyardHint(CardTypesInGraveyardCount value) { + this.value = value; + } + + private CardTypesInGraveyardHint(final CardTypesInGraveyardHint hint) { + this.value = hint.value; + } + + @Override + public String getText(Game game, Ability ability) { + Stream stream = this.value.getGraveyardCards(game, ability); + List types = stream + .map(card -> card.getCardType(game)) + .flatMap(Collection::stream) + .distinct() + .map(CardType::toString) + .sorted() + .collect(Collectors.toList()); + return "Card types in " + this.value.getMessage() + ": " + types.size() + + (types.size() > 0 ? " (" + String.join(", ", types) + ')' : ""); + } + + @Override + public CardTypesInGraveyardHint copy() { + return new CardTypesInGraveyardHint(this); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java index b06aa28ecfe..12ca32d02a9 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java @@ -4,6 +4,9 @@ import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.game.Game; +import mage.players.Player; + +import java.util.Optional; /** * @author TheElk801 @@ -13,8 +16,10 @@ public enum ControllerSpeedCount implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - // TODO: Implement this - return 0; + return Optional + .ofNullable(game.getPlayer(sourceAbility.getControllerId())) + .map(Player::getSpeed) + .orElse(0); } @Override diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedDiscardValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedDiscardValue.java new file mode 100644 index 00000000000..c41ccdc5d13 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedDiscardValue.java @@ -0,0 +1,40 @@ +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.game.Game; + +/** + * @author TheElk801 + */ +public enum SavedDiscardValue implements DynamicValue { + MANY("many"), + MUCH("much"); + + private final String message; + + SavedDiscardValue(String message) { + this.message = "that " + message; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return (Integer) effect.getValue("discarded"); + } + + @Override + public SavedDiscardValue copy() { + return this; + } + + @Override + public String toString() { + return message; + } + + @Override + public String getMessage() { + return ""; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index 5ad74ac0fb1..294ad8831cb 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -1487,8 +1487,12 @@ public class ContinuousEffects implements Serializable { } } + public int getTotalEffectsCount() { + return allEffectsLists.stream().mapToInt(ContinuousEffectsList::size).sum(); + } + @Override public String toString() { - return "Effects: " + allEffectsLists.stream().mapToInt(ContinuousEffectsList::size).sum(); + return "Effects: " + getTotalEffectsCount(); } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java index d01a7ada574..5ca8e531c68 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java @@ -13,6 +13,7 @@ import mage.constants.Outcome; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; +import mage.util.CardUtil; /** * @author LevelX2 @@ -49,7 +50,7 @@ public class ChooseModeEffect extends OneShotEffect { } if (controller != null && sourcePermanent != null) { Choice choice = new ChoiceImpl(true); - choice.setMessage(choiceMessage); + choice.setMessage(choiceMessage + CardUtil.getSourceLogName(game, source)); choice.getChoices().addAll(modes); if (controller.choose(Outcome.Neutral, choice, game)) { if (!game.isSimulation()) { diff --git a/Mage/src/main/java/mage/abilities/effects/common/DiscardOneOrMoreCardsTriggeredAbility.java b/Mage/src/main/java/mage/abilities/effects/common/DiscardOneOrMoreCardsTriggeredAbility.java new file mode 100644 index 00000000000..2f9d60137e9 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/DiscardOneOrMoreCardsTriggeredAbility.java @@ -0,0 +1,49 @@ +package mage.abilities.effects.common; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; + +/** + * @author TheElk801 + */ +public class DiscardOneOrMoreCardsTriggeredAbility extends TriggeredAbilityImpl { + + public DiscardOneOrMoreCardsTriggeredAbility(Effect effect) { + this(effect, false); + } + + public DiscardOneOrMoreCardsTriggeredAbility(Effect effect, boolean optional) { + this(Zone.BATTLEFIELD, effect, optional); + } + + public DiscardOneOrMoreCardsTriggeredAbility(Zone zone, Effect effect, boolean optional) { + super(zone, effect, optional); + setTriggerPhrase("Whenever you discard one or more cards, "); + } + + private DiscardOneOrMoreCardsTriggeredAbility(final DiscardOneOrMoreCardsTriggeredAbility ability) { + super(ability); + } + + @Override + public DiscardOneOrMoreCardsTriggeredAbility copy() { + return new DiscardOneOrMoreCardsTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DISCARDED_CARDS; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!isControlledBy(event.getPlayerId())) { + return false; + } + this.getEffects().setValue("discarded", event.getAmount()); + return true; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/LoseLifeOpponentsYouGainLifeLostEffect.java b/Mage/src/main/java/mage/abilities/effects/common/LoseLifeOpponentsYouGainLifeLostEffect.java index e390384b404..aecb28a6765 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/LoseLifeOpponentsYouGainLifeLostEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/LoseLifeOpponentsYouGainLifeLostEffect.java @@ -43,7 +43,7 @@ public class LoseLifeOpponentsYouGainLifeLostEffect extends OneShotEffect { return true; } int totalLifeLost = game - .getOpponents(source.getControllerId()) + .getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java index c75f26a8aac..03195c6b9e0 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java @@ -36,8 +36,9 @@ public class GainAbilitySourceEffect extends ContinuousEffectImpl { super(duration, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); this.ability = ability; this.onCard = onCard; - this.staticText = "{this} gains " + CardUtil.stripReminderText(ability.getRule()) - + (duration.toString().isEmpty() ? "" : ' ' + duration.toString()); + this.staticText = "{this} " + (duration == Duration.WhileOnBattlefield ? "has" : "gains") + + ' ' + CardUtil.stripReminderText(ability.getRule()) + + (duration.toString().isEmpty() ? "" : ' ' + duration.toString()); this.generateGainAbilityDependencies(ability, null); } diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java index aba3e39d53c..df1d492475b 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java @@ -58,7 +58,7 @@ import java.util.Set; * entering the battlefield, that card isn’t manifested. Its characteristics remain unmodified and it remains in * its previous zone. If it was face up, it remains face up. *

- * 701.34g TODO: need support it + * 701.34g * If a manifested permanent that’s represented by an instant or sorcery card would turn face up, its controller * reveals it and leaves it face down. Abilities that trigger whenever a permanent is turned face up won’t trigger. * diff --git a/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java b/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java index 66c114a8b72..b7172258258 100644 --- a/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java +++ b/Mage/src/main/java/mage/abilities/hint/ConditionTrueHint.java @@ -52,7 +52,7 @@ public class ConditionTrueHint implements Hint { } @Override - public Hint copy() { + public ConditionTrueHint copy() { return new ConditionTrueHint(this); } } diff --git a/Mage/src/main/java/mage/abilities/hint/StaticHint.java b/Mage/src/main/java/mage/abilities/hint/StaticHint.java index 848b09ac568..72d9f64b1d0 100644 --- a/Mage/src/main/java/mage/abilities/hint/StaticHint.java +++ b/Mage/src/main/java/mage/abilities/hint/StaticHint.java @@ -30,7 +30,7 @@ public class StaticHint implements Hint { } @Override - public Hint copy() { + public StaticHint copy() { return new StaticHint(this); } } diff --git a/Mage/src/main/java/mage/abilities/hint/common/CardTypesInGraveyardHint.java b/Mage/src/main/java/mage/abilities/hint/common/CardTypesInGraveyardHint.java deleted file mode 100644 index 81f920d30f0..00000000000 --- a/Mage/src/main/java/mage/abilities/hint/common/CardTypesInGraveyardHint.java +++ /dev/null @@ -1,79 +0,0 @@ -package mage.abilities.hint.common; - -import mage.abilities.Ability; -import mage.abilities.hint.Hint; -import mage.cards.Card; -import mage.constants.CardType; -import mage.game.Game; -import mage.players.Player; - -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * @author JayDi85 - */ -public enum CardTypesInGraveyardHint implements Hint { - - YOU("your graveyard"), - ALL("all graveyards"), - OPPONENTS("your opponents' graveyards"); - private final String message; - - CardTypesInGraveyardHint(String message) { - this.message = message; - } - - @Override - public String getText(Game game, Ability ability) { - Stream stream = getStream(game, ability); - if (stream == null) { - return null; - } - List types = stream - .map(card -> card.getCardType(game)) - .flatMap(Collection::stream) - .distinct() - .map(CardType::toString) - .sorted() - .collect(Collectors.toList()); - return "Card types in " + this.message + ": " + types.size() - + (types.size() > 0 ? " (" + String.join(", ", types) + ')' : ""); - } - - @Override - public Hint copy() { - return this; - } - - private final Stream getStream(Game game, Ability ability) { - Collection playerIds; - switch (this) { - case YOU: - Player player = game.getPlayer(ability.getControllerId()); - return player == null - ? null : player - .getGraveyard() - .getCards(game) - .stream(); - case OPPONENTS: - playerIds = game.getOpponents(ability.getControllerId()); - break; - case ALL: - playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game); - break; - default: - return null; - } - return playerIds.stream() - .map(game::getPlayer) - .filter(Objects::nonNull) - .map(Player::getGraveyard) - .map(graveyard -> graveyard.getCards(game)) - .flatMap(Collection::stream); - } -} diff --git a/Mage/src/main/java/mage/abilities/hint/common/CountersOnPermanentsHint.java b/Mage/src/main/java/mage/abilities/hint/common/CountersOnPermanentsHint.java index 027cd4d7dba..8b44491811b 100644 --- a/Mage/src/main/java/mage/abilities/hint/common/CountersOnPermanentsHint.java +++ b/Mage/src/main/java/mage/abilities/hint/common/CountersOnPermanentsHint.java @@ -1,19 +1,19 @@ package mage.abilities.hint.common; import mage.abilities.Ability; +import mage.abilities.condition.common.CountersOnPermanentsCondition; import mage.abilities.hint.Hint; import mage.counters.Counter; import mage.counters.CounterType; import mage.filter.FilterPermanent; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.abilities.condition.common.CountersOnPermanentsCondition; import mage.util.CardUtil; /** * A hint which keeps track of how many counters of a specific type there are * among some type of permanents - * + * * @author alexander-novo */ public class CountersOnPermanentsHint implements Hint { @@ -23,6 +23,10 @@ public class CountersOnPermanentsHint implements Hint { // Which counter type to count public final CounterType counterType; + public CountersOnPermanentsHint(CountersOnPermanentsCondition condition) { + this(condition.filter, condition.counterType); + } + /** * @param filter Which permanents to consider counters on * @param counterType Which counter type to count @@ -32,12 +36,9 @@ public class CountersOnPermanentsHint implements Hint { this.counterType = counterType; } - /** - * Copy parameters from a {@link CountersOnPermanentsCondition} - */ - public CountersOnPermanentsHint(CountersOnPermanentsCondition condition) { - this.filter = condition.filter; - this.counterType = condition.counterType; + public CountersOnPermanentsHint(final CountersOnPermanentsHint hint) { + this.filter = hint.filter.copy(); + this.counterType = hint.counterType; } @Override @@ -56,7 +57,7 @@ public class CountersOnPermanentsHint implements Hint { } @Override - public Hint copy() { - return this; + public CountersOnPermanentsHint copy() { + return new CountersOnPermanentsHint(this); } } diff --git a/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java b/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java index 8e4ea99d7fb..d6baf07740f 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ChampionAbility.java @@ -47,6 +47,7 @@ import java.util.UUID; public class ChampionAbility extends StaticAbility { protected final String objectDescription; + protected final String etbObjectDescription; /** * Champion one or more creature types or if the subtype array is empty @@ -59,6 +60,8 @@ public class ChampionAbility extends StaticAbility { public ChampionAbility(Card card, SubType... subtypes) { super(Zone.BATTLEFIELD, null); + this.etbObjectDescription = EntersBattlefieldTriggeredAbility.getThisObjectDescription(card); + List subTypes = Arrays.asList(subtypes); FilterControlledPermanent filter; switch (subTypes.size()) { @@ -105,6 +108,7 @@ public class ChampionAbility extends StaticAbility { protected ChampionAbility(final ChampionAbility ability) { super(ability); this.objectDescription = ability.objectDescription; + this.etbObjectDescription = ability.etbObjectDescription; } @Override @@ -115,7 +119,7 @@ public class ChampionAbility extends StaticAbility { @Override public String getRule() { return "Champion " + CardUtil.addArticle(objectDescription) - + " (When this enters the battlefield, sacrifice it unless you exile another " + objectDescription + + " (When " + etbObjectDescription + " enters, sacrifice it unless you exile another " + objectDescription + " you control. When this leaves the battlefield, that card returns to the battlefield.)"; } } diff --git a/Mage/src/main/java/mage/abilities/keyword/DecayedAbility.java b/Mage/src/main/java/mage/abilities/keyword/DecayedAbility.java index b002f4a8931..835f6a1b5eb 100644 --- a/Mage/src/main/java/mage/abilities/keyword/DecayedAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/DecayedAbility.java @@ -18,7 +18,7 @@ public class DecayedAbility extends StaticAbility { super(Zone.BATTLEFIELD, new CantBlockSourceEffect(Duration.WhileOnBattlefield)); this.addSubAbility(new AttacksTriggeredAbility(new CreateDelayedTriggeredAbilityEffect( new AtTheEndOfCombatDelayedTriggeredAbility(new SacrificeSourceEffect()) - ).setText("sacrifice it at end of combat")).setTriggerPhrase("When {this} attacks, ")); + ).setText("sacrifice it at end of combat")).setTriggerPhrase("When {this} attacks, ").setRuleVisible(false)); } private DecayedAbility(final DecayedAbility ability) { @@ -32,6 +32,6 @@ public class DecayedAbility extends StaticAbility { @Override public String getRule() { - return "decayed"; + return "decayed (This creature can't block. When it attacks, sacrifice it at end of combat.)"; } } diff --git a/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java b/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java index f793e3e4ce8..e43fbecdeaa 100644 --- a/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/DelveAbility.java @@ -63,10 +63,16 @@ public class DelveAbility extends SimpleStaticAbility implements AlternateManaPa private static final DynamicValue cardsInGraveyard = new CardsInControllerGraveyardCount(); - public DelveAbility() { + private boolean useSourceExileZone; + + /** + * @param useSourceExileZone - keep exiled cards in linked source zone, so next ability can find it + */ + public DelveAbility(boolean useSourceExileZone) { super(Zone.ALL, null); this.setRuleAtTheTop(true); this.addHint(new ValueHint("Cards in your graveyard", cardsInGraveyard)); + this.useSourceExileZone = useSourceExileZone; } protected DelveAbility(final DelveAbility ability) { @@ -101,8 +107,11 @@ public class DelveAbility extends SimpleStaticAbility implements AlternateManaPa unpaidAmount = 1; } specialAction.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard( - 0, Math.min(controller.getGraveyard().size(), unpaidAmount), - new FilterCard("cards from your graveyard"), true))); + 0, + Math.min(controller.getGraveyard().size(), unpaidAmount), + new FilterCard("cards from your graveyard"), + true + )).withSourceExileZone(this.useSourceExileZone)); if (specialAction.canActivate(source.getControllerId(), game).canActivate()) { game.getState().getSpecialActions().add(specialAction); } diff --git a/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java b/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java index 5779883b9fe..7e9d239c38e 100644 --- a/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/DredgeAbility.java @@ -9,7 +9,6 @@ import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.players.Player; import mage.util.CardUtil; @@ -63,13 +62,16 @@ class DredgeEffect extends ReplacementEffectImpl { if (sourceCard == null) { return false; } - Player owner = game.getPlayer(game.getCard(source.getSourceId()).getOwnerId()); - if (owner != null - && owner.getLibrary().size() >= amount - && owner.chooseUse(outcome, new StringBuilder("Dredge ").append(sourceCard.getLogName()). - append("? (").append(amount).append(" cards are milled)").toString(), source, game)) { + Player owner = game.getPlayer(sourceCard.getOwnerId()); + if (owner == null) { + return false; + } + + String message = "Dredge " + sourceCard.getLogName() + "? (" + amount + " cards are milled)"; + + if (owner.getLibrary().size() >= amount && owner.chooseUse(outcome, message, source, game)) { if (!game.isSimulation()) { - game.informPlayers(new StringBuilder(owner.getLogName()).append(" dredges ").append(sourceCard.getLogName()).toString()); + game.informPlayers(owner.getLogName() + " dredges " + sourceCard.getLogName() + CardUtil.getSourceLogName(game, source)); } owner.millCards(amount, source, game); owner.moveCards(sourceCard, Zone.HAND, source, game); @@ -85,9 +87,14 @@ class DredgeEffect extends ReplacementEffectImpl { @Override public boolean applies(GameEvent event, Ability source, Game game) { - Player owner = game.getPlayer(game.getCard(source.getSourceId()).getOwnerId()); - return (owner != null - && event.getPlayerId().equals(owner.getId()) - && owner.getLibrary().size() >= amount); + Card card = game.getCard(source.getSourceId()); + if (card == null) { + return false; + } + Player owner = game.getPlayer(card.getOwnerId()); + if (owner == null) { + return false; + } + return event.getPlayerId().equals(owner.getId()) && owner.getLibrary().size() >= amount; } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java b/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java index 5e08f7bef1a..4142d96a8eb 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java @@ -36,21 +36,25 @@ import java.util.*; public class HideawayAbility extends EntersBattlefieldTriggeredAbility { private final int amount; + private final String etbObjectDescription; - public HideawayAbility(int amount) { + public HideawayAbility(Card card, int amount) { super(new HideawayExileEffect(amount)); this.amount = amount; this.addWatcher(new HideawayWatcher()); + + this.etbObjectDescription = EntersBattlefieldTriggeredAbility.getThisObjectDescription(card); } private HideawayAbility(final HideawayAbility ability) { super(ability); this.amount = ability.amount; + this.etbObjectDescription = ability.etbObjectDescription; } @Override public String getRule() { - return "Hideaway " + this.amount + " (When this permanent enters the battlefield, look at the top " + return "Hideaway " + this.amount + " (When " + this.etbObjectDescription + " enters, look at the top " + CardUtil.numberToText(this.amount) + " cards of your library, exile one face down, " + "then put the rest on the bottom of your library in a random order.)"; } diff --git a/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java b/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java index 1c8d8c5516c..ecbafd688f4 100644 --- a/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java @@ -1,17 +1,21 @@ package mage.abilities.keyword; import mage.abilities.StaticAbility; +import mage.abilities.dynamicvalue.common.ControllerSpeedCount; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; import mage.constants.Zone; /** - * TODO: Implement this - * * @author TheElk801 */ public class StartYourEnginesAbility extends StaticAbility { + private static final Hint hint = new ValueHint("Your current speed", ControllerSpeedCount.instance); + public StartYourEnginesAbility() { super(Zone.BATTLEFIELD, null); + this.addHint(hint); } private StartYourEnginesAbility(final StartYourEnginesAbility ability) { @@ -25,6 +29,6 @@ public class StartYourEnginesAbility extends StaticAbility { @Override public String getRule() { - return "Start your engines!"; + return "start your engines!"; } } diff --git a/Mage/src/main/java/mage/abilities/mana/builder/common/ActivatedAbilityManaBuilder.java b/Mage/src/main/java/mage/abilities/mana/builder/common/ActivatedAbilityManaBuilder.java new file mode 100644 index 00000000000..9de687331b5 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/mana/builder/common/ActivatedAbilityManaBuilder.java @@ -0,0 +1,52 @@ +package mage.abilities.mana.builder.common; + +import mage.ConditionalMana; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.costs.Cost; +import mage.abilities.mana.builder.ConditionalManaBuilder; +import mage.abilities.mana.conditional.ManaCondition; +import mage.game.Game; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public class ActivatedAbilityManaBuilder extends ConditionalManaBuilder { + + @Override + public ConditionalMana build(Object... options) { + return new ActivatedAbilityConditionalMana(this.mana); + } + + @Override + public String getRule() { + return "Spend this mana only to activate abilities"; + } +} + +class ActivatedAbilityConditionalMana extends ConditionalMana { + + public ActivatedAbilityConditionalMana(Mana mana) { + super(mana); + staticText = "Spend this mana only to activate abilities"; + addCondition(new ActivatedAbilityManaCondition()); + } +} + +class ActivatedAbilityManaCondition extends ManaCondition implements Condition { + + @Override + public boolean apply(Game game, Ability source) { + return source != null + && !source.isActivated() + && source.isActivatedAbility(); + } + + @Override + public boolean apply(Game game, Ability source, UUID originalId, Cost costsToPay) { + return apply(game, source); + } +} diff --git a/Mage/src/main/java/mage/abilities/meta/OrTriggeredAbility.java b/Mage/src/main/java/mage/abilities/meta/OrTriggeredAbility.java index b3d2851fb42..2383d0bbf9a 100644 --- a/Mage/src/main/java/mage/abilities/meta/OrTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/meta/OrTriggeredAbility.java @@ -42,7 +42,7 @@ public class OrTriggeredAbility extends TriggeredAbilityImpl { //Remove useless data ability.getEffects().clear(); - for(Watcher watcher : ability.getWatchers()) { + for (Watcher watcher : ability.getWatchers()) { super.addWatcher(watcher); } @@ -51,6 +51,21 @@ public class OrTriggeredAbility extends TriggeredAbilityImpl { } } setTriggerPhrase(generateTriggerPhrase()); + + // runtime check: enters and sacrifice must use Zone.ALL, see https://github.com/magefree/mage/issues/12826 + boolean haveEnters = false; + boolean haveSacrifice = false; + for (Ability ability : abilities) { + if (ability.getRule().toLowerCase(Locale.ENGLISH).contains("enters")) { + haveEnters = true; + } + if (ability.getRule().toLowerCase(Locale.ENGLISH).contains("sacrifice")) { + haveSacrifice = true; + } + } + if (zone != Zone.ALL && haveEnters && haveSacrifice) { + throw new IllegalArgumentException("Wrong code usage: on enters and sacrifice OrTriggeredAbility must use Zone.ALL"); + } } public OrTriggeredAbility(OrTriggeredAbility ability) { diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index fa2462a09ed..e6c7aa4b9ae 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -4,6 +4,7 @@ import mage.MageObject; import mage.MageObjectImpl; import mage.Mana; import mage.abilities.*; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.continuous.HasSubtypesSourceEffect; import mage.abilities.keyword.ChangelingAbility; @@ -343,6 +344,19 @@ public abstract class CardImpl extends MageObjectImpl implements Card { } } } + + // rules fix: workaround to fix "When {this} enters" into "When this xxx enters" + if (EntersBattlefieldTriggeredAbility.ENABLE_TRIGGER_PHRASE_AUTO_FIX) { + if (ability instanceof TriggeredAbility) { + TriggeredAbility triggeredAbility = ((TriggeredAbility) ability); + if (triggeredAbility.getTriggerPhrase() != null && triggeredAbility.getTriggerPhrase().startsWith("When {this} enters")) { + // there are old sets with old oracle, but it's ok for newer sets, so keep that rules fix + // see https://github.com/magefree/mage/issues/12791 + String etbDescription = EntersBattlefieldTriggeredAbility.getThisObjectDescription(this); + triggeredAbility.setTriggerPhrase(triggeredAbility.getTriggerPhrase().replace("{this}", etbDescription)); + } + } + } } protected void addAbility(Ability ability, Watcher watcher) { diff --git a/Mage/src/main/java/mage/cards/repository/CardRepository.java b/Mage/src/main/java/mage/cards/repository/CardRepository.java index a8ce11f6408..24484308724 100644 --- a/Mage/src/main/java/mage/cards/repository/CardRepository.java +++ b/Mage/src/main/java/mage/cards/repository/CardRepository.java @@ -147,6 +147,9 @@ public enum CardRepository { if (card.getMeldsToCardName() != null && !card.getMeldsToCardName().isEmpty()) { namesList.add(card.getMeldsToCardName()); } + if (card.getAdventureSpellName() != null && !card.getAdventureSpellName().isEmpty()) { + namesList.add(card.getAdventureSpellName()); + } } public static Boolean haveSnowLands(String setCode) { @@ -157,7 +160,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { addNewNames(card, names); @@ -173,7 +176,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%')); List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { @@ -190,7 +193,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); Where where = qb.where(); where.and( where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'), @@ -211,7 +214,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%')); List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { @@ -228,7 +231,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%')); List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { @@ -245,7 +248,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%')); List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { @@ -262,7 +265,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); Where where = qb.where(); where.and( where.not().like("types", '%' + CardType.CREATURE.name() + '%'), @@ -283,7 +286,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); Where where = qb.where(); where.and( where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'), diff --git a/Mage/src/main/java/mage/cards/repository/TokenRepository.java b/Mage/src/main/java/mage/cards/repository/TokenRepository.java index ed25e535d39..a3a86c78c9e 100644 --- a/Mage/src/main/java/mage/cards/repository/TokenRepository.java +++ b/Mage/src/main/java/mage/cards/repository/TokenRepository.java @@ -35,6 +35,7 @@ public enum TokenRepository { public static final String XMAGE_IMAGE_NAME_RADIATION = "Radiation"; public static final String XMAGE_IMAGE_NAME_THE_RING = "The Ring"; public static final String XMAGE_IMAGE_NAME_HELPER_EMBLEM = "Helper Emblem"; + public static final String XMAGE_IMAGE_NAME_SPEED = "Speed"; private static final Logger logger = Logger.getLogger(TokenRepository.class); @@ -310,6 +311,9 @@ public enum TokenRepository { // The Ring res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_RING, 1, "https://api.scryfall.com/cards/tltr/H13/en?format=image")); + // Speed + res.add(createXmageToken(XMAGE_IMAGE_NAME_SPEED, 1, "https://api.scryfall.com/cards/tdft/14/en?format=image&&face=back")); + // Helper emblem (for global card hints) // use backface for it res.add(createXmageToken(XMAGE_IMAGE_NAME_HELPER_EMBLEM, 1, "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg")); diff --git a/Mage/src/main/java/mage/constants/TargetController.java b/Mage/src/main/java/mage/constants/TargetController.java index 2737b52adba..819f621c78f 100644 --- a/Mage/src/main/java/mage/constants/TargetController.java +++ b/Mage/src/main/java/mage/constants/TargetController.java @@ -16,6 +16,7 @@ import java.util.UUID; public enum TargetController { ACTIVE, + INACTIVE, ANY, YOU, NOT_YOU, @@ -85,6 +86,8 @@ public enum TargetController { return card.isOwnedBy(input.getSource().getFirstTarget()); case ACTIVE: return card.isOwnedBy(game.getActivePlayerId()); + case INACTIVE: + return !card.isOwnedBy(game.getActivePlayerId()); case MONARCH: return card.isOwnedBy(game.getMonarchId()); case ANY: @@ -130,6 +133,8 @@ public enum TargetController { return player.getId().equals(input.getSource().getFirstTarget()); case ACTIVE: return game.isActivePlayer(player.getId()); + case INACTIVE: + return !game.isActivePlayer(player.getId()); case MONARCH: return player.getId().equals(game.getMonarchId()); default: @@ -168,6 +173,8 @@ public enum TargetController { return !object.isControlledBy(playerId); case ACTIVE: return object.isControlledBy(game.getActivePlayerId()); + case INACTIVE: + return !object.isControlledBy(game.getActivePlayerId()); case ENCHANTED: Permanent permanent = input.getSource().getSourcePermanentIfItStillExists(game); return permanent != null && input.getObject().isControlledBy(permanent.getAttachedTo()); diff --git a/Mage/src/main/java/mage/designations/DesignationType.java b/Mage/src/main/java/mage/designations/DesignationType.java index 62b77b28c61..dd95ff9808f 100644 --- a/Mage/src/main/java/mage/designations/DesignationType.java +++ b/Mage/src/main/java/mage/designations/DesignationType.java @@ -6,8 +6,8 @@ package mage.designations; public enum DesignationType { THE_MONARCH("The Monarch"), // global CITYS_BLESSING("City's Blessing"), // per player - THE_INITIATIVE("The Initiative"); // global - + THE_INITIATIVE("The Initiative"), // global + SPEED("Speed"); // per player private final String text; DesignationType(String text) { @@ -18,5 +18,4 @@ public enum DesignationType { public String toString() { return text; } - } diff --git a/Mage/src/main/java/mage/designations/Speed.java b/Mage/src/main/java/mage/designations/Speed.java new file mode 100644 index 00000000000..8781ffe847e --- /dev/null +++ b/Mage/src/main/java/mage/designations/Speed.java @@ -0,0 +1,122 @@ +package mage.designations; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.OneShotEffect; +import mage.cards.repository.TokenInfo; +import mage.cards.repository.TokenRepository; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; + +import java.util.Optional; + +/** + * @author TheElk801 + */ +public class Speed extends Designation { + + public Speed() { + super(DesignationType.SPEED); + addAbility(new SpeedTriggeredAbility()); + + TokenInfo foundInfo = TokenRepository.instance.findPreferredTokenInfoForXmage(TokenRepository.XMAGE_IMAGE_NAME_SPEED, null); + if (foundInfo != null) { + this.setExpansionSetCode(foundInfo.getSetCode()); + this.setUsesVariousArt(true); + this.setCardNumber(""); + this.setImageFileName(""); // use default + this.setImageNumber(foundInfo.getImageNumber()); + } else { + // how-to fix: add image to the tokens-database TokenRepository->loadXmageTokens + throw new IllegalArgumentException("Wrong code usage: can't find xmage token info for: " + TokenRepository.XMAGE_IMAGE_NAME_SPEED); + } + } + + private Speed(final Speed card) { + super(card); + } + + @Override + public Speed copy() { + return new Speed(this); + } +} + +class SpeedTriggeredAbility extends TriggeredAbilityImpl { + + SpeedTriggeredAbility() { + super(Zone.ALL, new SpeedEffect()); + setTriggersLimitEachTurn(1); + } + + private SpeedTriggeredAbility(final SpeedTriggeredAbility ability) { + super(ability); + } + + @Override + public SpeedTriggeredAbility copy() { + return new SpeedTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return game.isActivePlayer(getControllerId()) + && game + .getOpponents(getControllerId()) + .contains(event.getTargetId()); + } + + @Override + public boolean checkInterveningIfClause(Game game) { + return Optional + .ofNullable(getControllerId()) + .map(game::getPlayer) + .map(Player::getSpeed) + .map(x -> x < 4) + .orElse(false); + } + + @Override + public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) { + return true; + } + + @Override + public String getRule() { + return "Whenever one or more opponents lose life during your turn, if your speed is less than 4, " + + "increase your speed by 1. This ability triggers only once each turn."; + } +} + +class SpeedEffect extends OneShotEffect { + + SpeedEffect() { + super(Outcome.Benefit); + } + + private SpeedEffect(final SpeedEffect effect) { + super(effect); + } + + @Override + public SpeedEffect copy() { + return new SpeedEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Optional.ofNullable(source.getControllerId()) + .map(game::getPlayer) + .ifPresent(player -> player.increaseSpeed(game)); + return true; + } +} diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java index 57a18bb9a08..dfb7cc6b4c9 100644 --- a/Mage/src/main/java/mage/filter/StaticFilters.java +++ b/Mage/src/main/java/mage/filter/StaticFilters.java @@ -754,6 +754,28 @@ public final class StaticFilters { FILTER_PERMANENT_CREATURE_OR_LAND.setLockedFilter(true); } + public static final FilterPermanent FILTER_PERMANENT_CREATURE_OR_VEHICLE = new FilterPermanent("creature or Vehicle"); + + static { + FILTER_PERMANENT_CREATURE_OR_VEHICLE.add( + Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + FILTER_PERMANENT_CREATURE_OR_VEHICLE.setLockedFilter(true); + } + + public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE = new FilterControlledPermanent("creature or Vehicle you control"); + + static { + FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE.add( + Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE.setLockedFilter(true); + } + public static final FilterCreaturePermanent FILTER_PERMANENT_A_CREATURE = new FilterCreaturePermanent("a creature"); static { diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 0dd53c256e2..2df39b9016d 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -174,7 +174,7 @@ public interface Game extends MageItem, Serializable, Copyable { * * Warning, it will return leaved players until end of turn. For dialogs and one shot effects use excludeLeavedPlayers */ - // TODO: check usage of getOpponents in cards and replace with correct call of excludeLeavedPlayers + // TODO: check usage of getOpponents in cards and replace with correct call of excludeLeavedPlayers, see #13289 default Set getOpponents(UUID playerId) { return getOpponents(playerId, false); } @@ -314,7 +314,9 @@ public interface Game extends MageItem, Serializable, Copyable { Player getLosingPlayer(); - int getTotalErrorsCount(); + int getTotalErrorsCount(); // debug only + + int getTotalEffectsCount(); // debug only //client event methods void addTableEventListener(Listener listener); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 1f554dbdd4e..c60f6499165 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -2862,6 +2862,13 @@ public abstract class GameImpl implements Game { } } } + + // Start Your Engines // Max Speed + if (perm.getAbilities(this).containsClass(StartYourEnginesAbility.class)) { + Optional.ofNullable(perm.getControllerId()) + .map(this::getPlayer) + .ifPresent(player -> player.initSpeed(this)); + } } //201300713 - 704.5k // If a player controls two or more legendary permanents with the same name, that player @@ -3169,7 +3176,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - makeSureCalledOutsideLayersEffects(); + makeSureCalledOutsideLayerEffects(); tableEventSource.fireTableEvent(EventType.INFO, message, this); } @@ -3178,7 +3185,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - makeSureCalledOutsideLayersEffects(); + makeSureCalledOutsideLayerEffects(); tableEventSource.fireTableEvent(EventType.STATUS, message, withTime, withTurnInfo, this); } @@ -3187,7 +3194,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - makeSureCalledOutsideLayersEffects(); + makeSureCalledOutsideLayerEffects(); tableEventSource.fireTableEvent(EventType.UPDATE, null, this); getState().clearLookedAt(); getState().clearRevealed(); @@ -3198,23 +3205,23 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - makeSureCalledOutsideLayersEffects(); + makeSureCalledOutsideLayerEffects(); tableEventSource.fireTableEvent(EventType.END_GAME_INFO, null, this); } @Override public void fireErrorEvent(String message, Exception ex) { - makeSureCalledOutsideLayersEffects(); + makeSureCalledOutsideLayerEffects(); tableEventSource.fireTableEvent(EventType.ERROR, message, ex, this); } - private void makeSureCalledOutsideLayersEffects() { + private void makeSureCalledOutsideLayerEffects() { // very slow, enable/comment it for debug or load/stability tests only // TODO: enable check and remove/rework all wrong usages if (true) return; Arrays.stream(Thread.currentThread().getStackTrace()).forEach(e -> { if (e.toString().contains("GameState.applyEffects")) { - throw new IllegalStateException("Wrong code usage: client side events can't be called from layers effects (wrong informPlayers usage?"); + throw new IllegalStateException("Wrong code usage: client side events can't be called from layers effects (wrong informPlayers usage?)"); } }); } @@ -3542,11 +3549,6 @@ public abstract class GameImpl implements Game { } - protected void removeCreaturesFromCombat() { - //20091005 - 511.3 - getCombat().endCombat(this); - } - @Override public ContinuousEffects getContinuousEffects() { return state.getContinuousEffects(); @@ -3689,6 +3691,11 @@ public abstract class GameImpl implements Game { return this.totalErrorsCount.get(); } + @Override + public int getTotalEffectsCount() { + return this.getContinuousEffects().getTotalEffectsCount(); + } + @Override public void cheat(UUID ownerId, Map commands) { if (commands != null) { @@ -3885,7 +3892,7 @@ public abstract class GameImpl implements Game { @Override public void initTimer(UUID playerId) { if (priorityTime > 0) { - makeSureCalledOutsideLayersEffects(); + makeSureCalledOutsideLayerEffects(); tableEventSource.fireTableEvent(EventType.INIT_TIMER, playerId, null, this); } } @@ -3893,7 +3900,7 @@ public abstract class GameImpl implements Game { @Override public void resumeTimer(UUID playerId) { if (priorityTime > 0) { - makeSureCalledOutsideLayersEffects(); + makeSureCalledOutsideLayerEffects(); tableEventSource.fireTableEvent(EventType.RESUME_TIMER, playerId, null, this); } } @@ -3901,7 +3908,7 @@ public abstract class GameImpl implements Game { @Override public void pauseTimer(UUID playerId) { if (priorityTime > 0) { - makeSureCalledOutsideLayersEffects(); + makeSureCalledOutsideLayerEffects(); tableEventSource.fireTableEvent(EventType.PAUSE_TIMER, playerId, null, this); } } diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index 38c300969ca..5a433b7e04f 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -312,7 +312,23 @@ public class Combat implements Serializable, Copyable { Player player = game.getPlayer(attackingPlayerId); if (player != null) { if (groups.size() > 0) { - game.informPlayers(player.getLogName() + " attacks with " + groups.size() + (groups.size() == 1 ? " creature" : " creatures")); + String defendersInfo = groups.stream() + .map(g -> g.defenderId) + .distinct() + .map(id -> { + Player defPlayer = game.getPlayer(id); + if (defPlayer != null) { + return defPlayer.getLogName(); + } + Permanent defPermanent = game.getPermanentOrLKIBattlefield(id); + if (defPermanent != null) { + return defPermanent.getLogName(); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.joining(", ")); + game.informPlayers(player.getLogName() + " attacks " + defendersInfo + " with " + groups.size() + (groups.size() == 1 ? " creature" : " creatures")); } else { game.informPlayers(player.getLogName() + " skip attack"); } @@ -670,9 +686,25 @@ public class Combat implements Serializable, Copyable { } // choosing until good block configuration + int aiTries = 0; while (true) { + aiTries++; + + if (controller.isComputer() && aiTries > 20) { + // TODO: AI must use real attacker/blocker configuration with all possible combination + // (current human like logic will fail sometime, e.g. with menace and big/low creatures) + // real game: send warning + // test: fast fail + game.informPlayers(controller.getLogName() + ": WARNING - AI can't find good blocker combination and will skip it - report your battlefield to github - " + game.getCombat()); + if (controller.isTestsMode()) { + // how-to fix: AI code must support failed abilities or use cases + throw new IllegalArgumentException("AI can't find good blocker combination"); + } + break; + } + // declare normal blockers - // TODO: need reseach - is it possible to concede on bad blocker configuration (e.g. user can't continue) + // TODO: need research - is it possible to concede on bad blocker configuration (e.g. user can't continue) controller.selectBlockers(source, game, defenderId); if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) { return; @@ -776,18 +808,16 @@ public class Combat implements Serializable, Copyable { /** * Check the block restrictions * - * @param player - * @param game * @return false - if block restrictions were not complied */ - public boolean checkBlockRestrictions(Player player, Game game) { + public boolean checkBlockRestrictions(Player defender, Game game) { int count = 0; boolean blockWasLegal = true; for (CombatGroup group : groups) { count += group.getBlockers().size(); } for (CombatGroup group : groups) { - blockWasLegal &= group.checkBlockRestrictions(game, count); + blockWasLegal &= group.checkBlockRestrictions(game, defender, count); } return blockWasLegal; } diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index c5b5a9cb677..c7caca995fa 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -243,7 +243,7 @@ public class CombatGroup implements Serializable, Copyable { * @param first true for first strike damage step, false for normal damage step * @return true if permanent should deal damage this step */ - private boolean dealsDamageThisStep(Permanent perm, boolean first, Game game) { + public static boolean dealsDamageThisStep(Permanent perm, boolean first, Game game) { if (perm == null) { return false; } @@ -773,11 +773,27 @@ public class CombatGroup implements Serializable, Copyable { } } - public boolean checkBlockRestrictions(Game game, int blockersCount) { + public boolean checkBlockRestrictions(Game game, Player defender, int blockersCount) { boolean blockWasLegal = true; if (attackers.isEmpty()) { return blockWasLegal; } + + // collect possible blockers + Map> possibleBlockers = new HashMap<>(); + for (UUID attackerId : attackers) { + Permanent attacker = game.getPermanent(attackerId); + Set goodBlockers = new HashSet<>(); + for (Permanent blocker : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, defender.getId(), game)) { + if (blocker.canBlock(attackerId, game)) { + goodBlockers.add(blocker.getId()); + } + } + possibleBlockers.put(attacker.getId(), goodBlockers); + } + + // effects: can't block alone + // too much blockers if (blockersCount == 1) { List toBeRemoved = new ArrayList<>(); for (UUID blockerId : getBlockers()) { @@ -802,7 +818,8 @@ public class CombatGroup implements Serializable, Copyable { for (UUID uuid : attackers) { Permanent attacker = game.getPermanent(uuid); if (attacker != null && this.blocked) { - // Check if there are enough blockers to have a legal block + // effects: can't be blocked except by xxx or more creatures + // too few blockers if (attacker.getMinBlockedBy() > 1 && !blockers.isEmpty() && blockers.size() < attacker.getMinBlockedBy()) { for (UUID blockerId : new ArrayList<>(blockers)) { game.getCombat().removeBlocker(blockerId, game); @@ -812,9 +829,16 @@ public class CombatGroup implements Serializable, Copyable { if (!game.isSimulation()) { game.informPlayers(attacker.getLogName() + " can't be blocked except by " + attacker.getMinBlockedBy() + " or more creatures. Blockers discarded."); } - blockWasLegal = false; + + // if there aren't any possible blocker configuration then it's legal due mtg rules + // warning, it's affect AI related logic like other block auto-fixes does, see https://github.com/magefree/mage/pull/13182 + if (attacker.getMinBlockedBy() <= possibleBlockers.getOrDefault(attacker.getId(), Collections.emptySet()).size()) { + blockWasLegal = false; + } } - // Check if there are too many blockers (maxBlockedBy = 0 means no restrictions) + + // effects: can't be blocked by more than xxx creature + // too much blockers if (attacker.getMaxBlockedBy() > 0 && attacker.getMaxBlockedBy() < blockers.size()) { for (UUID blockerId : new ArrayList<>(blockers)) { game.getCombat().removeBlocker(blockerId, game); @@ -827,6 +851,7 @@ public class CombatGroup implements Serializable, Copyable { .append(attacker.getMaxBlockedBy() == 1 ? " creature." : " creatures.") .append(" Blockers discarded.").toString()); } + blockWasLegal = false; } } diff --git a/Mage/src/main/java/mage/game/command/emblems/ChandraSparkHunterEmblem.java b/Mage/src/main/java/mage/game/command/emblems/ChandraSparkHunterEmblem.java new file mode 100644 index 00000000000..843f2f6bb4f --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/ChandraSparkHunterEmblem.java @@ -0,0 +1,38 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAllTriggeredAbility; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.command.Emblem; +import mage.target.common.TargetAnyTarget; + +/** + * @author TheElk801 + */ +public final class ChandraSparkHunterEmblem extends Emblem { + + /** + * Emblem with "Whenever an artifact you control enters, this emblem deals 3 damage to any target." + */ + + public ChandraSparkHunterEmblem() { + super("Emblem Chandra"); + Ability ability = new EntersBattlefieldAllTriggeredAbility( + Zone.COMMAND, new DamageTargetEffect(3, "this emblem"), + StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT, false + ); + ability.addTarget(new TargetAnyTarget()); + this.getAbilities().add(ability); + } + + private ChandraSparkHunterEmblem(final ChandraSparkHunterEmblem card) { + super(card); + } + + @Override + public ChandraSparkHunterEmblem copy() { + return new ChandraSparkHunterEmblem(this); + } +} diff --git a/Mage/src/main/java/mage/game/permanent/PermanentCard.java b/Mage/src/main/java/mage/game/permanent/PermanentCard.java index 7242b774892..b7ef957b100 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentCard.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentCard.java @@ -7,13 +7,11 @@ import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.keyword.NightboundAbility; import mage.abilities.keyword.TransformAbility; -import mage.cards.Card; -import mage.cards.LevelerCard; -import mage.cards.ModalDoubleFacedCard; -import mage.cards.SplitCard; +import mage.cards.*; import mage.constants.SpellAbilityType; import mage.game.Game; import mage.game.events.ZoneChangeEvent; +import mage.players.Player; import java.util.UUID; @@ -45,7 +43,7 @@ public class PermanentCard extends PermanentImpl { } // usage check: you must put to play only real card's part - // if you use it in test code then call CardUtil.getDefaultCardSideForBattlefield for default side + // if you use it in test code or for permanent's copy effects then call CardUtil.getDefaultCardSideForBattlefield for default side // it's a basic check and still allows to create permanent from instant or sorcery boolean goodForBattlefield = true; if (card instanceof ModalDoubleFacedCard) { @@ -185,7 +183,10 @@ public class PermanentCard extends PermanentImpl { // 701.34g. If a manifested permanent that's represented by an instant or sorcery card would turn face up, // its controller reveals it and leaves it face down. Abilities that trigger whenever a permanent // is turned face up won't trigger. - // TODO: add reveal effect + Player player = game.getPlayer(source.getControllerId()); + if (player != null) { + player.revealCards(source, new CardsImpl(this), game); + } return false; } if (super.turnFaceUp(source, game, playerId)) { diff --git a/Mage/src/main/java/mage/game/permanent/token/AshiokNightmareMuseToken.java b/Mage/src/main/java/mage/game/permanent/token/AshiokNightmareMuseToken.java index 1be43c33bcc..707718dc4ca 100644 --- a/Mage/src/main/java/mage/game/permanent/token/AshiokNightmareMuseToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/AshiokNightmareMuseToken.java @@ -66,7 +66,7 @@ class AshiokNightmareMuseTokenEffect extends OneShotEffect { return false; } Set cards = game - .getOpponents(source.getControllerId()) + .getOpponents(source.getControllerId(), true) .stream() .map(game::getPlayer) .filter(Objects::nonNull) diff --git a/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobOozeToken.java b/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobOozeToken.java index 42a7f067a44..176a4829c59 100644 --- a/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobOozeToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/ConsumingBlobOozeToken.java @@ -14,8 +14,6 @@ import mage.constants.Zone; */ public final class ConsumingBlobOozeToken extends TokenImpl { - private static final DynamicValue powerValue = CardTypesInGraveyardCount.YOU; - public ConsumingBlobOozeToken() { super("Ooze Token", "green Ooze creature token with \"This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1.\""); cardType.add(CardType.CREATURE); @@ -26,7 +24,9 @@ public final class ConsumingBlobOozeToken extends TokenImpl { toughness = new MageInt(1); // This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.YOU) + ).addHint(CardTypesInGraveyardCount.YOU.getHint())); } private ConsumingBlobOozeToken(final ConsumingBlobOozeToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/PilotSaddleCrewToken.java b/Mage/src/main/java/mage/game/permanent/token/PilotSaddleCrewToken.java index e96c267aae3..e5fe9a90055 100644 --- a/Mage/src/main/java/mage/game/permanent/token/PilotSaddleCrewToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/PilotSaddleCrewToken.java @@ -11,7 +11,7 @@ import mage.constants.SubType; public final class PilotSaddleCrewToken extends TokenImpl { public PilotSaddleCrewToken() { - super("Pilot Token", "1/1 colorless Pilot creature token with \"This creature saddles Mounts and crews Vehicles as though its power were 2 greater.\""); + super("Pilot Token", "1/1 colorless Pilot creature token with \"This token saddles Mounts and crews Vehicles as though its power were 2 greater.\""); cardType.add(CardType.CREATURE); subtype.add(SubType.PILOT); power = new MageInt(1); diff --git a/Mage/src/main/java/mage/game/permanent/token/TarmogoyfToken.java b/Mage/src/main/java/mage/game/permanent/token/TarmogoyfToken.java index c9107b97f5d..4c24a3ff76d 100644 --- a/Mage/src/main/java/mage/game/permanent/token/TarmogoyfToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/TarmogoyfToken.java @@ -24,7 +24,9 @@ public final class TarmogoyfToken extends TokenImpl { toughness = new MageInt(1); // Tarmogoyf's power is equal to the number of card types among cards in all graveyards and its toughness is equal to that number plus 1. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.ALL))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, + new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.ALL) + ).addHint(CardTypesInGraveyardCount.ALL.getHint())); } private TarmogoyfToken(final TarmogoyfToken token) { diff --git a/Mage/src/main/java/mage/game/permanent/token/VehicleToken.java b/Mage/src/main/java/mage/game/permanent/token/VehicleToken.java new file mode 100644 index 00000000000..58a5031d0c6 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/VehicleToken.java @@ -0,0 +1,30 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.keyword.CrewAbility; +import mage.constants.CardType; +import mage.constants.SubType; + +/** + * @author TheElk801 + */ +public final class VehicleToken extends TokenImpl { + + public VehicleToken() { + super("Vehicle Token", "3/2 colorless Vehicle artifact token with crew 1"); + cardType.add(CardType.ARTIFACT); + subtype.add(SubType.VEHICLE); + power = new MageInt(3); + toughness = new MageInt(2); + + this.addAbility(new CrewAbility(1)); + } + + private VehicleToken(final VehicleToken token) { + super(token); + } + + public VehicleToken copy() { + return new VehicleToken(this); + } +} diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 9566348bc7c..5e8286c5b6e 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -216,6 +216,14 @@ public interface Player extends MageItem, Copyable { boolean isDrawsOnOpponentsTurn(); + int getSpeed(); + + void initSpeed(Game game); + + void increaseSpeed(Game game); + + void decreaseSpeed(Game game); + /** * Returns alternative casting costs a player can cast spells for * @@ -620,6 +628,7 @@ public interface Player extends MageItem, Copyable { *

* Warning, if you use it from continuous effect, then check with extra call * isCanLookAtNextTopLibraryCard + * If you use revealCards with face-down permanents, they will be revealed face up. * * @param source * @param name diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index b1b3434cf55..4db86272845 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -27,6 +27,7 @@ import mage.counters.CounterType; import mage.counters.Counters; import mage.designations.Designation; import mage.designations.DesignationType; +import mage.designations.Speed; import mage.filter.FilterCard; import mage.filter.FilterMana; import mage.filter.FilterPermanent; @@ -153,6 +154,7 @@ public abstract class PlayerImpl implements Player, Serializable { protected boolean canPlotFromTopOfLibrary = false; protected boolean drawsFromBottom = false; protected boolean drawsOnOpponentsTurn = false; + protected int speed = 0; protected FilterPermanent sacrificeCostFilter; protected List alternativeSourceCosts = new ArrayList<>(); @@ -252,6 +254,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary; this.drawsFromBottom = player.drawsFromBottom; this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn; + this.speed = player.speed; this.attachments.addAll(player.attachments); @@ -367,6 +370,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.drawsFromBottom = player.isDrawsFromBottom(); this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn(); this.alternativeSourceCosts = CardUtil.deepCopyObject(((PlayerImpl) player).alternativeSourceCosts); + this.speed = player.getSpeed(); this.topCardRevealed = player.isTopCardRevealed(); @@ -480,6 +484,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.canPlotFromTopOfLibrary = false; this.drawsFromBottom = false; this.drawsOnOpponentsTurn = false; + this.speed = 0; this.sacrificeCostFilter = null; this.alternativeSourceCosts.clear(); @@ -1905,7 +1910,11 @@ public abstract class PlayerImpl implements Player, Serializable { int last = cards.size(); for (Card card : cards.getCards(game)) { current++; - sb.append(GameLog.getColoredObjectName(card)); // TODO: see same usage in OfferingAbility for hide card's id (is it needs for reveal too?!) + if (card instanceof PermanentCard && card.isFaceDown(game)) { + sb.append(GameLog.getColoredObjectName(card.getMainCard())); + } else { + sb.append(GameLog.getColoredObjectName(card)); // TODO: see same usage in OfferingAbility for hide card's id (is it needs for reveal too?!) + } if (current < last) { sb.append(", "); } @@ -4452,11 +4461,7 @@ public abstract class PlayerImpl implements Player, Serializable { } /** - * Only used for AIs - * - * @param ability - * @param game - * @return + * AI related code */ @Override public List getPlayableOptions(Ability ability, Game game) { @@ -4477,6 +4482,9 @@ public abstract class PlayerImpl implements Player, Serializable { return options; } + /** + * AI related code + */ private void addModeOptions(List options, Ability option, Game game) { // TODO: support modal spells with more than one selectable mode (also must use max modes filter) for (Mode mode : option.getModes().values()) { @@ -4499,11 +4507,18 @@ public abstract class PlayerImpl implements Player, Serializable { } } + /** + * AI related code + */ protected void addVariableXOptions(List options, Ability option, int targetNum, Game game) { addTargetOptions(options, option, targetNum, game); } + /** + * AI related code + */ 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 for (Target target : option.getTargets().getUnchosen(game).get(targetNum).getTargetOptions(option, game)) { Ability newOption = option.copy(); if (target instanceof TargetAmount) { @@ -4516,7 +4531,7 @@ public abstract class PlayerImpl implements Player, Serializable { newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true); } } - if (targetNum < option.getTargets().size() - 2) { + if (targetNum < option.getTargets().size() - 2) { // wtf addTargetOptions(options, newOption, targetNum + 1, game); } else if (!option.getCosts().getTargets().isEmpty()) { addCostTargetOptions(options, newOption, 0, game); @@ -4526,6 +4541,9 @@ public abstract class PlayerImpl implements Player, Serializable { } } + /** + * AI related code + */ private void addCostTargetOptions(List options, Ability option, int targetNum, Game game) { for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(playerId, option, game)) { Ability newOption = option.copy(); @@ -4674,6 +4692,37 @@ public abstract class PlayerImpl implements Player, Serializable { return drawsOnOpponentsTurn; } + @Override + public int getSpeed() { + return speed; + } + + @Override + public void initSpeed(Game game) { + if (speed > 0) { + return; + } + speed = 1; + game.getState().addDesignation(new Speed(), game, getId()); + game.informPlayers(this.getLogName() + "'s speed is now 1."); + } + + @Override + public void increaseSpeed(Game game) { + if (speed < 4) { + speed++; + game.informPlayers(this.getLogName() + "'s speed has increased to " + speed); + } + } + + @Override + public void decreaseSpeed(Game game) { + if (speed > 1) { + speed--; + game.informPlayers(this.getLogName() + "'s speed has decreased to " + speed); + } + } + @Override public boolean autoLoseGame() { return false; diff --git a/Mage/src/main/java/mage/target/Target.java b/Mage/src/main/java/mage/target/Target.java index 8ef2e97d69c..9664a0a10ca 100644 --- a/Mage/src/main/java/mage/target/Target.java +++ b/Mage/src/main/java/mage/target/Target.java @@ -79,6 +79,9 @@ public interface Target extends Serializable { boolean isLegal(Ability source, Game game); + /** + * AI related code. Returns all possible different target combinations + */ List getTargetOptions(Ability source, Game game); boolean canChoose(UUID sourceControllerId, Game game); diff --git a/Mage/src/main/java/mage/target/TargetAmount.java b/Mage/src/main/java/mage/target/TargetAmount.java index 68107f825a8..d128d38ec9b 100644 --- a/Mage/src/main/java/mage/target/TargetAmount.java +++ b/Mage/src/main/java/mage/target/TargetAmount.java @@ -1,11 +1,16 @@ 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.constants.Outcome; 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; @@ -118,45 +123,229 @@ public abstract class TargetAmount extends TargetImpl { } @Override - public List getTargetOptions(Ability source, Game game) { + final public List getTargetOptions(Ability source, Game game) { + if (!amountWasSet) { + setAmount(source, game); + } + List options = new ArrayList<>(); Set possibleTargets = possibleTargets(source.getControllerId(), source, game); - addTargets(this, possibleTargets, options, 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); - // debug target variations - //printTargetsVariations(possibleTargets, options); + // calc possible amount variations + addTargets(this, possibleTargets, options, source, game); + printTargetsTableAndVariations("after calc", game, possibleTargets, options, true); return options; } - private void printTargetsVariations(Set possibleTargets, List options) { - // debug target variations - // permanent index + amount - // example: 7 -> 2; 8 -> 3; 9 -> 1 + /** + * 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) + + // 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 = this.remainingAmount * 2; + 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 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("; "))) - .collect(Collectors.toList()); - Collections.sort(res); + .collect(Collectors.joining("; "))).sorted().collect(Collectors.toList()); System.out.println(); - System.out.println(res.stream().collect(Collectors.joining("\n"))); + System.out.println(String.format("Target variations (info): %d", options.size())); + System.out.println(String.join("\n", res)); System.out.println(); } - protected void addTargets(TargetAmount target, Set possibleTargets, List options, Ability source, Game game) { - if (!amountWasSet) { - setAmount(source, game); - } + final protected void addTargets(TargetAmount target, Set possibleTargets, List options, Ability source, Game game) { Set usedTargets = new HashSet<>(); for (UUID targetId : possibleTargets) { usedTargets.add(targetId); diff --git a/Mage/src/main/java/mage/target/TargetImpl.java b/Mage/src/main/java/mage/target/TargetImpl.java index 1a937a54ef4..0aae5458609 100644 --- a/Mage/src/main/java/mage/target/TargetImpl.java +++ b/Mage/src/main/java/mage/target/TargetImpl.java @@ -446,13 +446,6 @@ public abstract class TargetImpl implements Target { return !targets.isEmpty(); } - /** - * Returns all possible different target combinations - * - * @param source - * @param game - * @return - */ @Override public List getTargetOptions(Ability source, Game game) { List options = new ArrayList<>(); diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 7b81b895d68..7ba17e4036f 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -10,6 +10,7 @@ import mage.abilities.costs.VariableCost; import mage.abilities.costs.mana.*; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.SavedDamageValue; +import mage.abilities.dynamicvalue.common.SavedDiscardValue; import mage.abilities.dynamicvalue.common.SavedGainedLifeValue; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.ContinuousEffect; @@ -967,7 +968,9 @@ public final class CardUtil { boolean xValue = amount.toString().equals("X"); if (xValue) { sb.append("X ").append(counter.getName()).append(" counters"); - } else if (amount == SavedDamageValue.MANY || amount == SavedGainedLifeValue.MANY) { + } else if (amount == SavedDamageValue.MANY + || amount == SavedGainedLifeValue.MANY + || amount == SavedDiscardValue.MANY) { sb.append("that many ").append(counter.getName()).append(" counters"); } else { sb.append(counter.getDescription()); @@ -1176,7 +1179,7 @@ public final class CardUtil { .sum(); int remainingValue = maxValue - selectedValue; Set validTargets = new HashSet<>(); - for (UUID id: possibleTargets) { + for (UUID id : possibleTargets) { MageObject mageObject = game.getObject(id); if (mageObject != null && valueMapper.applyAsInt(mageObject) <= remainingValue) { validTargets.add(id); diff --git a/Mage/src/main/java/mage/util/DebugUtil.java b/Mage/src/main/java/mage/util/DebugUtil.java index 51f029ca0d9..09ba1e89659 100644 --- a/Mage/src/main/java/mage/util/DebugUtil.java +++ b/Mage/src/main/java/mage/util/DebugUtil.java @@ -11,6 +11,12 @@ public class DebugUtil { public static boolean NETWORK_SHOW_CLIENT_CALLBACK_MESSAGES_LOG = false; // show all callback messages (server commands) + // AI + // 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 + // cards basic (card panels) public static boolean GUI_CARD_DRAW_OUTER_BORDER = false; public static boolean GUI_CARD_DRAW_INNER_BORDER = false; diff --git a/Mage/src/main/resources/tokens-database.txt b/Mage/src/main/resources/tokens-database.txt index 2ca7c5dea43..8b69c37b086 100644 --- a/Mage/src/main/resources/tokens-database.txt +++ b/Mage/src/main/resources/tokens-database.txt @@ -139,6 +139,13 @@ |Generate|EMBLEM:DSK|Emblem Kaito|||KaitoBaneOfNightmaresEmblem| |Generate|EMBLEM:FDN|Emblem Kaito|||KaitoCunningInfiltratorEmblem| |Generate|EMBLEM:FDN|Emblem Vivien|||VivienReidEmblem| +|Generate|EMBLEM:INR|Emblem Arlinn|||ArlinnEmbracedByTheMoonEmblem| +|Generate|EMBLEM:INR|Emblem Chandra|||ChandraDressedToKillEmblem| +|Generate|EMBLEM:INR|Emblem Jace|||JaceUnravelerOfSecretsEmblem| +|Generate|EMBLEM:INR|Emblem Tamiyo|||TamiyoFieldResearcherEmblem| +|Generate|EMBLEM:INR|Emblem Wrenn|||WrennAndSevenEmblem| +|Generate|EMBLEM:DFT|Emblem Chandra|||ChandraSparkHunterEmblem| + # ALL PLANES # Usage hints: @@ -2422,3 +2429,27 @@ |Generate|TOK:FDN|Spirit|||SpiritWhiteToken| |Generate|TOK:FDN|Treasure|||TreasureToken| |Generate|TOK:FDN|Zombie|||ZombieToken| + +# INR +|Generate|TOK:INR|Blood|||BloodToken| +|Generate|TOK:INR|Clue|||ClueArtifactToken| +|Generate|TOK:INR|Demon|||DemonToken| +|Generate|TOK:INR|Eldrazi Horror|||EldraziHorrorToken| +|Generate|TOK:INR|Elemental|||SeizeTheStormElementalToken| +|Generate|TOK:INR|Human|1||RedHumanToken| +|Generate|TOK:INR|Human|2||HumanToken| +|Generate|TOK:INR|Human Cleric|||HumanClericToken| +|Generate|TOK:INR|Human Soldier|1||HumanSoldierToken| +|Generate|TOK:INR|Human Soldier|2||HumanSoldierTrainingToken| +|Generate|TOK:INR|Human Wizard|||HumanWizardToken| +|Generate|TOK:INR|Insect|||InsectToken| +|Generate|TOK:INR|Spider|||SpiderToken| +|Generate|TOK:INR|Spirit|||SpiritWhiteToken| +|Generate|TOK:INR|Treefolk|||WrennAndSevenTreefolkToken| +|Generate|TOK:INR|Vampire|1||EdgarMarkovToken| +|Generate|TOK:INR|Vampire|2||VampireToken| +|Generate|TOK:INR|Wolf|1||WolfTokenWithDeathtouch| +|Generate|TOK:INR|Wolf|2||WolfToken| +|Generate|TOK:INR|Zombie|1||ZombieToken2| +|Generate|TOK:INR|Zombie|2||ZombieToken| +|Generate|TOK:INR|Zombie|3||ZombieDecayedToken| diff --git a/Makefile b/Makefile index c7e26647203..066d4568749 100644 --- a/Makefile +++ b/Makefile @@ -6,19 +6,28 @@ # Alternatively, you can set this variable in the .env file TARGET_DIR ?= deploy/ -# Note that the proper install script is located under ./Utils/build-and-package.pl -# and that should be used instead. This script is purely for convenience. -# The perl script bundles the artifacts into a single zip -.PHONY: install -install: - # Building project - mvn clean install package -DskipTests +.PHONY: clean +clean: + mvn clean + +.PHONY: build +build: + mvn install package -DskipTests + +.PHONY: package +package: # Packaging Mage.Client to zip - cd Mage.Client && mvn assembly:assembly + cd Mage.Client && mvn assembly:single # Packaging Mage.Server to zip - cd Mage.Server && mvn assembly:assembly + cd Mage.Server && mvn assembly:single # Copying the files to the target directory mkdir -p $(TARGET_DIR) cp ./Mage.Server/target/mage-server.zip $(TARGET_DIR) cp ./Mage.Client/target/mage-client.zip $(TARGET_DIR) +# Note that the proper install script is located under ./Utils/build-and-package.pl +# and that should be used instead. This script is purely for convenience. +# The perl script bundles the artifacts into a single zip +.PHONY: install +install: clean build package + diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index 41de19925d7..db57fb79528 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -56011,7 +56011,7 @@ Bounce Off|Aetherdrift|39|C|{U}|Instant|||Return target creature or Vehicle to i Caelorna, Coral Tyrant|Aetherdrift|40|U|{1}{U}|Legendary Creature - Octopus|0|8|| Diversion Unit|Aetherdrift|41|U|{1}{U}|Artifact Creature - Robot|2|1|Flying${U}, Sacrifice this creature: Counter target instant or sorcery spell unless its controller pays {3}.| Flood the Engine|Aetherdrift|42|C|{2}{U}|Enchantment - Aura|||Enchant creature or Vehicle$When this Aura enters, tap enchanted permanent.$Enchanted permanent loses all abilities and doesn't untap during its controller's untap step.| -Gearseeker Serpent|Aetherdrift|43|C|{5}{U}{U}|Creature - Serpent|5|6|This spell costs {1} less to cast for each artifact you control.${5}{U}: Gearseeker Serpent can't be blocked this turn.| +Gearseeker Serpent|Aetherdrift|43|C|{5}{U}{U}|Creature - Serpent|5|6|Affinity for artifacts${5}{U}: Gearseeker Serpent can't be blocked this turn.| Glitch Ghost Surveyor|Aetherdrift|44|C|{2}{U}|Creature - Spirit Scout|2|2|Flying$Start your engines!$Max speed -- {3}, Exile this card from your graveyard: Draw a card.| Guidelight Optimizer|Aetherdrift|45|C|{1}{U}|Artifact Creature - Robot|2|1|{T}: Add {U}. Spend this mana only to cast an artifact spell or activate an ability.| Howler's Heavy|Aetherdrift|46|C|{3}{U}|Creature - Seal Pirate|3|4|Cycling {1}{U}$When you cycle this card, target creature or Vehicle an opponent controls gets -3/-0 until end of turn.| @@ -56029,7 +56029,7 @@ Riverchurn Monument|Aetherdrift|57|R|{1}{U}|Artifact|||{1}, {T}: Any number of t Roadside Blowout|Aetherdrift|58|U|{2}{U}|Sorcery|||This spell costs {2} less to cast if it targets a permanent with mana value 1.$Return target creature or Vehicle an opponent controls to its owner's hand.$Draw a card.| Sabotage Strategist|Aetherdrift|59|U|{2}{U}{U}|Creature - Vedalken Ranger|2|2|Flying, vigilance$Whenever one or more creatures attack you, those creatures get -1/-0 until end of turn.$Exhaust -- {5}{U}{U}: Put three +1/+1 counters on this creature.| Scrounging Skyray|Aetherdrift|60|U|{1}{U}|Creature - Fish Pirate|1|2|Flying$Whenever you discard one or more cards, put that many +1/+1 counters on this creature.$Cycling {2}| -Skystreak Engineer|Aetherdrift|61|C|{1}{U}|Creature - Human Pilot|1|3|Flying$Exhaust - {4}{U}: Put two +1/+1 counters on this creature.| +Skystreak Engineer|Aetherdrift|61|C|{1}{U}|Creature - Human Pilot|1|3|Flying$Exhaust -- {4}{U}: Put two +1/+1 counters on this creature.| Slick Imitator|Aetherdrift|62|U|{1}{U}|Creature - Ooze|1|3|Start your engines!$Max speed -- {1}, Sacrifice this creature: Copy target spell you control. You may choose new targets for the copy.| Spectral Interference|Aetherdrift|63|C|{1}{U}|Instant|||Counter target artifact or creature spell unless its controller pays {4}.| Spell Pierce|Aetherdrift|64|U|{U}|Instant|||Counter target noncreature spell unless its controller pays {2}.| @@ -56042,7 +56042,7 @@ Transit Mage|Aetherdrift|70|U|{2}{U}|Creature - Human Wizard|2|2|When this creat Trip Up|Aetherdrift|71|C|{3}{U}|Instant|||Target nonland permanent's owner puts it on their choice of the top or bottom of their library.$Cycling {2}| Unstoppable Plan|Aetherdrift|72|R|{2}{U}|Enchantment|||At the beginning of your end step, untap all nonland permanents you control.| Vnwxt, Verbose Host|Aetherdrift|73|R|{1}{U}|Legendary Creature - Homunculus|0|4|Start your engines!$You have no maximum hand size.$Max speed -- If you would draw a card, draw two cards instead.| -Waxen Shapethief|Aetherdrift|74|R|{3}{U}|Creature - Shapeshifter|0|0|Flash$You may have this creature enter as a copy of a creature or artifact you control.$Cycling {2}| +Waxen Shapethief|Aetherdrift|74|R|{3}{U}|Creature - Shapeshifter|0|0|Flash$You may have this creature enter as a copy of an artifact or creature you control.$Cycling {2}| Ancient Vendetta|Aetherdrift|75|U|{3}{B}|Sorcery|||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.| Back on Track|Aetherdrift|76|U|{4}{B}|Sorcery|||Return target creature or Vehicle card from your graveyard to the battlefield. Create a 1/1 colorless Pilot creature token with "This token saddles Mounts and crews Vehicles as though its power were 2 greater."| Bloodghast|Aetherdrift|77|R|{B}{B}|Creature - Vampire Spirit|2|1|Bloodghast can't block.$Bloodghast has haste as long as an opponent has 10 or less life.$Landfall -- Whenever a land you control enters, you may return Bloodghast from your graveyard to the battlefield.| @@ -56166,10 +56166,10 @@ Captain Howler, Sea Scourge|Aetherdrift|194|R|{2}{U}{R}|Legendary Creature - Sha Caradora, Heart of Alacria|Aetherdrift|195|R|{2}{G}{W}|Legendary Creature - Human Knight|4|2|When Caradora enters, you may search your library for a Mount or Vehicle card, reveal it, put it into your hand, then shuffle.$If one or more +1/+1 counters would be put on a creature or Vehicle you control, that many plus one +1/+1 counters are put on it instead.| Cloudspire Coordinator|Aetherdrift|196|U|{R}{W}|Creature - Human Pilot|3|1|When this creature enters, scry 2.${T}: Create X 1/1 colorless Pilot creature tokens, where X is the number of Mounts and/or Vehicles that entered the battlefield under your control this turn. The tokens have "This token saddles Mounts and crews Vehicles as though its power were 2 greater."| Cloudspire Skycycle|Aetherdrift|197|U|{2}{R}{W}|Artifact - Vehicle|2|3|Flying$When this Vehicle enters, distribute two +1/+1 counters among one or two other target Vehicles and/or creatures you control.$Crew 1| -Coalstoke Gearhulk|Aetherdrift|198|M|{1}{B}{B}{R}{R}|Artifact Creature - Construct|5|4|Menace, deathtouch$When this creature enters, put target creature card with mana value 4 or less from a graveyard onto the battlefield under your control with a finality counter on it. It gains menace, deathtouch, and haste. Exile that creature at the beginning of your next end step.| +Coalstoke Gearhulk|Aetherdrift|198|M|{1}{B}{B}{R}{R}|Artifact Creature - Construct|5|4|Menace, deathtouch$When this creature enters, put target creature card with mana value 4 or less from a graveyard onto the battlefield under your control with a finality counter on it. That creature gains menace, deathtouch, and haste. At the beginnning of your next end step, exile that creature.| Debris Beetle|Aetherdrift|199|R|{2}{B}{G}|Artifact - Vehicle|6|6|Trample$When this Vehicle enters, each opponent loses 3 life and you gain 3 life.$Crew 2| Dune Drifter|Aetherdrift|200|U|{X}{W}{B}|Artifact - Vehicle|3|3|When this Vehicle enters, return target artifact or creature card with mana value X or less from your graveyard to the battlefield.$Crew 2| -Embalmed Ascendant|Aetherdrift|201|U|{1}{W}{B}|Creature - Zombie|1|2|Start your engines!$When this creature enters, create a 2/2 black Zombie creature token.$Max speed--Whenever a creature you control dies, each opponent loses 1 life and you gain 1 life.| +Embalmed Ascendant|Aetherdrift|201|U|{1}{W}{B}|Creature - Zombie|1|2|Start your engines!$When this creature enters, create a 2/2 black Zombie creature token.$Max speed -- Whenever a creature you control dies, each opponent loses 1 life and you gain 1 life.| Explosive Getaway|Aetherdrift|202|R|{3}{R}{W}|Sorcery|||Exile up to one target artifact or creature. Return it to the battlefield under its owner's control at the beginning of the next end step.$Explosive Getaway deals 4 damage to each creature.| Far Fortune, End Boss|Aetherdrift|203|R|{2}{B}{R}|Legendary Creature - Human Mercenary|4|5|Start your engines!$Whenever you attack, Far Fortune deals 1 damage to each opponent.$Max speed -- If a source you control would deal damage to an opponent or a permanent an opponent controls, it deals that much damage plus 1 instead.| Fearless Swashbuckler|Aetherdrift|204|R|{1}{U}{R}|Creature - Fish Pirate|3|3|Haste$Vehicles you control have haste.$Whenever you attack, if a Pirate and a Vehicle attacked this combat, draw three cards, then discard two cards.| @@ -56201,11 +56201,11 @@ Zahur, Glory's Past|Aetherdrift|229|R|{W}{B}|Legendary Creature - Zombie Cat War Aetherjacket|Aetherdrift|230|C|{3}|Artifact Creature - Thopter|2|1|Flying, vigilance${2}, {T}, Sacrifice this creature: Destroy another target artifact. Activate only as a sorcery.| The Aetherspark|Aetherdrift|231|M|{4}|Legendary Artifact Planeswalker - Equipment|4|As long as The Aetherspark is attached to a creature, The Aetherspark can't be attacked and has "Whenever equipped creature deals combat damage during your turn, put that many loyalty counters on The Aetherspark."$+1: Attach The Aetherspark to up to one target creature you control. Put a +1/+1 counter on that creature.$-5: Draw two cards.$-10: Add ten mana of any one color.| Camera Launcher|Aetherdrift|232|C|{3}|Artifact Creature - Construct|2|2|Exhaust -- {3}: Put a +1/+1 counter on this creature. Create a 1/1 colorless Thopter artifact creature token with flying.| -Guidelight Matrix|Aetherdrift|233|C|{2}|Artifact|||When this artifact enters, draw a card.${2}, {T}: Target Mount you control becomes saddled until end of turn. Activate only as a sorcery.${2}, {t}: Target Vehicle you control becomes an artifact creature until end of turn.| +Guidelight Matrix|Aetherdrift|233|C|{2}|Artifact|||When this artifact enters, draw a card.${2}, {T}: Target Mount you control becomes saddled until end of turn. Activate only as a sorcery.${2}, {T}: Target Vehicle you control becomes an artifact creature until end of turn.| Lifecraft Engine|Aetherdrift|234|R|{3}|Artifact - Vehicle|4|4|As this Vehicle enters, choose a creature type.$Vehicle creatures you control are the chosen creature type in addition to their other types.$Each creature you control of the chosen type other than this Vehicle gets +1/+1.$Crew 3| Marketback Walker|Aetherdrift|235|R|{X}{X}|Artifact Creature - Construct|0|0|This creature enters with X +1/+1 counters on it.${4}: Put a +1/+1 counter on this creature.$When this creature dies, draw a card for each +1/+1 counter on it.| Marshals' Pathcruiser|Aetherdrift|236|U|{3}|Artifact - Vehicle|6|5|When this Vehicle enters, search your library for a basic land card, reveal it, put it into your hand, then shuffle.$Exhaust -- {W}{U}{B}{R}{G}: This Vehicle becomes an artifact creature. Put two +1/+1 counters on it.$Crew 5| -Monument to Endurance|Aetherdrift|237|R|{3}|Artifact|||Whenever you discard a card, choose one that hasn't been chosen this turn--$* Draw a card.$* Create a Treasure token.$* Each opponent loses 3 life.| +Monument to Endurance|Aetherdrift|237|R|{3}|Artifact|||Whenever you discard a card, choose one that hasn't been chosen this turn --$* Draw a card.$* Create a Treasure token.$* Each opponent loses 3 life.| Pit Automaton|Aetherdrift|238|U|{2}|Artifact Creature - Construct|0|4|Defender${T}: Add {C}{C}. Spend this mana only to activate abilities.${2}, {T}: When you next activate an exhaust ability this turn, copy it. You may choose new targets for the copy.| Racers' Scoreboard|Aetherdrift|239|U|{4}|Artifact|||Start your engines!$When this artifact enters, draw two cards, then discard a card.$Max speed -- Spells you cast cost {1} less to cast.| Radiant Lotus|Aetherdrift|240|M|{6}|Artifact|||{T}, Sacrifice one or more artifacts: Choose a color. Target player adds three mana of the chosen color for each artifact sacrificed this way.| diff --git a/pom.xml b/pom.xml index 34f0f0a8109..feb6a458482 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.mage mage-root - 1.4.55 + 1.4.56 pom Mage Root Mage Root POM @@ -16,7 +16,7 @@ ${project.basedir} - 1.4.55 + 1.4.56 -Dfile.encoding=UTF-8 UTF-8 yyyy-MM-dd'T'HH:mm:ss'Z'