master #17

Merged
Failure merged 124 commits from External/mage:master into master 2025-02-12 10:32:12 -08:00
433 changed files with 8704 additions and 1435 deletions

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-root</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-client</artifactId>

View file

@ -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);
}

View file

@ -3170,7 +3170,7 @@
<Properties>
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
<Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo">
<TitledBorder title="Proxy for server connection and images download">
<TitledBorder title="Proxy for server connection and images download (DO NOT SUPPORTED)">
<Border PropertyName="innerBorder" info="org.netbeans.modules.form.compat2.border.EtchedBorderInfo">
<EtchetBorder/>
</Border>

View file

@ -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, "");

View file

@ -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

View file

@ -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()) {

View file

@ -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

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-root</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-common</artifactId>

View file

@ -17,7 +17,7 @@ public class MageVersion implements Serializable, Comparable<MageVersion> {
// * 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

View file

@ -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

View file

@ -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;
}
}

View file

@ -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));
}
}
}

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-counter-plugin</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-root</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-plugins</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-root</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-reports</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-root</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-server-console</artifactId>

View file

@ -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());

View file

@ -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;

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-deck-constructed</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-deck-limited</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-brawlduel</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-brawlfreeforall</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-canadianhighlanderduel</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-commanderduel</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-commanderfreeforall</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-custompillaroftheparunsduel</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-freeforall</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-freeformcommanderduel</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-freeformcommanderfreeforall</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-freeformunlimitedcommander</artifactId>
@ -23,7 +23,7 @@
<dependency>
<groupId>org.mage</groupId>
<artifactId>mage-game-freeformcommanderfreeforall</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
<scope>compile</scope>
</dependency>
</dependencies>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-momirduel</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-momirfreeforall</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-oathbreakerduel</artifactId>
@ -22,7 +22,7 @@
<dependency>
<groupId>org.mage</groupId>
<artifactId>mage-game-oathbreakerfreeforall</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
<scope>compile</scope>
</dependency>
</dependencies>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-oathbreakerfreeforall</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-pennydreadfulcommanderfreeforall</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-tinyleadersduel</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-game-twoplayerduel</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-player-ai-draftbot</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-player-ai-ma</artifactId>

View file

@ -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());

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
}

View file

@ -15,11 +15,7 @@ public class CombatInfo {
private Map<Permanent, List<Permanent>> combat = new HashMap<>();
public void addPair(Permanent attacker, Permanent blocker) {
List<Permanent> blockers = combat.get(attacker);
if (blockers == null) {
blockers = new ArrayList<>();
combat.put(attacker, blockers);
}
List<Permanent> blockers = combat.computeIfAbsent(attacker, k -> new ArrayList<>());
blockers.add(blocker);
}

View file

@ -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<Permanent> creatures) {
if (creatures.isEmpty()) {
return null;
public static Permanent getWorstCreature(List<Permanent>... lists) {
for (List<Permanent> 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<Permanent>... lists) {
for (List<Permanent> 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<Permanent> attackersThatWontBeBlocked, Player defender) {
@ -144,25 +158,100 @@ public final class CombatUtil {
return canBlock;
}
public static CombatInfo blockWithGoodTrade(Game game, List<Permanent> attackers, List<Permanent> blockers) {
/**
* AI related code, find better block combination for attackers
*/
public static CombatInfo blockWithGoodTrade2(Game game, List<Permanent> attackers, List<Permanent> 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<Permanent> possibleBlockers = getPossibleBlockers(game, attacker, blockers);
List<Permanent> survivedBlockers = getBlockersThatWillSurvive(game, attackerId, defenderId, attacker, possibleBlockers);
if (!survivedBlockers.isEmpty()) {
Permanent blocker = getWorstCreature(survivedBlockers);
// simple combat simulation (1 vs 1)
List<Permanent> allBlockers = getPossibleBlockers(game, attacker, blockers);
List<SurviveInfo> blockerStats = getBlockersThatWillSurvive2(game, attackerId, defenderId, attacker, allBlockers);
Map<Permanent, Integer> blockingDiffScore = new HashMap<>();
Map<Permanent, Integer> nonBlockingDiffScore = new HashMap<>();
blockerStats.forEach(s -> {
blockingDiffScore.put(s.getBlocker(), s.getDiffBlockingScore());
nonBlockingDiffScore.put(s.getBlocker(), s.getDiffNonblockingScore());
});
// split blockers by usage priority
List<Permanent> survivedAndKillBlocker = new ArrayList<>();
List<Permanent> survivedBlockers = new ArrayList<>();
List<Permanent> 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<Permanent> getBlockersThatWillSurvive(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List<Permanent> possibleBlockers) {
List<Permanent> blockers = new ArrayList<>();
/**
* Game simulations to find all survived/killer blocker
*/
private static List<SurviveInfo> getBlockersThatWillSurvive2(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List<Permanent> possibleBlockers) {
List<SurviveInfo> 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<Permanent> attackers, List<Permanent> 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<Permanent> possibleBlockers = getPossibleBlockers(game, attacker, blockers);
List<Permanent> 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<Permanent> getBlockersThatWillSurvive2(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List<Permanent> possibleBlockers) {
List<Permanent> 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());
}
}
}

View file

@ -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;
}
}

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-player-ai</artifactId>

View file

@ -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<Mana, Card> unplayable = new TreeMap<>();
private final transient List<Card> playableNonInstant = new ArrayList<>();

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-player-ai-mcts</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-player-human</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-tournament-boosterdraft</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-tournament-constructed</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-server-plugins</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-tournament-sealed</artifactId>

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-root</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-server-plugins</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-root</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-server</artifactId>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
java -Xmx1024m -jar ./lib/mage-server-${project.version}.jar

View file

@ -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

View file

@ -42,6 +42,8 @@ public class GameSessionWatcher {
if (!killed) {
Optional<User> 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;
}

View file

@ -7,7 +7,7 @@
<parent>
<groupId>org.mage</groupId>
<artifactId>mage-root</artifactId>
<version>1.4.55</version>
<version>1.4.56</version>
</parent>
<artifactId>mage-sets</artifactId>

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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) {

View file

@ -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} <i>(energy counters)</i> 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;
}
}

View file

@ -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<Counter> 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<Counter> 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);
}
}

View file

@ -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);
}

View file

@ -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(

View file

@ -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));
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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)

View file

@ -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);
}

View file

@ -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, "<i>Delirium</i> &mdash; {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);
}

View file

@ -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.

View file

@ -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) {

View file

@ -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(

View file

@ -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("<br>"));
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()

View file

@ -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());

View file

@ -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(

View file

@ -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,
"<i>Delirium</i> &mdash; 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) {

View file

@ -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));
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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)

View file

@ -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

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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.

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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(

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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<UUID, Integer> 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);
}
}

Some files were not shown because too many files have changed in this diff Show more