diff --git a/Mage.Client/pom.xml b/Mage.Client/pom.xml index 7e58661a90..9166a85c1a 100644 --- a/Mage.Client/pom.xml +++ b/Mage.Client/pom.xml @@ -141,6 +141,18 @@ balloontip 1.2.4.1 + + + batik + batik-transcoder + 1.6-1 + + + + org.ocpsoft.prettytime + prettytime + 3.2.7.Final + diff --git a/Mage.Client/serverlist.txt b/Mage.Client/serverlist.txt index 138f37fe11..7e8a86d50f 100644 --- a/Mage.Client/serverlist.txt +++ b/Mage.Client/serverlist.txt @@ -1,6 +1,5 @@ XMage.de 1 (Europe/Germany) fast :xmage.de:17171 woogerworks (North America/USA) :xmage.woogerworks.com:17171 -xmage.lukeskywalk.com (North America) :xmage.lukeskywalk.com:17171 play.xmage.net (North America/Canada) :play.xmage.net:17171 XMageBr. (South America/Brazil) :magic.ncs3sistemas.com.br:17171 XMage.tahiti :xmage.tahiti.one:443 diff --git a/Mage.Client/src/main/java/mage/client/MageFrame.java b/Mage.Client/src/main/java/mage/client/MageFrame.java index 425172f7a6..dfa734f1af 100644 --- a/Mage.Client/src/main/java/mage/client/MageFrame.java +++ b/Mage.Client/src/main/java/mage/client/MageFrame.java @@ -92,6 +92,8 @@ import org.mage.plugins.card.images.DownloadPictures; import org.mage.plugins.card.info.CardInfoPaneImpl; import org.mage.plugins.card.utils.impl.ImageManagerImpl; +import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; + /** * @author BetaSteward_at_googlemail.com */ diff --git a/Mage.Client/src/main/java/mage/client/cards/CardsList.java b/Mage.Client/src/main/java/mage/client/cards/CardsList.java index aca7e83919..654634527d 100644 --- a/Mage.Client/src/main/java/mage/client/cards/CardsList.java +++ b/Mage.Client/src/main/java/mage/client/cards/CardsList.java @@ -48,6 +48,7 @@ import mage.view.CardView; import mage.view.CardsView; import mage.view.SimpleCardView; import org.mage.card.arcane.CardPanel; +import org.mage.card.arcane.ManaSymbolsCellRenderer; import javax.swing.*; import javax.swing.table.DefaultTableCellRenderer; @@ -164,6 +165,9 @@ public class CardsList extends javax.swing.JPanel implements MouseListener, ICar mainTable.getColumnModel().getColumn(6).setPreferredWidth(15); mainTable.getColumnModel().getColumn(7).setPreferredWidth(15); + // new mana render (svg support) + mainTable.getColumnModel().getColumn(mainModel.COLUMN_INDEX_COST).setCellRenderer(new ManaSymbolsCellRenderer()); + if (PreferencesDialog.getCachedValue(PreferencesDialog.KEY_DRAFT_VIEW, "cardView").equals("listView")) { jToggleListView.setSelected(true); panelCardArea.setViewportView(mainTable); diff --git a/Mage.Client/src/main/java/mage/client/constants/Constants.java b/Mage.Client/src/main/java/mage/client/constants/Constants.java index 6c65550a45..b1de743dc2 100644 --- a/Mage.Client/src/main/java/mage/client/constants/Constants.java +++ b/Mage.Client/src/main/java/mage/client/constants/Constants.java @@ -27,6 +27,7 @@ */ package mage.client.constants; +import java.awt.*; import java.io.File; import javax.swing.BorderFactory; import javax.swing.border.Border; @@ -72,18 +73,45 @@ public final class Constants { public static final double SCALE_FACTOR = 0.5; - public static final String PLUGINS_DIRECTORY = "plugins/"; + // cards render + public static final Rectangle CARD_SIZE_FULL = new Rectangle(101, 149); + public static final Rectangle THUMBNAIL_SIZE_FULL = new Rectangle(102, 146); - public static final String RESOURCE_PATH_MANA_LARGE = IO.imageBaseDir + "symbols" + File.separator + "large"; - public static final String RESOURCE_PATH_MANA_MEDIUM = IO.imageBaseDir + "symbols" + File.separator + "medium"; - public static final String RESOURCE_PATH_SET = IO.imageBaseDir + "sets" + File.separator; - public static final String RESOURCE_PATH_SET_SMALL = RESOURCE_PATH_SET + File.separator + "small" + File.separator; - public static final String BASE_SOUND_PATH = "sounds" + File.separator; + // resources - default images + public static final String RESOURCE_PATH_DEFAUL_IMAGES = File.separator + "default"; + + // resources - symbols + public static final String RESOURCE_PATH_SYMBOLS = File.separator + "symbols"; + public static final String RESOURCE_SYMBOL_FOLDER_SMALL = "small"; + public static final String RESOURCE_SYMBOL_FOLDER_MEDIUM = "medium"; + public static final String RESOURCE_SYMBOL_FOLDER_LARGE = "large"; + public static final String RESOURCE_SYMBOL_FOLDER_SVG = "svg"; + public static final String RESOURCE_SYMBOL_FOLDER_PNG = "png"; + public enum ResourceSymbolSize { + SMALL, + MEDIUM, + LARGE, + SVG, + PNG + } + + // resources - sets + public static final String RESOURCE_PATH_SETS = File.separator + "sets"; + public static final String RESOURCE_SET_FOLDER_SMALL = "small"; + public static final String RESOURCE_SET_FOLDER_MEDIUM = ""; // empty, medium images laydown in "sets" folder, TODO: delete that and auto gen, use png for html, not gif + public static final String RESOURCE_SET_FOLDER_SVG = "svg"; + public enum ResourceSetSize { + SMALL, + MEDIUM, + SVG + } + + // sound + public static final String BASE_SOUND_PATH = "sounds" + File.separator; // TODO: check path with File.separator public static final String BASE_MUSICS_PATH = "music" + File.separator; public interface IO { - - String imageBaseDir = "plugins" + File.separator + "images" + File.separator; + String DEFAULT_IMAGES_DIR = "plugins" + File.separator + "images" + File.separator; String IMAGE_PROPERTIES_FILE = "image.url.properties"; } diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java b/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java index 80b936a178..90249dc915 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java @@ -64,6 +64,7 @@ import mage.filter.predicate.other.CardTextPredicate; import mage.filter.predicate.other.ExpansionSetPredicate; import mage.view.CardView; import mage.view.CardsView; +import org.mage.card.arcane.ManaSymbolsCellRenderer; /** * @@ -133,6 +134,9 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene mainTable.getColumnModel().getColumn(6).setPreferredWidth(15); mainTable.getColumnModel().getColumn(7).setPreferredWidth(15); + // new mana render (svg support) + mainTable.getColumnModel().getColumn(mainModel.COLUMN_INDEX_COST).setCellRenderer(new ManaSymbolsCellRenderer()); + // mainTable.setToolTipText(cardSelectorScrollPane.getToolTipText()); cardSelectorScrollPane.setViewportView(mainTable); mainTable.setOpaque(false); diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java index 6c5600abf3..2dd577bd08 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java @@ -204,7 +204,7 @@ public class MageBook extends JComponent { if (setImage != null) { tab.setOverlayImage(setImage); } else { - System.out.println("Couldn't find symbol image: " + "/plugins/images/sets/" + set + "-C.jpg"); + System.out.println("Couldn't find symbol image: " + set + "-C.jpg"); } tab.setSet(set); tab.setBounds(0, y, 39, 120); diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java b/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java index 44ed87185b..7015a2cdd6 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/table/TableModel.java @@ -79,6 +79,7 @@ public class TableModel extends AbstractTableModel implements ICardGrid { private UpdateCountsCallback updateCountsCallback; private final String column[] = {"Qty", "Name", "Cost", "Color", "Type", "Stats", "Rarity", "Set", "#"}; + public final int COLUMN_INDEX_COST = 2; private SortSetting sortSetting; private int recentSortedColumn; @@ -239,6 +240,10 @@ public class TableModel extends AbstractTableModel implements ICardGrid { case 1: return c.getName(); case 2: + // new svg images version + return ManaSymbols.getStringManaCost(c.getManaCost()); + /* + // old html images version String manaCost = ""; for (String m : c.getManaCost()) { manaCost += m; @@ -246,6 +251,8 @@ public class TableModel extends AbstractTableModel implements ICardGrid { String castingCost = UI.getDisplayManaCost(manaCost); castingCost = ManaSymbols.replaceSymbolsWithHTML(castingCost, ManaSymbols.Type.TABLE); return "" + castingCost + ""; + return castingCost; + */ case 3: return c.getColorText(); case 4: diff --git a/Mage.Client/src/main/java/mage/client/plugins/impl/Plugins.java b/Mage.Client/src/main/java/mage/client/plugins/impl/Plugins.java index 1fd7a2e725..3696f8e9b2 100644 --- a/Mage.Client/src/main/java/mage/client/plugins/impl/Plugins.java +++ b/Mage.Client/src/main/java/mage/client/plugins/impl/Plugins.java @@ -32,9 +32,11 @@ import org.apache.log4j.Logger; import org.mage.plugins.card.CardPluginImpl; import org.mage.plugins.theme.ThemePluginImpl; +import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; + public enum Plugins implements MagePlugins { instance; - public static final String PLUGINS_DIRECTORY = "plugins/"; + public static final String PLUGINS_DIRECTORY = "plugins"; private static final Logger LOGGER = Logger.getLogger(Plugins.class); private static PluginManager pm; @@ -48,9 +50,10 @@ public enum Plugins implements MagePlugins { @Override public void loadPlugins() { + LOGGER.info("Loading plugins..."); pm = PluginManagerFactory.createPluginManager(); - pm.addPluginsFrom(new File(PLUGINS_DIRECTORY).toURI()); + pm.addPluginsFrom(new File(PLUGINS_DIRECTORY + File.separator).toURI()); this.cardPlugin = new CardPluginImpl(); this.counterPlugin = pm.getPlugin(CounterPlugin.class); this.themePlugin = new ThemePluginImpl(); @@ -131,10 +134,8 @@ public enum Plugins implements MagePlugins { @Override public void downloadSymbols() { - String useDefault = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_USE_DEFAULT, "true"); - String path = useDefault.equals("true") ? null : PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_PATH, null); if (this.cardPlugin != null) { - this.cardPlugin.downloadSymbols(path); + this.cardPlugin.downloadSymbols(getImagesDir()); } } diff --git a/Mage.Client/src/main/java/mage/client/table/TablesPanel.java b/Mage.Client/src/main/java/mage/client/table/TablesPanel.java index e473497f6d..7784dc6411 100644 --- a/Mage.Client/src/main/java/mage/client/table/TablesPanel.java +++ b/Mage.Client/src/main/java/mage/client/table/TablesPanel.java @@ -46,6 +46,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.swing.*; import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; + import mage.cards.decks.importer.DeckImporterUtil; import mage.client.MageFrame; import mage.client.SessionHandler; @@ -54,7 +57,6 @@ import mage.client.components.MageComponents; import mage.client.dialog.*; import static mage.client.dialog.PreferencesDialog.KEY_TABLES_COLUMNS_ORDER; import static mage.client.dialog.PreferencesDialog.KEY_TABLES_COLUMNS_WIDTH; -import static mage.client.table.TablesPanel.PASSWORDED; import mage.client.util.ButtonColumn; import mage.client.util.GUISizeHelper; import mage.client.util.IgnoreList; @@ -70,6 +72,9 @@ import mage.view.RoomUsersView; import mage.view.TableView; import mage.view.UserRequestMessage; import org.apache.log4j.Logger; +import org.ocpsoft.prettytime.PrettyTime; +import org.ocpsoft.prettytime.units.JustNow; +import org.ocpsoft.prettytime.Duration; /** * @@ -80,7 +85,6 @@ public class TablesPanel extends javax.swing.JPanel { private static final Logger LOGGER = Logger.getLogger(TablesPanel.class); private static final int[] DEFAULT_COLUMNS_WIDTH = {35, 150, 120, 180, 80, 120, 80, 60, 40, 40, 60}; - public static final String PASSWORDED = "***"; private final TableTableModel tableModel; private final MatchesTableModel matchesModel; private UUID roomId; @@ -94,12 +98,62 @@ public class TablesPanel extends javax.swing.JPanel { private java.util.List messages; private int currentMessage; private final MageTableRowSorter activeTablesSorter; + private final MageTableRowSorter completedTablesSorter; private final ButtonColumn actionButton1; private final ButtonColumn actionButton2; final JToggleButton[] filterButtons; + // time formater + private PrettyTime timeFormater = new PrettyTime(); + + // time ago renderer + TableCellRenderer timeAgoCellRenderer = new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + Date d = (Date)value; + label.setText(timeFormater.format(d)); + return label; + } + }; + + // duration renderer + TableCellRenderer durationCellRenderer = new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + Long ms = (Long)value; + + if(ms != 0){ + Duration dur = timeFormater.approximateDuration(new Date(ms)); + label.setText((timeFormater.formatDuration(dur))); + }else{ + label.setText(""); + } + return label; + } + }; + + // datetime render + TableCellRenderer datetimeCellRenderer = new DefaultTableCellRenderer() { + DateFormat datetimeFormater = new SimpleDateFormat("HH:mm:ss"); + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + Date d = (Date)value; + if(d != null){ + label.setText(datetimeFormater.format(d)); + }else{ + label.setText(""); + } + + return label; + } + }; + /** * Creates new form TablesPanel */ @@ -112,20 +166,56 @@ public class TablesPanel extends javax.swing.JPanel { initComponents(); // tableModel.setSession(session); - tableTables.createDefaultColumnsFromModel(); + // formater + timeFormater.setLocale(Locale.ENGLISH); + JustNow jn = timeFormater.getUnit(JustNow.class); + jn.setMaxQuantity(1000L * 30L); // 30 seconds gap (show "just now" from 0 to 30 secs) + // 1. TABLE CURRENT + tableTables.createDefaultColumnsFromModel(); activeTablesSorter = new MageTableRowSorter(tableModel); tableTables.setRowSorter(activeTablesSorter); + // time ago + tableTables.getColumnModel().getColumn(TableTableModel.COLUMN_CREATED).setCellRenderer(timeAgoCellRenderer); + /* date sorter (not need, default is good - see getColumnClass) + activeTablesSorter.setComparator(TableTableModel.COLUMN_CREATED, new Comparator() { + @Override + public int compare(Date v1, Date v2) { + return v1.compareTo(v2); + } + + });*/ + // default sort by created date (last games from above) + ArrayList list = new ArrayList(); + list.add(new RowSorter.SortKey(TableTableModel.COLUMN_CREATED, SortOrder.DESCENDING)); + activeTablesSorter.setSortKeys(list); + TableUtil.setColumnWidthAndOrder(tableTables, DEFAULT_COLUMNS_WIDTH, - PreferencesDialog.KEY_TABLES_COLUMNS_WIDTH, PreferencesDialog.KEY_TABLES_COLUMNS_ORDER); + PreferencesDialog.KEY_TABLES_COLUMNS_WIDTH, PreferencesDialog.KEY_TABLES_COLUMNS_ORDER); // TODO: is sort order save and restore after app restart/window open? - tableCompleted.setRowSorter(new MageTableRowSorter(matchesModel)); + + // 2. TABLE COMPLETED + completedTablesSorter = new MageTableRowSorter(matchesModel); + tableCompleted.setRowSorter(completedTablesSorter); + + // duration + tableCompleted.getColumnModel().getColumn(MatchesTableModel.COLUMN_DURATION).setCellRenderer(durationCellRenderer); + // start-end + tableCompleted.getColumnModel().getColumn(MatchesTableModel.COLUMN_START).setCellRenderer(datetimeCellRenderer); + tableCompleted.getColumnModel().getColumn(MatchesTableModel.COLUMN_END).setCellRenderer(datetimeCellRenderer); + // default sort by ended date (last games from above) + ArrayList list2 = new ArrayList(); + list2.add(new RowSorter.SortKey(MatchesTableModel.COLUMN_END, SortOrder.DESCENDING)); + completedTablesSorter.setSortKeys(list2); + + // 3. CHAT chatPanelMain.getUserChatPanel().useExtendedView(ChatPanelBasic.VIEW_MODE.NONE); chatPanelMain.getUserChatPanel().setBorder(null); chatPanelMain.getUserChatPanel().setChatType(ChatPanelBasic.ChatType.TABLES); + // 4. BUTTONS filterButtons = new JToggleButton[]{btnStateWaiting, btnStateActive, btnStateFinished, btnTypeMatch, btnTypeTourneyConstructed, btnTypeTourneyLimited, btnFormatBlock, btnFormatStandard, btnFormatModern, btnFormatLegacy, btnFormatVintage, btnFormatCommander, btnFormatTinyLeader, btnFormatLimited, btnFormatOther, @@ -181,7 +271,7 @@ public class TablesPanel extends javax.swing.JPanel { if (isTournament) { LOGGER.info("Joining tournament " + tableId); if (deckType.startsWith("Limited")) { - if (PASSWORDED.equals(pwdColumn)) { + if (TableTableModel.PASSWORD_VALUE_YES.equals(pwdColumn)) { joinTableDialog.showDialog(roomId, tableId, true, deckType.startsWith("Limited")); } else { SessionHandler.joinTournamentTable(roomId, tableId, SessionHandler.getUserName(), PlayerType.HUMAN, 1, null, ""); @@ -225,7 +315,7 @@ public class TablesPanel extends javax.swing.JPanel { @Override public void actionPerformed(ActionEvent e) { int modelRow = Integer.valueOf(e.getActionCommand()); - String action = (String) matchesModel.getValueAt(modelRow, MatchesTableModel.ACTION_COLUMN); + String action = (String) matchesModel.getValueAt(modelRow, MatchesTableModel.COLUMN_ACTION); switch (action) { case "Replay": java.util.List gameList = matchesModel.getListofGames(modelRow); @@ -250,7 +340,7 @@ public class TablesPanel extends javax.swing.JPanel { // !!!! adds action buttons to the table panel (don't delete this) actionButton1 = new ButtonColumn(tableTables, openTableAction, tableTables.convertColumnIndexToView(TableTableModel.ACTION_COLUMN)); - actionButton2 = new ButtonColumn(tableCompleted, closedTableAction, tableCompleted.convertColumnIndexToView(MatchesTableModel.ACTION_COLUMN)); + actionButton2 = new ButtonColumn(tableCompleted, closedTableAction, tableCompleted.convertColumnIndexToView(MatchesTableModel.COLUMN_ACTION)); // !!!! } @@ -583,21 +673,27 @@ public class TablesPanel extends javax.swing.JPanel { skillFilterList.add(RowFilter.regexFilter(SkillLevel.SERIOUS.toString(), TableTableModel.COLUMN_SKILL)); } + String ratedMark = TableTableModel.RATED_VALUE_YES; java.util.List> ratingFilterList = new ArrayList<>(); if (btnRated.isSelected()) { - ratingFilterList.add(RowFilter.regexFilter("^Rated", TableTableModel.COLUMN_RATING)); + // yes word + ratingFilterList.add(RowFilter.regexFilter("^" + ratedMark, TableTableModel.COLUMN_RATING)); } if (btnUnrated.isSelected()) { - ratingFilterList.add(RowFilter.regexFilter("^Unrated", TableTableModel.COLUMN_RATING)); + // not yes word, see https://stackoverflow.com/a/406408/1276632 + ratingFilterList.add(RowFilter.regexFilter("^((?!" + ratedMark + ").)*$", TableTableModel.COLUMN_RATING)); } // Password + String passwordMark = TableTableModel.PASSWORD_VALUE_YES; java.util.List> passwordFilterList = new ArrayList<>(); - if (btnOpen.isSelected()) { - passwordFilterList.add(RowFilter.regexFilter("^$", TableTableModel.COLUMN_PASSWORD)); - } if (btnPassword.isSelected()) { - passwordFilterList.add(RowFilter.regexFilter("^\\*\\*\\*$", TableTableModel.COLUMN_PASSWORD)); + // yes + passwordFilterList.add(RowFilter.regexFilter("^" + passwordMark, TableTableModel.COLUMN_PASSWORD)); + } + if (btnOpen.isSelected()) { + // no + passwordFilterList.add(RowFilter.regexFilter("^((?!" + passwordMark + ").)*$", TableTableModel.COLUMN_PASSWORD)); } // Hide games of ignored players @@ -1281,16 +1377,24 @@ class TableTableModel extends AbstractTableModel { public static final int COLUMN_QUIT_RATIO = 10; public static final int ACTION_COLUMN = 11; // column the action is located (starting with 0) + public static final String RATED_VALUE_YES = "YES"; + public static final String RATED_VALUE_NO = ""; + + public static final String PASSWORD_VALUE_YES = "YES"; + private final String[] columnNames = new String[]{"M/T", "Deck Type", "Owner / Players", "Game Type", "Info", "Status", "Password", "Created / Started", "Skill Level", "Rating", "Quit %", "Action"}; private TableView[] tables = new TableView[0]; - private static final DateFormat timeFormatter = new SimpleDateFormat("HH:mm:ss"); + + TableTableModel() { + } public void loadData(Collection tables) throws MageRemoteException { this.tables = tables.toArray(new TableView[0]); this.fireTableDataChanged(); } + @Override public int getRowCount() { return tables.length; @@ -1317,13 +1421,13 @@ class TableTableModel extends AbstractTableModel { case 5: return tables[arg0].getTableStateText(); case 6: - return tables[arg0].isPassworded() ? PASSWORDED : ""; + return tables[arg0].isPassworded() ? PASSWORD_VALUE_YES : ""; case 7: - return timeFormatter.format(tables[arg0].getCreateTime()); + return tables[arg0].getCreateTime(); // use cell render, not format here case 8: return tables[arg0].getSkillLevel(); case 9: - return tables[arg0].isRated() ? "Rated" : "Unrated"; + return tables[arg0].isRated() ? RATED_VALUE_YES : RATED_VALUE_NO; case 10: return tables[arg0].getQuitRatio(); case 11: @@ -1384,6 +1488,8 @@ class TableTableModel extends AbstractTableModel { return Icon.class; case COLUMN_SKILL: return SkillLevel.class; + case COLUMN_CREATED: + return Date.class; default: return String.class; } @@ -1486,17 +1592,22 @@ class UpdatePlayersTask extends SwingWorker> { class MatchesTableModel extends AbstractTableModel { - public static final int ACTION_COLUMN = 7; // column the action is located (starting with 0) - public static final int GAMES_LIST_COLUMN = 8; - private final String[] columnNames = new String[]{"Deck Type", "Players", "Game Type", "Rating", "Result", "Start Time", "End Time", "Action"}; + private final String[] columnNames = new String[]{"Deck Type", "Players", "Game Type", "Rating", "Result", "Duration", "Start Time", "End Time", "Action"}; + public static final int COLUMN_DURATION = 5; + public static final int COLUMN_START = 6; + public static final int COLUMN_END = 7; + public static final int COLUMN_ACTION = 8; // column the action is located (starting with 0) + private MatchView[] matches = new MatchView[0]; - private static final DateFormat timeFormatter = SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); public void loadData(Collection matches) throws MageRemoteException { this.matches = matches.toArray(new MatchView[0]); this.fireTableDataChanged(); } + MatchesTableModel(){ + } + @Override public int getRowCount() { return matches.length; @@ -1517,22 +1628,20 @@ class MatchesTableModel extends AbstractTableModel { case 2: return matches[arg0].getGameType(); case 3: - return matches[arg0].isRated() ? "Rated" : "Unrated"; + return matches[arg0].isRated() ? TableTableModel.RATED_VALUE_YES : TableTableModel.RATED_VALUE_NO; case 4: return matches[arg0].getResult(); case 5: - if (matches[arg0].getStartTime() != null) { - return timeFormatter.format(matches[arg0].getStartTime()); + if (matches[arg0].getEndTime() != null) { + return matches[arg0].getEndTime().getTime() - matches[arg0].getStartTime().getTime() + new Date().getTime(); } else { - return ""; + return 0L; } case 6: - if (matches[arg0].getEndTime() != null) { - return timeFormatter.format(matches[arg0].getEndTime()); - } else { - return ""; - } + return matches[arg0].getStartTime(); case 7: + return matches[arg0].getEndTime(); + case 8: if (matches[arg0].isTournament()) { return "Show"; } else if (matches[arg0].isReplayAvailable()) { @@ -1540,7 +1649,7 @@ class MatchesTableModel extends AbstractTableModel { } else { return "None"; } - case 8: + case 9: return matches[arg0].getGames(); } return ""; @@ -1575,12 +1684,21 @@ class MatchesTableModel extends AbstractTableModel { @Override public Class getColumnClass(int columnIndex) { - return String.class; + switch (columnIndex) { + case COLUMN_DURATION: + return Long.class; + case COLUMN_START: + return Date.class; + case COLUMN_END: + return Date.class; + default: + return String.class; + } } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { - return columnIndex == ACTION_COLUMN; + return columnIndex == COLUMN_ACTION; } } diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelComponentImpl.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelComponentImpl.java index 311e1dd969..e75da6e4f3 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelComponentImpl.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelComponentImpl.java @@ -13,23 +13,21 @@ import mage.view.CardView; import mage.view.CounterView; import mage.view.PermanentView; import mage.view.StackAbilityView; -import net.java.truevfs.access.TFile; import org.apache.log4j.Logger; import org.jdesktop.swingx.graphics.GraphicsUtilities; -import org.mage.plugins.card.dl.sources.DirectLinksForDownload; import org.mage.plugins.card.images.ImageCache; import org.mage.plugins.card.utils.impl.ImageManagerImpl; +import mage.client.constants.Constants; import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.File; import java.util.Map; import java.util.StringTokenizer; import java.util.UUID; -import static org.mage.plugins.card.constants.Constants.THUMBNAIL_SIZE_FULL; - /** * Class for drawing the mage card object by using a form based JComponent * approach @@ -479,8 +477,10 @@ public class CardPanelComponentImpl extends CardPanel { if (getShowCastingCost() && !isAnimationPanel() && canShowCardIcons(getCardWidth(), hasImage)) { + int symbolMarginX = 2; // 2 px between icons + String manaCost = ManaSymbols.getStringManaCost(gameCard.getManaCost()); - int manaWidth = getManaWidth(manaCost); + int manaWidth = getManaWidth(manaCost, symbolMarginX); // right top corner with margin (sizes from any sample card, length from black border to mana icon) int manaMarginRight = Math.round(22f / 672f * getCardWidth()); @@ -489,25 +489,19 @@ public class CardPanelComponentImpl extends CardPanel { int manaX = getCardXOffset() + getCardWidth() - manaMarginRight - manaWidth; int manaY = getCardYOffset() + manaMarginTop; - if (hasImage) { - // top right corner if have image like real card - ManaSymbols.draw(g, manaCost, manaX, manaY, getSymbolWidth()); - } else { - // old version - bottom left corner if haven't card - // ManaSymbols.draw(g, manaCost, getCardXOffset() + manaMarginRight, getCardYOffset() + getCardHeight() - manaMarginTop - getSymbolWidth(), getSymbolWidth()); - - // new version - like a normal image (it's best to view and construct decks) - ManaSymbols.draw(g, manaCost, manaX, manaY, getSymbolWidth()); - } + ManaSymbols.draw(g, manaCost, manaX, manaY, getSymbolWidth(), Color.black, symbolMarginX); } } - private int getManaWidth(String manaCost) { + private int getManaWidth(String manaCost, int symbolMarginX) { int width = 0; manaCost = manaCost.replace("\\", ""); StringTokenizer tok = new StringTokenizer(manaCost, " "); while (tok.hasMoreTokens()) { tok.nextToken(); + if(width != 0) { + width += symbolMarginX; + } width += getSymbolWidth(); } return width; @@ -654,7 +648,7 @@ public class CardPanelComponentImpl extends CardPanel { final BufferedImage srcImage; if (gameCard.isFaceDown()) { srcImage = getFaceDownImage(); - } else if (getCardWidth() > THUMBNAIL_SIZE_FULL.width) { + } else if (getCardWidth() > Constants.THUMBNAIL_SIZE_FULL.width) { srcImage = ImageCache.getImage(gameCard, getCardWidth(), getCardHeight()); } else { srcImage = ImageCache.getThumbnail(gameCard); diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderImpl.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderImpl.java index a3a5abd5b5..950069c3fb 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderImpl.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelRenderImpl.java @@ -14,6 +14,7 @@ import org.apache.log4j.Logger; import org.jdesktop.swingx.graphics.GraphicsUtilities; import org.mage.plugins.card.dl.sources.DirectLinksForDownload; import org.mage.plugins.card.images.ImageCache; +import mage.client.constants.Constants; import java.awt.*; import java.awt.image.BufferedImage; @@ -21,8 +22,6 @@ import java.io.File; import java.util.Map; import java.util.UUID; -import static org.mage.plugins.card.constants.Constants.THUMBNAIL_SIZE_FULL; - public class CardPanelRenderImpl extends CardPanel { private static final Logger LOGGER = Logger.getLogger(CardPanelRenderImpl.class); @@ -341,7 +340,7 @@ public class CardPanelRenderImpl extends CardPanel { // Nothing to do srcImage = null; faceArtSrcImage = null; - } else if (getCardWidth() > THUMBNAIL_SIZE_FULL.width) { + } else if (getCardWidth() > Constants.THUMBNAIL_SIZE_FULL.width) { srcImage = ImageCache.getImage(gameCard, getCardWidth(), getCardHeight()); faceArtSrcImage = ImageCache.getFaceImage(gameCard, getCardWidth(), getCardHeight()); } else { diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbols.java b/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbols.java index dd9e5078d0..fce453e0ef 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbols.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbols.java @@ -1,11 +1,13 @@ package org.mage.card.arcane; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Image; -import java.awt.Rectangle; +import java.awt.*; import java.awt.image.BufferedImage; +import java.awt.image.FilteredImageSource; +import java.awt.image.ImageProducer; +import java.awt.image.RGBImageFilter; import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; @@ -16,27 +18,37 @@ import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; -import java.util.HashMap; -import java.util.HashSet; +import java.util.*; import java.util.List; -import java.util.Map; -import java.util.StringTokenizer; import java.util.regex.Pattern; import javax.imageio.ImageIO; +import javax.swing.*; + import mage.cards.repository.ExpansionRepository; -import mage.client.dialog.PreferencesDialog; import mage.client.util.GUISizeHelper; import mage.client.util.ImageHelper; import mage.client.util.gui.BufferedImageBuilder; +import org.apache.batik.dom.svg.SVGDOMImplementation; +import org.apache.batik.transcoder.TranscoderException; +import org.apache.batik.transcoder.TranscoderInput; +import org.apache.batik.transcoder.TranscoderOutput; +import org.apache.batik.transcoder.TranscodingHints; +import org.apache.batik.transcoder.image.ImageTranscoder; +import org.apache.batik.util.SVGConstants; import org.apache.log4j.Logger; -import org.mage.plugins.card.constants.Constants; + +import mage.client.constants.Constants; +import mage.client.constants.Constants.ResourceSymbolSize; +import mage.client.constants.Constants.ResourceSetSize; + +import org.jdesktop.swingx.graphics.ShadowRenderer; +import org.jdesktop.swingx.graphics.GraphicsUtilities; +import org.mage.plugins.card.utils.CardImageUtils; public final class ManaSymbols { private static final Logger LOGGER = Logger.getLogger(ManaSymbols.class); private static final Map> manaImages = new HashMap<>(); - private static boolean smallSymbolsFound = false; - private static boolean mediumSymbolsFound = false; private static final Map> setImages = new HashMap<>(); @@ -63,11 +75,46 @@ public final class ManaSymbols { "BR", "G", "GU", "GW", "R", "RG", "RW", "S", "T", "U", "UB", "UR", "W", "WB", "WU", "WP", "UP", "BP", "RP", "GP", "X", "C", "E"}; - public static void loadImages() { - renameSymbols(getSymbolsPath() + File.separator + "symbols"); - smallSymbolsFound = loadSymbolsImages(15); - mediumSymbolsFound = loadSymbolsImages(25); + private static final JLabel labelRender = new JLabel(); // render mana text + public static void loadImages() { + // TODO: delete files rename jpg->gif (it was for backward compatibility for one of the old version?) + renameSymbols(getResourceSymbolsPath(ResourceSymbolSize.SMALL)); + renameSymbols(getResourceSymbolsPath(ResourceSymbolSize.MEDIUM)); + renameSymbols(getResourceSymbolsPath(ResourceSymbolSize.LARGE)); + //renameSymbols(getSymbolsPath(ResourceSymbolSize.SVG)); // not need + // TODO: remove medium sets files to "medium" folder like symbols above? + + // preload symbol images + loadSymbolImages(15); + loadSymbolImages(25); + loadSymbolImages(50); + + // save symbol images in png for html replacement in texts + // you can add bigger size for better quality + Map pngImages = manaImages.get(50); + if (pngImages != null){ + + File pngPath = new File(getResourceSymbolsPath(ResourceSymbolSize.PNG)); + if (!pngPath.exists()) { + pngPath.mkdirs(); + } + + for(String symbol: symbols){ + try + { + BufferedImage image = pngImages.get(symbol); + if (image != null){ + File newFile = new File(pngPath.getPath() + File.separator + symbol + ".png"); + ImageIO.write(image, "png", newFile); + } + }catch (Exception e) { + LOGGER.warn("Can't generate png image for symbol:" + symbol); + } + } + } + + // preload set images List setCodes = ExpansionRepository.instance.getSetCodes(); if (setCodes == null) { // the cards db file is probaly not included in the client. It will be created after the first connect to a server. @@ -75,9 +122,11 @@ public final class ManaSymbols { return; } for (String set : setCodes) { + if (withoutSymbols.contains(set)) { continue; } + String[] codes; if (onlyMythics.contains(set)) { codes = new String[]{"M"}; @@ -88,8 +137,9 @@ public final class ManaSymbols { Map rarityImages = new HashMap<>(); setImages.put(set, rarityImages); + // load medium size for (String rarityCode : codes) { - File file = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET + set + '-' + rarityCode + ".jpg"); + File file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM) + set + '-' + rarityCode + ".jpg"); try { Image image = UI.getImageIcon(file.getAbsolutePath()).getImage(); int width = image.getWidth(null); @@ -107,18 +157,19 @@ public final class ManaSymbols { } } + // generate small size try { - File file = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET_SMALL); + File file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM)); if (!file.exists()) { file.mkdirs(); } for (String code : codes) { - file = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET_SMALL + set + '-' + code + ".png"); + file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM) + set + '-' + code + ".png"); if (file.exists()) { continue; } - file = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET + set + '-' + code + ".jpg"); + file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM) + set + '-' + code + ".jpg"); Image image = UI.getImageIcon(file.getAbsolutePath()).getImage(); try { int width = image.getWidth(null); @@ -130,7 +181,7 @@ public final class ManaSymbols { } Rectangle r = new Rectangle(15 + dx, (int) (height * (15.0f + dx) / width)); BufferedImage resized = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(image, BufferedImage.TYPE_INT_ARGB), r); - File newFile = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET_SMALL + File.separator + set + '-' + code + ".png"); + File newFile = new File(getResourceSetsPath(ResourceSetSize.SMALL) + set + '-' + code + ".png"); ImageIO.write(resized, "png", newFile); } } catch (Exception e) { @@ -144,13 +195,15 @@ public final class ManaSymbols { } } + // mark loaded images + // TODO: delete that code, images draw-show must dynamicly File file; for (String set : ExpansionRepository.instance.getSetCodes()) { - file = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET_SMALL); + file = new File(getResourceSetsPath(ResourceSetSize.SMALL)); if (!file.exists()) { break; } - file = new File(getSymbolsPath() + Constants.RESOURCE_PATH_SET_SMALL + set + "-C.png"); + file = new File(getResourceSetsPath(ResourceSetSize.SMALL) + set + "-C.png"); try { Image image = UI.getImageIcon(file.getAbsolutePath()).getImage(); int width = image.getWidth(null); @@ -161,42 +214,253 @@ public final class ManaSymbols { } } - private static boolean loadSymbolsImages(int size) { - boolean fileErrors = false; - HashMap sizedSymbols = new HashMap<>(); - String resourcePath = Constants.RESOURCE_PATH_MANA_SMALL; - if (size > 25) { - resourcePath = Constants.RESOURCE_PATH_MANA_LARGE; - } else if (size > 15) { - resourcePath = Constants.RESOURCE_PATH_MANA_MEDIUM; - } - for (String symbol : symbols) { - File file = new File(getSymbolsPath() + resourcePath + '/' + symbol + ".gif"); - try { + public static BufferedImage loadSVG(File svgFile, int resizeToWidth, int resizeToHeight, boolean useShadow) throws IOException { - if (size == 15 || size == 25) { - BufferedImage notResized = ImageIO.read(file); - sizedSymbols.put(symbol, notResized); - } else { - Rectangle r = new Rectangle(size, size); - //Image image = UI.getImageIcon(file.getAbsolutePath()).getImage(); - BufferedImage image = ImageIO.read(file); - //BufferedImage resized = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(image, BufferedImage.TYPE_INT_ARGB), r); - if (image != null) { - BufferedImage resized = ImageHelper.getResizedImage(image, r); - sizedSymbols.put(symbol, resized); - } + // debug: disable shadow gen, need to test it + useShadow = false; + + // load SVG image + // base loader code: https://stackoverflow.com/questions/11435671/how-to-get-a-buffererimage-from-a-svg + // resize code: https://vibranttechie.wordpress.com/2015/05/15/svg-loading-to-javafx-stage-and-auto-scaling-when-stage-resize/ + + if (useShadow && ((resizeToWidth <= 0) || (resizeToHeight <= 0))){ + throw new IllegalArgumentException("Must use non zero sizes for shadow."); + } + + final BufferedImage[] imagePointer = new BufferedImage[1]; + + // Rendering hints can't be set programatically, so + // we override defaults with a temporary stylesheet. + // These defaults emphasize quality and precision, and + // are more similar to the defaults of other SVG viewers. + // SVG documents can still override these defaults. + String css = "svg {" + + "shape-rendering: geometricPrecision;" + + "text-rendering: geometricPrecision;" + + "color-rendering: optimizeQuality;" + + "image-rendering: optimizeQuality;" + + "}"; + File cssFile = File.createTempFile("batik-default-override-", ".css"); + FileWriter w = new FileWriter(cssFile); + w.write(css); + w.close(); + + TranscodingHints transcoderHints = new TranscodingHints(); + + // resize + int shadowX = 0; + int shadowY = 0; + if(useShadow) { + // shadow size (16px image: 1px left, 2px bottom) + shadowX = 1 * Math.round(1f / 16f * resizeToWidth); + shadowY = 2 * Math.round(1f / 16f * resizeToHeight); + resizeToWidth = resizeToWidth - shadowX; + resizeToHeight = resizeToHeight - shadowY; + }; + + if(resizeToWidth > 0){ + transcoderHints.put(ImageTranscoder.KEY_WIDTH, (float)resizeToWidth); //your image width + } + if(resizeToHeight > 0){ + transcoderHints.put(ImageTranscoder.KEY_HEIGHT, (float)resizeToHeight); //your image height + } + + transcoderHints.put(ImageTranscoder.KEY_XML_PARSER_VALIDATING, Boolean.FALSE); + transcoderHints.put(ImageTranscoder.KEY_DOM_IMPLEMENTATION, + SVGDOMImplementation.getDOMImplementation()); + transcoderHints.put(ImageTranscoder.KEY_DOCUMENT_ELEMENT_NAMESPACE_URI, + SVGConstants.SVG_NAMESPACE_URI); + transcoderHints.put(ImageTranscoder.KEY_DOCUMENT_ELEMENT, "svg"); + transcoderHints.put(ImageTranscoder.KEY_USER_STYLESHEET_URI, cssFile.toURI().toString()); + + try { + TranscoderInput input = new TranscoderInput(new FileInputStream(svgFile)); + + ImageTranscoder t = new ImageTranscoder() { + + @Override + public BufferedImage createImage(int w, int h) { + return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); } - } catch (IOException e) { - LOGGER.error("Error for symbol:" + symbol); + + @Override + public void writeImage(BufferedImage image, TranscoderOutput out) + throws TranscoderException { + imagePointer[0] = image; + } + }; + t.setTranscodingHints(transcoderHints); + t.transcode(input, null); + } + catch (TranscoderException ex) { + // Requires Java 6 + ex.printStackTrace(); + throw new IOException("Couldn't convert " + svgFile); + } + finally { + cssFile.delete(); + } + + BufferedImage originImage = imagePointer[0]; + + if(useShadow && (originImage.getWidth() > 0)){ + // draw shadow + // origin image was reduces in sizes to fit shadow + // see https://stackoverflow.com/a/40833715/1276632 + + // a filter which converts all colors except 0 to black + ImageProducer prod = new FilteredImageSource(originImage.getSource(), new RGBImageFilter() { + @Override + public int filterRGB(int x, int y, int rgb) { + if (rgb == 0) + return 0; + else + return 0xff000000; + } + }); + // create whe black image + Image shadow = Toolkit.getDefaultToolkit().createImage(prod); + // result + BufferedImage result = new BufferedImage(originImage.getWidth() + shadowX, originImage.getHeight() + shadowY, originImage.getType()); + Graphics2D g = (Graphics2D) result.getGraphics(); + // draw shadow with offset (left bottom) + g.drawImage(shadow, -1 * shadowX, shadowY, null); + // draw original image + g.drawImage(originImage, 0, 0, null); + return result; + }else{ + // return origin image without shadow + return originImage; + } + + /* + BufferedImage base = GraphicsUtilities.createCompatibleTranslucentImage(w, h); + Graphics2D g2 = base.createGraphics(); + g2.setColor(Color.WHITE); + g2.fillRoundRect(0, 0, image.getWidth(), image.getHeight(), 10, 10); + g2.dispose(); + + ShadowRenderer renderer = new ShadowRenderer(shadowSize, 0.5f, + Color.GRAY); + return renderer.createShadow(base); + */ + + //imagePointer[0]; + } + + private static File getSymbolFileNameAsSVG(String symbol){ + return new File(getResourceSymbolsPath(ResourceSymbolSize.SVG) + symbol + ".svg"); + } + + private static BufferedImage loadSymbolAsSVG(String symbol, int resizeToWidth, int resizeToHeight){ + + File sourceFile = getSymbolFileNameAsSVG(symbol); + return loadSymbolAsSVG(sourceFile, resizeToWidth, resizeToHeight); + } + + private static BufferedImage loadSymbolAsSVG(File sourceFile, int resizeToWidth, int resizeToHeight){ + try{ + // no need to resize svg (lib already do it on load) + return loadSVG(sourceFile, resizeToWidth, resizeToHeight, true); + + } catch (Exception e) { + LOGGER.error("Can't load svg symbol: " + sourceFile.getPath()); + return null; + } + } + + private static File getSymbolFileNameAsGIF(String symbol, int size){ + + ResourceSymbolSize needSize = null; + if (size <= 15){ + needSize = ResourceSymbolSize.SMALL; + }else if (size <= 25){ + needSize = ResourceSymbolSize.MEDIUM; + } else { + needSize = ResourceSymbolSize.LARGE; + } + + return new File(getResourceSymbolsPath(needSize) + symbol + ".gif"); + } + + private static BufferedImage loadSymbolAsGIF(String symbol, int resizeToWidth, int resizeToHeight){ + File file = getSymbolFileNameAsGIF(symbol, resizeToWidth); + return loadSymbolAsGIF(file, resizeToWidth, resizeToHeight); + } + + private static BufferedImage loadSymbolAsGIF(File sourceFile, int resizeToWidth, int resizeToHeight){ + + BufferedImage image = null; + + try { + if ((resizeToWidth == 15) || (resizeToWidth == 25)){ + // normal size + image = ImageIO.read(sourceFile); + }else{ + // resize size + image = ImageIO.read(sourceFile); + + if (image != null) { + Rectangle r = new Rectangle(resizeToWidth, resizeToHeight); + image = ImageHelper.getResizedImage(image, r); + } + } + } catch (IOException e) { + LOGGER.error("Can't load gif symbol: " + sourceFile.getPath()); + return null; + } + + return image; + } + + private static boolean loadSymbolImages(int size) { + // load all symbols to cash + // priority: SVG -> GIF + // gif remain for backward compatibility + + boolean fileErrors = false; + + HashMap sizedSymbols = new HashMap<>(); + for (String symbol : symbols) { + + BufferedImage image = null; + File file = null; + + // svg + file = getSymbolFileNameAsSVG(symbol); + if (file.exists()) { + image = loadSymbolAsSVG(file, size, size); + } + + // gif + if (image == null) { + //LOGGER.info("SVG symbol can't be load: " + file.getPath()); + + file = getSymbolFileNameAsGIF(symbol, size); + if (file.exists()) { + image = loadSymbolAsGIF(file, size, size); + } + } + + // save + if (image != null) { + sizedSymbols.put(symbol, image); + } else { fileErrors = true; + LOGGER.warn("SVG or GIF symbol can't be load: " + symbol); } } + manaImages.put(size, sizedSymbols); return !fileErrors; } private static void renameSymbols(String path) { + File file = new File(path); + if (!file.exists()){ + return; + } + final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.jpg"); try { Files.walkFileTree(Paths.get(path), new SimpleFileVisitor() { @@ -210,60 +474,184 @@ public final class ManaSymbols { } }); } catch (IOException e) { - LOGGER.error("Couldn't rename mana symbols!"); + LOGGER.error("Couldn't rename mana symbols on " + path, e); } } - private static String getSymbolsPath() { - return getSymbolsPath(false); + private static String getResourceSymbolsPath(ResourceSymbolSize needSize){ + // return real path to symbols (default or user defined) + + String path = CardImageUtils.getImagesDir() + Constants.RESOURCE_PATH_SYMBOLS + File.separator; + + // folder by sizes + switch (needSize) { + case SMALL: + path = path + Constants.RESOURCE_SYMBOL_FOLDER_SMALL; + break; + case MEDIUM: + path = path + Constants.RESOURCE_SYMBOL_FOLDER_MEDIUM; + break; + case LARGE: + path = path + Constants.RESOURCE_SYMBOL_FOLDER_LARGE; + break; + case SVG: + path = path + Constants.RESOURCE_SYMBOL_FOLDER_SVG; + break; + case PNG: + path = path + Constants.RESOURCE_SYMBOL_FOLDER_PNG; + break; + default: + throw new java.lang.IllegalArgumentException( + "ResourceSymbolSize value is unknown"); + } + + // fix double separator if size folder is not set + while(path.endsWith(File.separator)) + { + path = path.substring(0, path.length() - 1); + } + + return path + File.separator; } - private static String getSymbolsPath(boolean forHtmlCode) { - String useDefault = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_USE_DEFAULT, "true"); - String path = useDefault.equals("true") ? null : PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_PATH, null); - if (path == null) { - if (forHtmlCode) { - // for html code we need to use double '//' symbols - // and seems it should be hard coded - as it is not the same as using File.separator - return "plugins/images/"; - } else { - return mage.client.constants.Constants.IO.imageBaseDir; - } + private static String getResourceSetsPath(ResourceSetSize needSize){ + // return real path to sets icons (default or user defined) + + String path = CardImageUtils.getImagesDir() + Constants.RESOURCE_PATH_SYMBOLS + File.separator; + + // folder by sizes + switch (needSize) { + case SMALL: + path = path + Constants.RESOURCE_SET_FOLDER_SMALL; + break; + case MEDIUM: + path = path + Constants.RESOURCE_SET_FOLDER_MEDIUM; + break; + case SVG: + path = path + Constants.RESOURCE_SET_FOLDER_SVG; + break; + default: + throw new java.lang.IllegalArgumentException( + "ResourceSetSize value is unknown"); } - if (forHtmlCode) { - if (cachedPath != null) { - return cachedPath; - } - if (path.contains("\\")) { - cachedPath = path.replaceAll("[\\\\]", "/"); - return cachedPath; - } + + // fix double separator if size folder is not set + while(path.endsWith(File.separator)) + { + path = path.substring(0, path.length() - 1); } - return path; + + return path + File.separator; } public static void draw(Graphics g, String manaCost, int x, int y, int symbolWidth) { + draw(g, manaCost, x, y, symbolWidth, Color.white, 0); + } + + public static void draw(Graphics g, String manaCost, int x, int y, int symbolWidth, Color symbolsTextColor, int symbolMarginX) { if (!manaImages.containsKey(symbolWidth)) { - loadSymbolsImages(symbolWidth); + loadSymbolImages(symbolWidth); } + + // TODO: replace with jlabel render (look at table rendere)? + + /* + // NEW version with component draw + JPanel manaPanel = new JPanel(); + + // icons size with margin + int symbolHorizontalMargin = 2; + + // create each mana symbol as child label + manaPanel.removeAll(); + manaPanel.setLayout(new BoxLayout(manaPanel, BoxLayout.X_AXIS)); + StringTokenizer tok = new StringTokenizer(manaCost, " "); + while (tok.hasMoreTokens()) { + String symbol = tok.nextToken(); + + JLabel symbolLabel = new JLabel(); + //symbolLabel.setBorder(new LineBorder(new Color(150, 150, 150))); // debug + symbolLabel.setBorder(new EmptyBorder(0, symbolHorizontalMargin,0, 0)); + + BufferedImage image = ManaSymbols.getSizedManaSymbol(symbol, symbolWidth); + if (image != null){ + // icon + symbolLabel.setIcon(new ImageIcon(image)); + }else + { + // text + symbolLabel.setText("{" + symbol + "}"); + //symbolLabel.setOpaque(baseLabel.isOpaque()); + //symbolLabel.setForeground(baseLabel.getForeground()); + //symbolLabel.setBackground(baseLabel.getBackground()); + } + + manaPanel.add(symbolLabel); + } + + // draw result + Dimension d = manaPanel.getPreferredSize(); + BufferedImage image = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_ARGB); + Graphics2D gg = image.createGraphics(); + manaPanel.paint(gg); + g.drawImage(image, x, y, null); +*/ + + + + // OLD version with custom draw Map sizedSymbols = manaImages.get(symbolWidth); if (manaCost.isEmpty()) { return; } + manaCost = manaCost.replace("\\", ""); manaCost = UI.getDisplayManaCost(manaCost); StringTokenizer tok = new StringTokenizer(manaCost, " "); while (tok.hasMoreTokens()) { String symbol = tok.nextToken(); - // Check and load symbol in the width Image image = sizedSymbols.get(symbol); + if (image == null) { - //log.error("Symbol not recognized \"" + symbol + "\" in mana cost: " + manaCost); - continue; + // TEXT draw + + labelRender.setText("{" + symbol + "}"); + labelRender.setForeground(symbolsTextColor); + labelRender.setSize(symbolWidth, symbolWidth); + labelRender.setVerticalAlignment(SwingConstants.CENTER); + labelRender.setHorizontalAlignment(SwingConstants.CENTER); + //labelRender.setBorder(new LineBorder(new Color(125, 250, 250), 1)); + + // fix font size for mana text + // work for labels WITHOUT borders + // https://stackoverflow.com/questions/2715118/how-to-change-the-size-of-the-font-of-a-jlabel-to-take-the-maximum-size + Font labelFont = labelRender.getFont(); + String labelText = "{W}"; //labelRender.getText(); // need same font size for all -- use max symbol ever, not current text + int stringWidth = labelRender.getFontMetrics(labelFont).stringWidth(labelText); + int componentWidth = labelRender.getWidth(); + // Find out how much the font can grow in width. + double widthRatio = (double)componentWidth / (double)stringWidth; + int newFontSize = (int)(labelFont.getSize() * widthRatio); + int componentHeight = labelRender.getHeight(); + // Pick a new font size so it will not be larger than the height of label. + int fontSizeToUse = Math.min(newFontSize, componentHeight); + // Set the label's font size to the newly determined size. + labelRender.setFont(new Font(labelFont.getName(), Font.PLAIN + Font.BOLD, fontSizeToUse - 1)); // - for "..." fix in text + + // render component to new position + // need to copy graphics, overvise it draw at top left corner + // https://stackoverflow.com/questions/4974268/java-paint-problem + Graphics2D labelG = (Graphics2D)g.create(x, y, symbolWidth, symbolWidth); + labelG.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + labelG.fillOval(x + 1, y + 1, symbolWidth - 2, symbolWidth - 2); + labelRender.paint(labelG); + }else { + // ICON draw + g.drawImage(image, x, y, null); } - g.drawImage(image, x, y, null); - x += symbolWidth; + x += symbolWidth + symbolMarginX; } + } public static String getStringManaCost(List manaCost) { @@ -281,11 +669,21 @@ public final class ManaSymbols { TOOLTIP, } + private static String filePathToUrl(String path){ + // convert file path to uri path (for html docs) + if((path != null) && (!path.equals(""))){ + File file = new File(path); + return file.toURI().toString(); + }else{ + return null; + } + } + public static synchronized String replaceSymbolsWithHTML(String value, Type type) { - value = value.replace("{source}", "|source|"); - value = value.replace("{this}", "|this|"); - String replaced = value; - boolean symbolFilesFound; + + // mana cost to HTML images (urls to files) + // do not use it for new code - try to suppotr svg render + int symbolSize; switch (type) { case TABLE: @@ -304,21 +702,37 @@ public final class ManaSymbols { symbolSize = 11; break; } - String resourcePath = "small"; - symbolFilesFound = smallSymbolsFound; - if (symbolSize > 25) { - resourcePath = "large"; - } else if (symbolSize > 15) { - resourcePath = "medium"; - symbolFilesFound = mediumSymbolsFound; - } - if (symbolFilesFound) { - replaced = REPLACE_SYMBOLS_PATTERN.matcher(value).replaceAll( - "$1$2"; + return "" + rarity + ""; } else { return set; } @@ -353,9 +767,10 @@ public final class ManaSymbols { public static BufferedImage getSizedManaSymbol(String symbol, int size) { if (!manaImages.containsKey(size)) { - loadSymbolsImages(size); + loadSymbolImages(size); } Map sizedSymbols = manaImages.get(size); return sizedSymbols.get(symbol); } } + diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbolsCellRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbolsCellRenderer.java new file mode 100644 index 0000000000..1de0abceb1 --- /dev/null +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbolsCellRenderer.java @@ -0,0 +1,64 @@ +package org.mage.card.arcane; + +import mage.client.util.GUISizeHelper; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.table.DefaultTableCellRenderer; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.StringTokenizer; + +public final class ManaSymbolsCellRenderer extends DefaultTableCellRenderer { + + // base panel to render + private JPanel manaPanel = new JPanel(); + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + + // get table text cell settings + DefaultTableCellRenderer baseRenderer = (DefaultTableCellRenderer) table.getDefaultRenderer(String.class); + JLabel baseLabel = (JLabel)baseRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + // apply settings to mana panel from parent + manaPanel.setOpaque(baseLabel.isOpaque()); + manaPanel.setForeground(baseLabel.getForeground()); + manaPanel.setBackground(baseLabel.getBackground()); + + // icons size with margin + int symbolWidth = GUISizeHelper.symbolTableSize; + int symbolHorizontalMargin = 2; + + // create each mana symbol as child label + String manaCost = (String)value; + manaPanel.removeAll(); + manaPanel.setLayout(new BoxLayout(manaPanel, BoxLayout.X_AXIS)); + StringTokenizer tok = new StringTokenizer(manaCost, " "); + while (tok.hasMoreTokens()) { + String symbol = tok.nextToken(); + + JLabel symbolLabel = new JLabel(); + //symbolLabel.setBorder(new LineBorder(new Color(150, 150, 150))); // debug + symbolLabel.setBorder(new EmptyBorder(0, symbolHorizontalMargin,0, 0)); + + BufferedImage image = ManaSymbols.getSizedManaSymbol(symbol, symbolWidth); + if (image != null){ + // icon + symbolLabel.setIcon(new ImageIcon(image)); + }else + { + // text + symbolLabel.setText("{" + symbol + "}"); + symbolLabel.setOpaque(baseLabel.isOpaque()); + symbolLabel.setForeground(baseLabel.getForeground()); + symbolLabel.setBackground(baseLabel.getBackground()); + } + + manaPanel.add(symbolLabel); + } + + return manaPanel; + } +} diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java index 312d232683..a75c12fe63 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java @@ -561,7 +561,7 @@ public class ModernCardRenderer extends CardRenderer { // Draw the mana symbols if (!cardView.isAbility() && !cardView.isFaceDown()) { - ManaSymbols.draw(g, manaCost, x + w - manaCostWidth, y + boxTextOffset, boxTextHeight); + ManaSymbols.draw(g, manaCost, x + w - manaCostWidth, y + boxTextOffset, boxTextHeight, Color.black, 2); } } @@ -844,7 +844,7 @@ public class ModernCardRenderer extends CardRenderer { String symbs = symbol; int symbHeight = (int) (0.8 * h); int manaCostWidth = CardRendererUtils.getManaCostWidth(symbs, symbHeight); - ManaSymbols.draw(g, symbs, x + (w - manaCostWidth) / 2, y + (h - symbHeight) / 2, symbHeight); + ManaSymbols.draw(g, symbs, x + (w - manaCostWidth) / 2, y + (h - symbHeight) / 2, symbHeight, Color.black, 2); } // Get the first line of the textbox, the keyword string diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java b/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java index 080772869e..5825c34def 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java @@ -6,7 +6,6 @@ import mage.client.dialog.PreferencesDialog; import mage.client.util.GUISizeHelper; import mage.constants.Rarity; import mage.interfaces.plugin.CardPlugin; -import mage.utils.CardUtil; import mage.view.CardView; import mage.view.CounterView; import mage.view.PermanentView; @@ -19,7 +18,6 @@ import org.mage.card.arcane.*; import org.mage.plugins.card.dl.DownloadGui; import org.mage.plugins.card.dl.DownloadJob; import org.mage.plugins.card.dl.Downloader; -import org.mage.plugins.card.dl.sources.CardFrames; import org.mage.plugins.card.dl.sources.DirectLinksForDownload; import org.mage.plugins.card.dl.sources.GathererSets; import org.mage.plugins.card.dl.sources.GathererSymbols; @@ -525,30 +523,30 @@ public class CardPluginImpl implements CardPlugin { /** * Download various symbols (mana, tap, set). * - * @param imagesPath Path to check in and store symbols to. Can be null, in - * such case default path should be used. + * @param imagesDir Path to check in and store symbols to. Can't be null. */ @Override - public void downloadSymbols(String imagesPath) { + public void downloadSymbols(String imagesDir) { final DownloadGui g = new DownloadGui(new Downloader()); - Iterable it = new GathererSymbols(imagesPath); - + Iterable it = new GathererSymbols(); for (DownloadJob job : it) { g.getDownloader().add(job); } - it = new GathererSets(imagesPath); + it = new GathererSets(); for (DownloadJob job : it) { g.getDownloader().add(job); } - it = new CardFrames(imagesPath); + /* + it = new CardFrames(imagesDir); // TODO: delete frames download (not need now) for (DownloadJob job : it) { g.getDownloader().add(job); } + */ - it = new DirectLinksForDownload(imagesPath); + it = new DirectLinksForDownload(); for (DownloadJob job : it) { g.getDownloader().add(job); } @@ -560,6 +558,7 @@ public class CardPluginImpl implements CardPlugin { public void windowClosing(WindowEvent e) { g.getDownloader().dispose(); ManaSymbols.loadImages(); + // TODO: check reload process after download (icons do not update) } }); d.setLayout(new BorderLayout()); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/constants/Constants.java b/Mage.Client/src/main/java/org/mage/plugins/card/constants/Constants.java deleted file mode 100644 index cbf5eb1880..0000000000 --- a/Mage.Client/src/main/java/org/mage/plugins/card/constants/Constants.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.mage.plugins.card.constants; - -import java.awt.Rectangle; -import java.io.File; - -public final class Constants { - - public static final String RESOURCE_PATH_SET = File.separator + "sets" + File.separator; - - public static final String RESOURCE_PATH_MANA_SMALL = File.separator + "symbols" + File.separator + "small"; - public static final String RESOURCE_PATH_MANA_LARGE = File.separator + "symbols" + File.separator + "large"; - public static final String RESOURCE_PATH_MANA_MEDIUM = File.separator + "symbols" + File.separator + "medium"; - - public static final String RESOURCE_PATH_SET_SMALL = RESOURCE_PATH_SET + File.separator + "small" + File.separator; - - public static final Rectangle CARD_SIZE_FULL = new Rectangle(101, 149); - public static final Rectangle THUMBNAIL_SIZE_FULL = new Rectangle(102, 146); - - public interface IO { - - String imageBaseDir = "plugins" + File.separator + "images"; - String IMAGE_PROPERTIES_FILE = "image.url.properties"; - } - - public static final String CARD_IMAGE_PATH_TEMPLATE = '.' + File.separator + "plugins" + File.separator + "images/{set}/{name}.{collector}.full.jpg"; -} diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java index 374257a693..7896fa26ef 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java @@ -11,9 +11,12 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; + +import mage.client.constants.Constants; import org.mage.plugins.card.dl.DownloadJob; import static org.mage.plugins.card.dl.DownloadJob.fromURL; import static org.mage.plugins.card.dl.DownloadJob.toFile; +import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; /** * Used when we need to point to direct links to download resources from. @@ -33,15 +36,13 @@ public class DirectLinksForDownload implements Iterable { directLinks.put(cardbackFilename, backsideUrl); } - private static final String DEFAULT_IMAGES_PATH = File.separator + "default"; - private static final File DEFAULT_OUT_DIR = new File("plugins" + File.separator + "images" + DEFAULT_IMAGES_PATH); - public static File outDir = DEFAULT_OUT_DIR; + public static File outDir; - public DirectLinksForDownload(String path) { - if (path == null) { - useDefaultDir(); - } else { - changeOutDir(path); + public DirectLinksForDownload() { + outDir = new File(getImagesDir() + Constants.RESOURCE_PATH_DEFAUL_IMAGES); + + if (!outDir.exists()){ + outDir.mkdirs(); } } @@ -55,20 +56,4 @@ public class DirectLinksForDownload implements Iterable { } return jobs.iterator(); } - - private void changeOutDir(String path) { - File file = new File(path + DEFAULT_IMAGES_PATH); - if (file.exists()) { - outDir = file; - } else { - file.mkdirs(); - if (file.exists()) { - outDir = file; - } - } - } - - private void useDefaultDir() { - outDir = DEFAULT_OUT_DIR; - } } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java index 496768a6cf..8406c4e541 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java @@ -9,11 +9,14 @@ import java.util.Iterator; import mage.cards.ExpansionSet; import mage.cards.Sets; +import mage.client.constants.Constants; import mage.constants.Rarity; import org.mage.plugins.card.dl.DownloadJob; import static org.mage.plugins.card.dl.DownloadJob.fromURL; import static org.mage.plugins.card.dl.DownloadJob.toFile; +import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; + import org.apache.log4j.Logger; public class GathererSets implements Iterable { @@ -36,13 +39,11 @@ public class GathererSets implements Iterable { } } + private static File outDir; + private static final int DAYS_BEFORE_RELEASE_TO_DOWNLOAD = +14; // Try to load the symbolsBasic eralies 14 days before release date private static final Logger logger = Logger.getLogger(GathererSets.class); - private static final String SETS_PATH = File.separator + "sets"; - private static final File DEFAULT_OUT_DIR = new File("plugins" + File.separator + "images" + SETS_PATH); - private static File outDir = DEFAULT_OUT_DIR; - private static final String[] symbolsBasic = {"10E", "9ED", "8ED", "7ED", "6ED", "5ED", "4ED", "3ED", "2ED", "LEB", "LEA", "HOP", "ARN", "ATQ", "LEG", "DRK", "FEM", "HML", @@ -150,11 +151,12 @@ public class GathererSets implements Iterable { codeReplacements.put("CHR", "CH"); } - public GathererSets(String path) { - if (path == null) { - useDefaultDir(); - } else { - changeOutDir(path); + public GathererSets() { + + outDir = new File(getImagesDir() + Constants.RESOURCE_PATH_SYMBOLS); + + if (!outDir.exists()){ + outDir.mkdirs(); } } @@ -311,20 +313,4 @@ public class GathererSets implements Iterable { String url = "http://gatherer.wizards.com/Handlers/Image.ashx?type=symbol&set=" + set + "&size=small&rarity=" + urlRarity; return new DownloadJob(set + '-' + rarity, fromURL(url), toFile(dst)); } - - private void changeOutDir(String path) { - File file = new File(path + SETS_PATH); - if (file.exists()) { - outDir = file; - } else { - file.mkdirs(); - if (file.exists()) { - outDir = file; - } - } - } - - private void useDefaultDir() { - outDir = DEFAULT_OUT_DIR; - } } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java index 995b182e5b..6a6061e06c 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java @@ -9,9 +9,12 @@ import com.google.common.collect.AbstractIterator; import java.io.File; import static java.lang.String.format; import java.util.Iterator; + +import mage.client.constants.Constants; import org.mage.plugins.card.dl.DownloadJob; import static org.mage.plugins.card.dl.DownloadJob.fromURL; import static org.mage.plugins.card.dl.DownloadJob.toFile; +import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; /** * The class GathererSymbols. @@ -23,9 +26,7 @@ public class GathererSymbols implements Iterable { //TODO chaos and planeswalker symbol //chaos: http://gatherer.wizards.com/Images/Symbols/chaos.gif - private static final String SYMBOLS_PATH = File.separator + "symbols"; - private static final File DEFAULT_OUT_DIR = new File("plugins" + File.separator + "images" + SYMBOLS_PATH); - private static File outDir = DEFAULT_OUT_DIR; + private static File outDir; private static final String urlFmt = "http://gatherer.wizards.com/handlers/image.ashx?size=%1$s&name=%2$s&type=symbol"; @@ -38,11 +39,11 @@ public class GathererSymbols implements Iterable { "X", "S", "T", "Q", "C", "E"}; private static final int minNumeric = 0, maxNumeric = 16; - public GathererSymbols(String path) { - if (path == null) { - useDefaultDir(); - } else { - changeOutDir(path); + public GathererSymbols() { + outDir = new File(getImagesDir() + Constants.RESOURCE_PATH_SYMBOLS); + + if (!outDir.exists()){ + outDir.mkdirs(); } } @@ -115,20 +116,4 @@ public class GathererSymbols implements Iterable { } }; } - - private void changeOutDir(String path) { - File file = new File(path + SYMBOLS_PATH); - if (file.exists()) { - outDir = file; - } else { - file.mkdirs(); - if (file.exists()) { - outDir = file; - } - } - } - - private void useDefaultDir() { - outDir = DEFAULT_OUT_DIR; - } } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPictures.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPictures.java index 0895b79219..59a6be5bc5 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPictures.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPictures.java @@ -27,13 +27,9 @@ import mage.cards.repository.CardCriteria; import mage.cards.repository.CardInfo; import mage.cards.repository.CardRepository; import mage.client.MageFrame; -import mage.client.constants.Constants; import mage.client.dialog.PreferencesDialog; import mage.client.util.sets.ConstructedFormats; import mage.remote.Connection; -import static mage.remote.Connection.ProxyType.HTTP; -import static mage.remote.Connection.ProxyType.NONE; -import static mage.remote.Connection.ProxyType.SOCKS; import net.java.truevfs.access.TFile; import net.java.truevfs.access.TFileOutputStream; import net.java.truevfs.access.TVFS; @@ -43,6 +39,8 @@ import org.mage.plugins.card.dl.sources.*; import org.mage.plugins.card.properties.SettingsManager; import org.mage.plugins.card.utils.CardImageUtils; +import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; + public class DownloadPictures extends DefaultBoundedRangeModel implements Runnable { private static DownloadPictures instance; @@ -462,7 +460,7 @@ public class DownloadPictures extends DefaultBoundedRangeModel implements Runnab */ List cardsToDownload = Collections.synchronizedList(new ArrayList<>()); allCardsUrls.parallelStream().forEach(card -> { - TFile file = new TFile(CardImageUtils.generateImagePath(card)); + File file = new TFile(CardImageUtils.buildImagePathToCard(card)); logger.debug(card.getName() + " (is_token=" + card.isToken() + "). Image is here:" + file.getAbsolutePath() + " (exists=" + file.exists() + ')'); if (!file.exists()) { logger.debug("Missing: " + file.getAbsolutePath()); @@ -544,7 +542,7 @@ public class DownloadPictures extends DefaultBoundedRangeModel implements Runnab public void run() { this.cardIndex = 0; - File base = new File(Constants.IO.imageBaseDir); + File base = new File(getImagesDir()); if (!base.exists()) { base.mkdir(); } @@ -678,38 +676,78 @@ public class DownloadPictures extends DefaultBoundedRangeModel implements Runnab @Override public void run() { - StringBuilder filePath = new StringBuilder(); - File temporaryFile = null; - TFile outputFile = null; + if (cancel) { + synchronized (sync) { + update(cardIndex + 1, count); + } + return; + } + + TFile fileTempImage; + TFile destFile; try { - filePath.append(Constants.IO.imageBaseDir); - if (!useSpecifiedPaths && card != null) { - filePath.append(card.hashCode()).append('.').append(card.getName().replace(":", "").replace("//", "-")).append(".jpg"); - temporaryFile = new File(filePath.toString()); - } - String imagePath; - if (useSpecifiedPaths) { - if (card != null && card.isToken()) { - imagePath = CardImageUtils.getTokenBasePath() + actualFilename; - } else if (card != null) { - imagePath = CardImageUtils.getImageBasePath() + actualFilename; - } else { - imagePath = Constants.IO.imageBaseDir; - } - String tmpFile = filePath + "temporary" + actualFilename; - temporaryFile = new File(tmpFile); - if (!temporaryFile.exists()) { - temporaryFile.getParentFile().mkdirs(); + if (card == null){ + synchronized (sync) { + update(cardIndex + 1, count); } - } else { - imagePath = CardImageUtils.generateImagePath(card); + return; } - outputFile = new TFile(imagePath); - if (!outputFile.exists()) { - outputFile.getParentFile().mkdirs(); + // gen temp file (download to images folder) + String tempPath = getImagesDir() + File.separator + "downloading" + File.separator; + if(useSpecifiedPaths){ + fileTempImage = new TFile(tempPath + actualFilename + "-" + card.hashCode() + ".jpg"); + }else{ + fileTempImage = new TFile(tempPath + CardImageUtils.prepareCardNameForFile(card.getName()) + "-" + card.hashCode() + ".jpg"); } + if(!fileTempImage.getParentFile().exists()){ + fileTempImage.getParentFile().mkdirs(); + } + + // gen dest file name + if(useSpecifiedPaths) + { + if(card.isToken()){ + destFile = new TFile(CardImageUtils.buildImagePathToSet(card) + actualFilename + ".jpg"); + }else{ + destFile = new TFile(CardImageUtils.buildImagePathToTokens() + actualFilename + ".jpg"); + } + }else{ + destFile = new TFile(CardImageUtils.buildImagePathToCard(card)); + } + + // FILE already exists (in zip or in dir) + if (destFile.exists()){ + synchronized (sync) { + update(cardIndex + 1, count); + } + return; + } + + // zip can't be read + TFile testArchive = destFile.getTopLevelArchive(); + if (testArchive != null && testArchive.exists()) { + try { + testArchive.list(); + } catch (Exception e) { + logger.error("Error reading archive, may be it was corrapted. Try to delete it: " + testArchive.toString()); + + synchronized (sync) { + update(cardIndex + 1, count); + } + return; + } + } + + /* + if(!destFile.getParentFile().exists()){ + destFile.getParentFile().mkdirs(); + } + */ + + /* + // WTF start?! TODO: wtf File existingFile = new File(imagePath.replaceFirst("\\w{3}.zip", "")); if (existingFile.exists()) { try { @@ -727,7 +765,86 @@ public class DownloadPictures extends DefaultBoundedRangeModel implements Runnab } return; } + // WTF end?! + */ + + // START to download + cardImageSource.doPause(url.getPath()); + URLConnection httpConn = url.openConnection(p); + httpConn.connect(); + int responseCode = ((HttpURLConnection) httpConn).getResponseCode(); + + if (responseCode == 200){ + // download OK + // save data to temp + BufferedOutputStream out; + try (BufferedInputStream in = new BufferedInputStream(httpConn.getInputStream())) { + out = new BufferedOutputStream(new TFileOutputStream(fileTempImage)); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) != -1) { + // user cancelled + if (cancel) { + in.close(); + out.flush(); + out.close(); + + // stop download, save current state and exit + TFile archive = destFile.getTopLevelArchive(); + ///* not need to unmout/close - it's auto action + if (archive != null && archive.exists()){ + logger.info("User canceled download. Closing archive file: " + destFile.toString()); + try { + TVFS.umount(archive); + }catch (Exception e) { + logger.error("Can't close archive file: " + e.getMessage(), e); + } + + }//*/ + try { + TFile.rm(fileTempImage); + }catch (Exception e) { + logger.error("Can't delete temp file: " + e.getMessage(), e); + } + return; + } + out.write(buf, 0, len); + } + } + // TODO: remove to finnaly section? + out.flush(); + out.close(); + + // TODO: add two faces card correction? (WTF) + + // SAVE final data + if (fileTempImage.exists()) { + if (!destFile.getParentFile().exists()){ + destFile.getParentFile().mkdirs(); + } + new TFile(fileTempImage).cp_rp(destFile); + try { + TFile.rm(fileTempImage); + }catch (Exception e) { + logger.error("Can't delete temp file: " + e.getMessage(), e); + } + + } + }else{ + // download ERROR + logger.warn("Image download for " + card.getName() + + (!card.getDownloadName().equals(card.getName()) ? " downloadname: " + card.getDownloadName() : "") + + '(' + card.getSet() + ") failed - responseCode: " + responseCode + " url: " + url.toString() + ); + + if (logger.isDebugEnabled()) { + // Shows the returned html from the request to the web server + logger.debug("Returned HTML ERROR:\n" + convertStreamToString(((HttpURLConnection) httpConn).getErrorStream())); + } + } + + /* // Logger.getLogger(this.getClass()).info(url.toString()); boolean useTempFile = false; int responseCode = 0; @@ -736,7 +853,6 @@ public class DownloadPictures extends DefaultBoundedRangeModel implements Runnab if (temporaryFile != null && temporaryFile.length() > 100) { useTempFile = true; } else { - cardImageSource.doPause(url.getPath()); httpConn = url.openConnection(p); httpConn.connect(); @@ -768,6 +884,7 @@ public class DownloadPictures extends DefaultBoundedRangeModel implements Runnab out.close(); } + // TODO: WTF?! start if (card != null && card.isTwoFacedCard()) { BufferedImage image = ImageIO.read(temporaryFile); if (image.getHeight() == 470) { @@ -790,6 +907,7 @@ public class DownloadPictures extends DefaultBoundedRangeModel implements Runnab outputFile.getParentFile().mkdirs(); new TFile(temporaryFile).cp_rp(outputFile); } + // WTF?! end } else { if (card != null && !useSpecifiedPaths) { logger.warn("Image download for " + card.getName() @@ -800,16 +918,15 @@ public class DownloadPictures extends DefaultBoundedRangeModel implements Runnab logger.debug("Returned HTML ERROR:\n" + convertStreamToString(((HttpURLConnection) httpConn).getErrorStream())); } } + */ } catch (AccessDeniedException e) { - logger.error("The file " + (outputFile != null ? outputFile.toString() : "to add the image of " + card.getName() + '(' + card.getSet() + ')') + " can't be accessed. Try rebooting your system to remove the file lock."); + logger.error("Can't access to files: " + card.getName() + "(" + card.getSet() + "). Try rebooting your system to remove the file lock."); } catch (Exception e) { - logger.error(e, e); + logger.error(e.getMessage(), e); } finally { - if (temporaryFile != null) { - temporaryFile.delete(); - } } + synchronized (sync) { update(cardIndex + 1, count); } @@ -823,7 +940,7 @@ public class DownloadPictures extends DefaultBoundedRangeModel implements Runnab iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); iwp.setCompressionQuality(0.96f); - File tempFile = new File(Constants.IO.imageBaseDir + File.separator + image.hashCode() + file.getName()); + File tempFile = new File(getImagesDir() + File.separator + image.hashCode() + file.getName()); FileImageOutputStream output = new FileImageOutputStream(tempFile); writer.setOutput(output); IIOImage image2 = new IIOImage(image, null, null); @@ -846,7 +963,7 @@ public class DownloadPictures extends DefaultBoundedRangeModel implements Runnab } else { List remainingCards = Collections.synchronizedList(new ArrayList<>()); DownloadPictures.this.allCardsMissingImage.parallelStream().forEach(cardDownloadData -> { - TFile file = new TFile(CardImageUtils.generateImagePath(cardDownloadData)); + TFile file = new TFile(CardImageUtils.buildImagePathToCard(cardDownloadData)); if (!file.exists()) { remainingCards.add(cardDownloadData); } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java index e1f2c66513..337b3daeb5 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java @@ -20,9 +20,9 @@ import net.java.truevfs.access.TFile; import net.java.truevfs.access.TFileInputStream; import net.java.truevfs.access.TFileOutputStream; import org.apache.log4j.Logger; -import org.mage.plugins.card.constants.Constants; import org.mage.plugins.card.dl.sources.DirectLinksForDownload; import org.mage.plugins.card.utils.CardImageUtils; +import mage.client.constants.Constants; /** * This class stores ALL card images in a cache with soft values. this means @@ -94,7 +94,7 @@ public final class ImageCache { path = DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename; // TODO: replace empty token by other default card, not cardback } } else { - path = CardImageUtils.generateImagePath(info); + path = CardImageUtils.buildImagePathToCard(info); } if (path == null) { @@ -263,7 +263,7 @@ public final class ImageCache { path = DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename; // TODO: replace empty token by other default card, not cardback } } else { - path = CardImageUtils.generateImagePath(info); + path = CardImageUtils.buildImagePathToCard(info); } if (thumbnail && path.endsWith(".jpg")) { @@ -340,7 +340,7 @@ public final class ImageCache { // image draw to buffer gg.setComposite(AlphaComposite.SrcAtop); gg.drawImage(image, 0, 0, null); - //gg.dispose(); + gg.dispose(); return cornerImage; } else { diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/properties/SettingsManager.java b/Mage.Client/src/main/java/org/mage/plugins/card/properties/SettingsManager.java index ec46805da6..075a3d9008 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/properties/SettingsManager.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/properties/SettingsManager.java @@ -6,7 +6,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Properties; -import org.mage.plugins.card.constants.Constants; +import mage.client.constants.Constants; public class SettingsManager { diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java b/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java index e8611b63e0..919ad545d3 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java @@ -1,5 +1,6 @@ package org.mage.plugins.card.utils; +import java.io.File; import java.net.InetSocketAddress; import java.net.Proxy; import java.util.HashMap; @@ -63,7 +64,7 @@ public final class CardImageUtils { } private static String getTokenImagePath(CardDownloadData card) { - String filename = generateImagePath(card); + String filename = buildImagePathToCard(card); TFile file = new TFile(filename); if (!file.exists()) { @@ -74,12 +75,12 @@ public final class CardImageUtils { if (!file.exists()) { CardDownloadData updated = new CardDownloadData(card); updated.setName(card.getName() + " 1"); - filename = generateImagePath(updated); + filename = buildImagePathToCard(updated); file = new TFile(filename); if (!file.exists()) { updated = new CardDownloadData(card); updated.setName(card.getName() + " 2"); - filename = generateImagePath(updated); + filename = buildImagePathToCard(updated); } } @@ -121,102 +122,127 @@ public final class CardImageUtils { return set; } - private static String getImageDir(CardDownloadData card, String imagesPath) { + public static String prepareCardNameForFile(String cardName){ + return cardName.replace(":", "").replace("\"", "").replace("//", "-"); + } + + public static String getImagesDir(){ + // return real images dir (path without separator) + + String path = null; + + // user path + if (!PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_USE_DEFAULT, "true").equals("true")){ + path = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_PATH, null); + } + + // default path + if (path == null) { + path = Constants.IO.DEFAULT_IMAGES_DIR; + } + + while(path.endsWith(File.separator)) + { + path = path.substring(0, path.length() - 1); + } + + return path; + } + + public static String buildImagePathToTokens() { + String imagesPath = getImagesDir() + File.separator; + + + if (PreferencesDialog.isSaveImagesToZip()) { + return imagesPath + "TOK.zip" + File.separator; + } else { + return imagesPath + "TOK" + File.separator; + } + } + + private static String buildImagePathToTokenDescriptor(CardDownloadData card) { + return buildImagePathToTokens() + card.getTokenDescriptor() + ".full.jpg"; + } + + public static String buildImagePathToSet(CardDownloadData card) { + if (card.getSet() == null) { - return ""; + throw new IllegalArgumentException("Card " + card.getName() + " have empty set."); } - String set = updateSet(card.getSet(), false).toUpperCase(); - String imagesDir = (imagesPath != null ? imagesPath : Constants.IO.imageBaseDir); + + String set = updateSet(card.getSet(), false).toUpperCase(); // TODO: research auto-replace... old code? + if (card.isToken()) { - return buildTokenPath(imagesDir, set); + return buildImagePathToSetAsToken(set); } else { - return buildPath(imagesDir, set); + return buildImagePathToSetAsCard(set); } } - public static String getImageBasePath() { - String useDefault = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_USE_DEFAULT, "true"); - String imagesPath = Objects.equals(useDefault, "true") ? Constants.IO.imageBaseDir : PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_PATH, null); + private static String buildImagePathToSetAsCard(String set) { + String imagesPath = getImagesDir() + File.separator; - if (imagesPath != null && !imagesPath.endsWith(TFile.separator)) { - imagesPath += TFile.separator; - } - return imagesPath; - } - - public static String getTokenBasePath() { - String imagesPath = getImageBasePath(); - - String finalPath = ""; if (PreferencesDialog.isSaveImagesToZip()) { - finalPath = imagesPath + "TOK" + ".zip" + TFile.separator; + return imagesPath + set + ".zip" + File.separator + set + File.separator; } else { - finalPath = imagesPath + "TOK" + TFile.separator; - } - return finalPath; - } - - private static String getTokenDescriptorImagePath(CardDownloadData card) { - return getTokenBasePath() + card.getTokenDescriptor() + ".full.jpg"; - } - - private static String buildTokenPath(String imagesDir, String set) { - if (PreferencesDialog.isSaveImagesToZip()) { - return imagesDir + TFile.separator + "TOK" + ".zip" + TFile.separator + set; - } else { - return imagesDir + TFile.separator + "TOK" + TFile.separator + set; + return imagesPath + set + File.separator; } } - private static String buildPath(String imagesDir, String set) { - if (PreferencesDialog.isSaveImagesToZip()) { - return imagesDir + TFile.separator + set + ".zip" + TFile.separator + set; - } else { - return imagesDir + TFile.separator + set; - } + private static String buildImagePathToSetAsToken(String set) { + return buildImagePathToTokens() + set + File.separator; } - public static String generateImagePath(CardDownloadData card) { - String useDefault = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_USE_DEFAULT, "true"); - String imagesPath = Objects.equals(useDefault, "true") ? null : PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_PATH, null); + public static String buildImagePathToCard(CardDownloadData card) { - String imageDir = getImageDir(card, imagesPath); - String imageName; + String setPath = buildImagePathToSet(card); - String type = card.getType() != 0 ? ' ' + Integer.toString(card.getType()) : ""; - String name = card.getFileName().isEmpty() ? card.getName().replace(":", "").replace("\"", "").replace("//", "-") : card.getFileName(); - - if (card.getUsesVariousArt()) { - imageName = name + '.' + card.getCollectorId() + ".full.jpg"; - } else { - imageName = name + type + ".full.jpg"; + String prefixType = ""; + if(card.getType() != 0){ + prefixType = " " + Integer.toString(card.getType()); } - if (new TFile(imageDir).exists() && !new TFile(imageDir + TFile.separator + imageName).exists()) { - for (String fileName : new TFile(imageDir).list()) { - if (fileName.toLowerCase().equals(imageName.toLowerCase())) { - imageName = fileName; - break; + String cardName = card.getFileName(); + if (cardName.isEmpty()){ + cardName = prepareCardNameForFile(card.getName()); + } + + String finalFileName = ""; + if (card.getUsesVariousArt()){ + finalFileName = cardName + '.' + card.getCollectorId() + ".full.jpg"; + }else{ + finalFileName = cardName + prefixType + ".full.jpg"; + } + + // if image file exists, correct name (for case sensitive systems) + // use TFile for zips + TFile dirFile = new TFile(setPath); + TFile imageFile = new TFile(setPath + finalFileName); + // warning, zip files can be broken + try{ + if (dirFile.exists() && !imageFile.exists()) { + // search like names + for (String fileName: dirFile.list()) { + if (fileName.toLowerCase().equals(finalFileName.toLowerCase())) { + finalFileName = fileName; + break; + } } } + }catch (Exception ex) { + log.error("Can't read card name from file, may be it broken: " + setPath); } - return imageDir + TFile.separator + imageName; + return setPath + finalFileName; } public static String generateFaceImagePath(String cardname, String set) { - String useDefault = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_USE_DEFAULT, "true"); - String imagesPath = Objects.equals(useDefault, "true") ? Constants.IO.imageBaseDir : PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_PATH, null); - String imageDir = imagesPath; - String imageName = set + TFile.separator + cardname + ".jpg"; - return imageDir + TFile.separator + "FACE" + TFile.separator + imageName; + return getImagesDir() + File.separator + "FACE" + File.separator + set + File.separator + prepareCardNameForFile(cardname) + File.separator + ".jpg"; } public static String generateTokenDescriptorImagePath(CardDownloadData card) { - // String useDefault = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_USE_DEFAULT, "true"); - // String imagesPath = Objects.equals(useDefault, "true") ? null : PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_PATH, null); - String straightImageFile = getTokenDescriptorImagePath(card); + String straightImageFile = buildImagePathToTokenDescriptor(card); TFile file = new TFile(straightImageFile); if (file.exists()) { return straightImageFile; diff --git a/Mage.Common/src/main/java/mage/interfaces/plugin/CardPlugin.java b/Mage.Common/src/main/java/mage/interfaces/plugin/CardPlugin.java index b1c43b508a..ac3c739f07 100644 --- a/Mage.Common/src/main/java/mage/interfaces/plugin/CardPlugin.java +++ b/Mage.Common/src/main/java/mage/interfaces/plugin/CardPlugin.java @@ -32,10 +32,9 @@ public interface CardPlugin extends Plugin { /** * Download various symbols (mana, tap, set). * - * @param imagesPath Path to check in and store symbols to. Can be null, in - * such case default path should be used. + * @param imagesDir Path to check in and store symbols to. Can't be null. */ - void downloadSymbols(String imagesPath); + void downloadSymbols(String imagesDir); void onAddCard(MagePermanent card, int count); diff --git a/Mage.Common/src/main/java/mage/utils/MageVersion.java b/Mage.Common/src/main/java/mage/utils/MageVersion.java index ac35216c02..4780e89977 100644 --- a/Mage.Common/src/main/java/mage/utils/MageVersion.java +++ b/Mage.Common/src/main/java/mage/utils/MageVersion.java @@ -41,7 +41,7 @@ public class MageVersion implements Serializable, Comparable { public final static int MAGE_VERSION_MAJOR = 1; public final static int MAGE_VERSION_MINOR = 4; public final static int MAGE_VERSION_PATCH = 26; - public final static String MAGE_VERSION_MINOR_PATCH = "V8"; + public final static String MAGE_VERSION_MINOR_PATCH = "V9b"; public final static String MAGE_VERSION_INFO = ""; private final int major; diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/DuelCommander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/DuelCommander.java index cbbe513112..22387f9a43 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/DuelCommander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/DuelCommander.java @@ -44,6 +44,7 @@ public class DuelCommander extends Commander { banned.add("Eidolon of the Great Revel"); banned.add("Emrakul, the Aeons Torn"); banned.add("Entomb"); + banned.add("Fastbond"); banned.add("Fireblast"); banned.add("Food Chain"); banned.add("Gaea's Cradle"); diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index b8515e19bf..79018eb4d6 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -2261,6 +2261,8 @@ public class ComputerPlayer extends PlayerImpl implements Player { break; } } + } else if (opponents.size() == 1) { + randomOpponentId = game.getOpponents(abilityControllerId).iterator().next(); } return randomOpponentId; } diff --git a/Mage.Sets/src/mage/cards/b/Banshee.java b/Mage.Sets/src/mage/cards/b/Banshee.java new file mode 100644 index 0000000000..1e363ba069 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/Banshee.java @@ -0,0 +1,76 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.b; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.dynamicvalue.common.HalfValue; +import mage.abilities.dynamicvalue.common.ManacostVariableValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DamageControllerEffect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.target.common.TargetCreatureOrPlayer; + +/** + * + * @author L_J + */ +public class Banshee extends CardImpl { + + public Banshee(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{B}{B}"); + this.subtype.add(SubType.SPIRIT); + this.power = new MageInt(0); + this.toughness = new MageInt(1); + + // {X}, {T}: Banshee deals half X damage, rounded down, to target creature or player, and half X damage, rounded up, to you. + Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DamageTargetEffect(new HalfValue(new ManacostVariableValue(), false)).setText("Banshee deals half X damage, rounded down, to target creature or player,"), new ManaCostsImpl("{X}")); + ability.addCost(new TapSourceCost()); + ability.addEffect(new DamageControllerEffect(new HalfValue(new ManacostVariableValue(), true)).setText(" and half X damage, rounded up, to you")); + ability.addTarget(new TargetCreatureOrPlayer()); + this.addAbility(ability); + } + + public Banshee(final Banshee card) { + super(card); + } + + @Override + public Banshee copy() { + return new Banshee(this); + } +} diff --git a/Mage.Sets/src/mage/cards/b/BattleHymn.java b/Mage.Sets/src/mage/cards/b/BattleHymn.java index 0cf3de4242..4d730d9409 100644 --- a/Mage.Sets/src/mage/cards/b/BattleHymn.java +++ b/Mage.Sets/src/mage/cards/b/BattleHymn.java @@ -34,7 +34,7 @@ import mage.abilities.effects.common.DynamicManaEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.StaticFilters; /** * @@ -43,11 +43,10 @@ import mage.filter.common.FilterControlledCreaturePermanent; public class BattleHymn extends CardImpl { public BattleHymn(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{1}{R}"); - + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{R}"); // Add {R} to your mana pool for each creature you control. - this.getSpellAbility().addEffect(new DynamicManaEffect(Mana.RedMana(1), new PermanentsOnBattlefieldCount(new FilterControlledCreaturePermanent()))); + this.getSpellAbility().addEffect(new DynamicManaEffect(Mana.RedMana(1), new PermanentsOnBattlefieldCount(StaticFilters.FILTER_CONTROLLED_CREATURE))); } public BattleHymn(final BattleHymn card) { diff --git a/Mage.Sets/src/mage/cards/b/BloodOfTheMartyr.java b/Mage.Sets/src/mage/cards/b/BloodOfTheMartyr.java new file mode 100644 index 0000000000..cf24c13827 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BloodOfTheMartyr.java @@ -0,0 +1,110 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.b; + +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.events.DamageEvent; +import mage.game.events.GameEvent; +import mage.players.Player; + +/** + * + * @author L_J + */ +public class BloodOfTheMartyr extends CardImpl { + + public BloodOfTheMartyr(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{W}{W}{W}"); + + // Until end of turn, if damage would be dealt to any creature, you may have that damage dealt to you instead. + this.getSpellAbility().addEffect(new BloodOfTheMartyrEffect()); + } + + public BloodOfTheMartyr(final BloodOfTheMartyr card) { + super(card); + } + + @Override + public BloodOfTheMartyr copy() { + return new BloodOfTheMartyr(this); + } +} + +class BloodOfTheMartyrEffect extends ReplacementEffectImpl { + + public BloodOfTheMartyrEffect() { + super(Duration.EndOfTurn, Outcome.RedirectDamage); + staticText = "Until end of turn, if damage would be dealt to any creature, you may have that damage dealt to you instead"; + } + + public BloodOfTheMartyrEffect(final BloodOfTheMartyrEffect effect) { + super(effect); + } + + @Override + public BloodOfTheMartyrEffect copy() { + return new BloodOfTheMartyrEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGE_CREATURE; + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + DamageEvent damageEvent = (DamageEvent) event; + if (controller != null) { + controller.damage(damageEvent.getAmount(), damageEvent.getSourceId(), game, damageEvent.isCombatDamage(), damageEvent.isPreventable(), damageEvent.getAppliedEffects()); + return true; + } + return false; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + DamageEvent damageEvent = (DamageEvent) event; + return controller != null + && controller.chooseUse(outcome, "Would you like to have " + damageEvent.getAmount() + " damage dealt to you instead of " + game.getPermanentOrLKIBattlefield(damageEvent.getTargetId()).getLogName() + "?", source, game); + } +} diff --git a/Mage.Sets/src/mage/cards/e/EternalFlame.java b/Mage.Sets/src/mage/cards/e/EternalFlame.java new file mode 100644 index 0000000000..9c28d75f57 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EternalFlame.java @@ -0,0 +1,72 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.e; + +import java.util.UUID; +import mage.abilities.dynamicvalue.common.HalfValue; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.DamageControllerEffect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.mageobject.SubtypePredicate; +import mage.target.common.TargetOpponent; + +/** + * + * @author L_J + */ +public class EternalFlame extends CardImpl { + + private static final FilterControlledPermanent filter = new FilterControlledPermanent("Mountains you control"); + + static { + filter.add(new SubtypePredicate(SubType.MOUNTAIN)); + } + + public EternalFlame(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{2}{R}{R}"); + + // Eternal Flame deals X damage to target opponent, where X is the number of Mountains you control. It deals half X damage, rounded up, to you.); + this.getSpellAbility().addEffect(new DamageTargetEffect(new PermanentsOnBattlefieldCount(filter)).setText("{this} deals X damage to target opponent, where X is the number of Mountains you control")); + this.getSpellAbility().addEffect(new DamageControllerEffect(new HalfValue(new PermanentsOnBattlefieldCount(filter), true)).setText("It deals half X damage, rounded up, to you")); + this.getSpellAbility().addTarget(new TargetOpponent()); + } + + public EternalFlame(final EternalFlame card) { + super(card); + } + + @Override + public EternalFlame copy() { + return new EternalFlame(this); + } +} diff --git a/Mage.Sets/src/mage/cards/g/GhaltaPrimalHunger.java b/Mage.Sets/src/mage/cards/g/GhaltaPrimalHunger.java new file mode 100644 index 0000000000..4475fd1c4e --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GhaltaPrimalHunger.java @@ -0,0 +1,125 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.g; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.cost.CostModificationEffectImpl; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.CostModificationType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledArtifactPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.CardTypePredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.util.CardUtil; + +/** + * + * @author LevelX2 + */ +public class GhaltaPrimalHunger extends CardImpl { + + public GhaltaPrimalHunger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{10}{G}{G}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.ELDER); + this.subtype.add(SubType.DINOSAUR); + this.power = new MageInt(12); + this.toughness = new MageInt(12); + + // Ghalta, Primal Hunger costs {X} less to cast, where X is the total power of creatures you control. + this.addAbility(new SimpleStaticAbility(Zone.STACK, new GhaltaPrimalHungerCostReductionEffect())); + // Trample + this.addAbility(TrampleAbility.getInstance()); + + } + + public GhaltaPrimalHunger(final GhaltaPrimalHunger card) { + super(card); + } + + @Override + public GhaltaPrimalHunger copy() { + return new GhaltaPrimalHunger(this); + } +} + +class GhaltaPrimalHungerCostReductionEffect extends CostModificationEffectImpl { + + private static final FilterPermanent filter = new FilterControlledArtifactPermanent("noncreature artifacts you control"); + + static { + filter.add(Predicates.not(new CardTypePredicate(CardType.CREATURE))); + } + + GhaltaPrimalHungerCostReductionEffect() { + super(Duration.Custom, Outcome.Benefit, CostModificationType.REDUCE_COST); + staticText = "{this} costs {X} less to cast, where X is the total power of creatures you control"; + } + + GhaltaPrimalHungerCostReductionEffect(final GhaltaPrimalHungerCostReductionEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source, Ability abilityToModify) { + int totalPower = 0; + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(source.getControllerId())) { + if (permanent.isCreature()) { + totalPower += permanent.getPower().getValue(); + } + + } + CardUtil.reduceCost(abilityToModify, totalPower); + return true; + } + + @Override + public boolean applies(Ability abilityToModify, Ability source, Game game) { + return abilityToModify.getSourceId().equals(source.getSourceId()) && (abilityToModify instanceof SpellAbility); + } + + @Override + public GhaltaPrimalHungerCostReductionEffect copy() { + return new GhaltaPrimalHungerCostReductionEffect(this); + } +} diff --git a/Mage.Sets/src/mage/cards/g/GoblinRockSled.java b/Mage.Sets/src/mage/cards/g/GoblinRockSled.java new file mode 100644 index 0000000000..8c2ad210f8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GoblinRockSled.java @@ -0,0 +1,134 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.g; + +import java.util.Set; +import java.util.UUID; +import mage.MageObjectReference; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.abilities.effects.common.combat.CantAttackUnlessDefenderControllsPermanent; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.common.FilterLandPermanent; +import mage.filter.predicate.mageobject.SubtypePredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.GameEvent.EventType; +import mage.game.permanent.Permanent; +import mage.watchers.common.AttackedLastTurnWatcher; + +/** + * + * @author L_J + */ +public class GoblinRockSled extends CardImpl { + + private static final FilterLandPermanent filter = new FilterLandPermanent("a Mountain"); + + static { + filter.add(new SubtypePredicate(SubType.MOUNTAIN)); + } + + public GoblinRockSled(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{1}{R}"); + this.subtype.add(SubType.GOBLIN); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + + // Goblin Rock Sled doesn't untap during your untap step if it attacked during your last turn. + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new DontUntapIfAttackedLastTurnSourceEffect()), new AttackedLastTurnWatcher()); + + // Goblin Rock Sled can't attack unless defending player controls a Mountain. + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new CantAttackUnlessDefenderControllsPermanent(filter))); + } + + public GoblinRockSled(final GoblinRockSled card) { + super(card); + } + + @Override + public GoblinRockSled copy() { + return new GoblinRockSled(this); + } +} + +class DontUntapIfAttackedLastTurnSourceEffect extends ContinuousRuleModifyingEffectImpl { + + public DontUntapIfAttackedLastTurnSourceEffect() { + super(Duration.WhileOnBattlefield, Outcome.Detriment, false, true); + staticText = "{this} doesn't untap during your untap step if it attacked during your last turn"; + } + + public DontUntapIfAttackedLastTurnSourceEffect(final DontUntapIfAttackedLastTurnSourceEffect effect) { + super(effect); + } + + @Override + public DontUntapIfAttackedLastTurnSourceEffect copy() { + return new DontUntapIfAttackedLastTurnSourceEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == EventType.UNTAP; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (game.getTurn().getStepType() == PhaseStep.UNTAP + && event.getTargetId().equals(source.getSourceId())) { + Permanent permanent = game.getPermanent(source.getSourceId()); + if (permanent != null && permanent.getControllerId().equals(game.getActivePlayerId())) { + AttackedLastTurnWatcher watcher = (AttackedLastTurnWatcher) game.getState().getWatchers().get(AttackedLastTurnWatcher.class.getSimpleName()); + if (watcher != null) { + Set attackingCreatures = watcher.getAttackedLastTurnCreatures(permanent.getControllerId()); + MageObjectReference mor = new MageObjectReference(permanent, game); + if (attackingCreatures.contains(mor)) { + return true; + } + } + } + } + return false; + } + +} diff --git a/Mage.Sets/src/mage/cards/g/GrowingRitesOfItlimoc.java b/Mage.Sets/src/mage/cards/g/GrowingRitesOfItlimoc.java index bd78168fff..0e3a1cb05f 100644 --- a/Mage.Sets/src/mage/cards/g/GrowingRitesOfItlimoc.java +++ b/Mage.Sets/src/mage/cards/g/GrowingRitesOfItlimoc.java @@ -45,7 +45,7 @@ import mage.constants.SuperType; import mage.constants.TargetController; import mage.constants.Zone; import mage.filter.FilterCard; -import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.StaticFilters; import mage.filter.predicate.mageobject.CardTypePredicate; /** @@ -62,7 +62,7 @@ public class GrowingRitesOfItlimoc extends CardImpl { public GrowingRitesOfItlimoc(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); - + this.addSuperType(SuperType.LEGENDARY); this.transformable = true; @@ -77,7 +77,7 @@ public class GrowingRitesOfItlimoc extends CardImpl { this.addAbility(new TransformAbility()); this.addAbility(new ConditionalTriggeredAbility( new BeginningOfEndStepTriggeredAbility(new TransformSourceEffect(true), TargetController.YOU, false), - new PermanentsOnTheBattlefieldCondition(new FilterControlledCreaturePermanent(), ComparisonType.MORE_THAN, 3), + new PermanentsOnTheBattlefieldCondition(StaticFilters.FILTER_CONTROLLED_A_CREATURE, ComparisonType.MORE_THAN, 3), "At the beginning of your end step, if you control four or more creatures, transform {this}")); } diff --git a/Mage.Sets/src/mage/cards/i/InvasionPlans.java b/Mage.Sets/src/mage/cards/i/InvasionPlans.java index 55d41fd13d..dda406b036 100644 --- a/Mage.Sets/src/mage/cards/i/InvasionPlans.java +++ b/Mage.Sets/src/mage/cards/i/InvasionPlans.java @@ -95,8 +95,11 @@ class InvasionPlansEffect extends ContinuousRuleModifyingEffectImpl { public boolean applies(GameEvent event, Ability source, Game game) { Player blockController = game.getPlayer(game.getCombat().getAttackingPlayerId()); if (blockController != null) { - game.getCombat().selectBlockers(blockController, game); - return event.getPlayerId().equals(game.getCombat().getAttackingPlayerId()); + // temporary workaround for AI bugging out while choosing blockers + if (blockController.isHuman()) { + game.getCombat().selectBlockers(blockController, game); + return event.getPlayerId().equals(game.getCombat().getAttackingPlayerId()); + } } return false; } diff --git a/Mage.Sets/src/mage/cards/i/IzzetChemister.java b/Mage.Sets/src/mage/cards/i/IzzetChemister.java index ec022d586e..2663fc9d5a 100644 --- a/Mage.Sets/src/mage/cards/i/IzzetChemister.java +++ b/Mage.Sets/src/mage/cards/i/IzzetChemister.java @@ -48,6 +48,8 @@ import mage.constants.Outcome; import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.common.FilterOwnedCard; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.CardTypePredicate; import mage.game.ExileZone; import mage.game.Game; import mage.players.Player; @@ -62,6 +64,13 @@ public class IzzetChemister extends CardImpl { private static final FilterCard filter = new FilterOwnedCard("instant or sorcery card from your graveyard"); + static { + filter.add(Predicates.or( + new CardTypePredicate(CardType.INSTANT), + new CardTypePredicate(CardType.SORCERY)) + ); + } + public IzzetChemister(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); diff --git a/Mage.Sets/src/mage/cards/m/MairsilThePretender.java b/Mage.Sets/src/mage/cards/m/MairsilThePretender.java index 1265793027..5d59ec7163 100644 --- a/Mage.Sets/src/mage/cards/m/MairsilThePretender.java +++ b/Mage.Sets/src/mage/cards/m/MairsilThePretender.java @@ -46,14 +46,12 @@ import mage.constants.Layer; import mage.constants.Outcome; import mage.constants.SubLayer; import mage.constants.SuperType; -import mage.constants.TargetController; import mage.constants.Zone; import mage.counters.CounterType; import mage.filter.FilterCard; import mage.filter.predicate.Predicates; import mage.filter.predicate.mageobject.CardTypePredicate; import mage.filter.predicate.other.CounterCardPredicate; -import mage.filter.predicate.other.OwnerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; @@ -149,7 +147,6 @@ class MairsilThePretenderGainAbilitiesEffect extends ContinuousEffectImpl { static { filter.add(new CounterCardPredicate(CounterType.CAGE)); - filter.add(new OwnerPredicate(TargetController.YOU)); } public MairsilThePretenderGainAbilitiesEffect() { @@ -164,21 +161,21 @@ class MairsilThePretenderGainAbilitiesEffect extends ContinuousEffectImpl { @Override public boolean apply(Game game, Ability source) { Permanent perm = game.getPermanent(source.getSourceId()); - if (perm != null) { - for (Card card : game.getExile().getAllCards(game)) { - if (filter.match(card, game)) { - for (Ability ability : card.getAbilities()) { - if (ability instanceof ActivatedAbility) { - ActivatedAbilityImpl copyAbility = (ActivatedAbilityImpl) ability.copy(); - copyAbility.setMaxActivationsPerTurn(1); - perm.addAbility(copyAbility, card.getId(), game); - } + if (perm == null) { + return false; + } + for (Card card : game.getExile().getAllCards(game)) { + if (filter.match(card, game) && card.getOwnerId() == perm.getControllerId()) { + for (Ability ability : card.getAbilities()) { + if (ability instanceof ActivatedAbility) { + ActivatedAbilityImpl copyAbility = (ActivatedAbilityImpl) ability.copy(); + copyAbility.setMaxActivationsPerTurn(1); + perm.addAbility(copyAbility, card.getId(), game); } } } - return true; } - return false; + return true; } @Override diff --git a/Mage.Sets/src/mage/cards/n/NaturesWill.java b/Mage.Sets/src/mage/cards/n/NaturesWill.java index 48c70b9c44..215999aa9b 100644 --- a/Mage.Sets/src/mage/cards/n/NaturesWill.java +++ b/Mage.Sets/src/mage/cards/n/NaturesWill.java @@ -32,19 +32,14 @@ import java.util.List; import java.util.Set; import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.Effect; +import mage.abilities.common.ControlledCreaturesDealCombatDamagePlayerTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; -import mage.constants.Zone; import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.events.DamagedPlayerEvent; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.game.permanent.Permanent; /** @@ -54,11 +49,10 @@ import mage.game.permanent.Permanent; public class NaturesWill extends CardImpl { public NaturesWill(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{2}{G}{G}"); - + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}{G}"); // Whenever one or more creatures you control deal combat damage to a player, tap all lands that player controls and untap all lands you control. - this.addAbility(new NaturesWillTriggeredAbility()); + this.addAbility(new ControlledCreaturesDealCombatDamagePlayerTriggeredAbility(new NaturesWillEffect())); } public NaturesWill(final NaturesWill card) { @@ -71,63 +65,6 @@ public class NaturesWill extends CardImpl { } } -class NaturesWillTriggeredAbility extends TriggeredAbilityImpl { - - private boolean madeDamge = false; - private Set damagedPlayers = new HashSet<>(); - - public NaturesWillTriggeredAbility() { - super(Zone.BATTLEFIELD, new NaturesWillEffect(), false); - } - - public NaturesWillTriggeredAbility(final NaturesWillTriggeredAbility ability) { - super(ability); - this.madeDamge = ability.madeDamge; - this.damagedPlayers = new HashSet<>(); - this.damagedPlayers.addAll(ability.damagedPlayers); - } - - @Override - public NaturesWillTriggeredAbility copy() { - return new NaturesWillTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == EventType.DAMAGED_PLAYER || event.getType() == EventType.COMBAT_DAMAGE_STEP_POST; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getType() == EventType.DAMAGED_PLAYER) { - DamagedPlayerEvent damageEvent = (DamagedPlayerEvent) event; - Permanent p = game.getPermanent(event.getSourceId()); - if (damageEvent.isCombatDamage() && p != null && p.getControllerId().equals(this.getControllerId())) { - madeDamge = true; - damagedPlayers.add(event.getPlayerId()); - } - } - if (event.getType() == EventType.COMBAT_DAMAGE_STEP_POST) { - if (madeDamge) { - Set damagedPlayersCopy = new HashSet<>(); - damagedPlayersCopy.addAll(damagedPlayers); - for(Effect effect: this.getEffects()) { - effect.setValue("damagedPlayers", damagedPlayersCopy); - } - damagedPlayers.clear(); - madeDamge = false; - return true; - } - } - return false; - } - - @Override - public String getRule() { - return "Whenever one or more creatures you control deal combat damage to a player, " + super.getRule(); - } -} - class NaturesWillEffect extends OneShotEffect { public NaturesWillEffect() { @@ -160,7 +97,6 @@ class NaturesWillEffect extends OneShotEffect { } } - return false; } } diff --git a/Mage.Sets/src/mage/cards/o/OngoingInvestigation.java b/Mage.Sets/src/mage/cards/o/OngoingInvestigation.java index 1dd5bef22f..fcf6569d42 100644 --- a/Mage.Sets/src/mage/cards/o/OngoingInvestigation.java +++ b/Mage.Sets/src/mage/cards/o/OngoingInvestigation.java @@ -27,11 +27,9 @@ */ package mage.cards.o; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.ControlledCreaturesDealCombatDamagePlayerTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.ExileFromGraveCost; import mage.abilities.costs.mana.ManaCostsImpl; @@ -42,10 +40,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Zone; import mage.filter.common.FilterCreatureCard; -import mage.game.Game; -import mage.game.events.DamagedPlayerEvent; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; import mage.target.common.TargetCardInYourGraveyard; /** @@ -58,7 +52,7 @@ public class OngoingInvestigation extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}"); // Whenever one or more creatures you control deal combat damage to a player, investigate. - this.addAbility(new OngoingInvestigationTriggeredAbility()); + this.addAbility(new ControlledCreaturesDealCombatDamagePlayerTriggeredAbility(new InvestigateEffect())); // {1}{G}, Exile a creature card from your graveyard: Investigate. You gain 2 life. Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new InvestigateEffect(), new ManaCostsImpl("{1}{G}")); @@ -76,50 +70,3 @@ public class OngoingInvestigation extends CardImpl { return new OngoingInvestigation(this); } } - -class OngoingInvestigationTriggeredAbility extends TriggeredAbilityImpl { - - List damagedPlayerIds = new ArrayList<>(); - - public OngoingInvestigationTriggeredAbility() { - super(Zone.BATTLEFIELD, new InvestigateEffect(), false); - } - - public OngoingInvestigationTriggeredAbility(final OngoingInvestigationTriggeredAbility ability) { - super(ability); - } - - @Override - public OngoingInvestigationTriggeredAbility copy() { - return new OngoingInvestigationTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_PLAYER - || event.getType() == GameEvent.EventType.END_COMBAT_STEP_POST; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.DAMAGED_PLAYER) { - if (((DamagedPlayerEvent) event).isCombatDamage()) { - Permanent creature = game.getPermanent(event.getSourceId()); - if (creature != null && creature.getControllerId().equals(controllerId) - && !damagedPlayerIds.contains(event.getTargetId())) { - damagedPlayerIds.add(event.getTargetId()); - return true; - } - } - } - if (event.getType() == GameEvent.EventType.END_COMBAT_STEP_POST) { - damagedPlayerIds.clear(); - } - return false; - } - - @Override - public String getRule() { - return "Whenever one or more creatures you control deal combat damage to a player, investigate"; - } -} diff --git a/Mage.Sets/src/mage/cards/p/PyrewildShaman.java b/Mage.Sets/src/mage/cards/p/PyrewildShaman.java index d8e6d15213..72fa861271 100644 --- a/Mage.Sets/src/mage/cards/p/PyrewildShaman.java +++ b/Mage.Sets/src/mage/cards/p/PyrewildShaman.java @@ -25,14 +25,11 @@ * authors and should not be interpreted as representing official policies, either expressed * or implied, of BetaSteward_at_googlemail.com. */ - package mage.cards.p; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; import mage.MageInt; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.ControlledCreaturesDealCombatDamagePlayerTriggeredAbility; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.ReturnToHandSourceEffect; @@ -41,22 +38,14 @@ import mage.abilities.keyword.BloodrushAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; +import mage.constants.SubType; import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.DamagedEvent; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; -import mage.game.events.ZoneChangeEvent; -import mage.game.permanent.Permanent; - - public class PyrewildShaman extends CardImpl { - public PyrewildShaman (UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{R}"); + public PyrewildShaman(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); this.subtype.add(SubType.GOBLIN); this.subtype.add(SubType.SHAMAN); @@ -64,14 +53,16 @@ public class PyrewildShaman extends CardImpl { this.toughness = new MageInt(1); // Bloodrush — {1}{R}, Discard Pyrewild Shaman: Target attacking creature gets +3/+1 until end of turn. - this.addAbility(new BloodrushAbility("{1}{R}", new BoostTargetEffect(3,1,Duration.EndOfTurn))); + this.addAbility(new BloodrushAbility("{1}{R}", new BoostTargetEffect(3, 1, Duration.EndOfTurn))); // Whenever one or more creatures you control deal combat damage to a player, if Pyrewild Shaman is in your graveyard, you may pay {3}. If you do, return Pyrewild Shaman to your hand. - this.addAbility(new PyrewildShamanTriggeredAbility()); + this.addAbility(new ControlledCreaturesDealCombatDamagePlayerTriggeredAbility(Zone.GRAVEYARD, + new DoIfCostPaid(new ReturnToHandSourceEffect(), new ManaCostsImpl("{3}")) + .setText("if {this} is in your graveyard, you may pay {3}. If you do, return {this} to your hand"))); } - public PyrewildShaman (final PyrewildShaman card) { + public PyrewildShaman(final PyrewildShaman card) { super(card); } @@ -81,56 +72,3 @@ public class PyrewildShaman extends CardImpl { } } - -class PyrewildShamanTriggeredAbility extends TriggeredAbilityImpl { - - List damagedPlayerIds = new ArrayList<>(); - - public PyrewildShamanTriggeredAbility() { - super(Zone.GRAVEYARD, new DoIfCostPaid(new ReturnToHandSourceEffect(), new ManaCostsImpl("{3}")), false); - } - - public PyrewildShamanTriggeredAbility(final PyrewildShamanTriggeredAbility ability) { - super(ability); - } - - @Override - public PyrewildShamanTriggeredAbility copy() { - return new PyrewildShamanTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == EventType.DAMAGED_PLAYER - || event.getType() == EventType.END_COMBAT_STEP_POST - || event.getType() == EventType.ZONE_CHANGE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.DAMAGED_PLAYER) { - if (((DamagedEvent) event).isCombatDamage()) { - Permanent creature = game.getPermanent(event.getSourceId()); - if (creature != null && creature.getControllerId().equals(controllerId) && !damagedPlayerIds.contains(event.getTargetId())) { - damagedPlayerIds.add(event.getTargetId()); - return true; - } - } - } - if (event.getType() == EventType.END_COMBAT_STEP_POST){ - damagedPlayerIds.clear(); - } - if (event.getType() == EventType.ZONE_CHANGE && event.getTargetId().equals(getSourceId())){ - ZoneChangeEvent zEvent = (ZoneChangeEvent) event; - if (zEvent.getFromZone() == Zone.GRAVEYARD) { - damagedPlayerIds.clear(); - } - } - return false; - } - - @Override - public String getRule() { - return "Whenever one or more creatures you control deal combat damage to a player, if {this} is in your graveyard, you may pay {3}. If you do, return {this} to your hand."; - } -} diff --git a/Mage.Sets/src/mage/cards/s/StormTheVault.java b/Mage.Sets/src/mage/cards/s/StormTheVault.java new file mode 100644 index 0000000000..4a1647230f --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StormTheVault.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.s; + +import java.util.UUID; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.ControlledCreaturesDealCombatDamagePlayerTriggeredAbility; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.v.VaultOfCatlacan; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SuperType; +import mage.constants.TargetController; +import mage.filter.StaticFilters; +import mage.game.permanent.token.TreasureToken; + +/** + * + * @author LevelX2 + */ +public class StormTheVault extends CardImpl { + + public StormTheVault(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{U}{R}"); + + this.addSuperType(SuperType.LEGENDARY); + + this.transformable = true; + this.secondSideCardClazz = VaultOfCatlacan.class; + + // Whenever one or more creatures you control deal combat damage to a player, create a colorless Treasure artifact token with "{T}, Sacrifice this artifact: Add one mana of any color to your mana pool." + this.addAbility(new ControlledCreaturesDealCombatDamagePlayerTriggeredAbility(new CreateTokenEffect(new TreasureToken()))); + + // At the beginning of your end step, if you control five or more artifacts, transform Storm the Vault. + this.addAbility(new TransformAbility()); + this.addAbility(new ConditionalTriggeredAbility( + new BeginningOfEndStepTriggeredAbility(new TransformSourceEffect(true), TargetController.YOU, false), + new PermanentsOnTheBattlefieldCondition(StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT, ComparisonType.MORE_THAN, 4), + "At the beginning of your end step, if you control five or more artifacts, transform {this}")); + + } + + public StormTheVault(final StormTheVault card) { + super(card); + } + + @Override + public StormTheVault copy() { + return new StormTheVault(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TangleKelp.java b/Mage.Sets/src/mage/cards/t/TangleKelp.java new file mode 100644 index 0000000000..88695b4069 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TangleKelp.java @@ -0,0 +1,150 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.t; + +import java.util.Set; +import java.util.UUID; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.TapEnchantedEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.GameEvent.EventType; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; +import mage.watchers.common.AttackedLastTurnWatcher; + +/** + * + * @author L_J + */ +public class TangleKelp extends CardImpl { + + public TangleKelp(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{U}"); + + this.subtype.add(SubType.AURA); + + // Enchant creature + TargetPermanent auraTarget = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.Detriment)); + Ability ability = new EnchantAbility(auraTarget.getTargetName()); + this.addAbility(ability); + + // When Tangle Kelp enters the battlefield, tap enchanted creature. + this.addAbility(new EntersBattlefieldTriggeredAbility(new TapEnchantedEffect())); + + // Enchanted creature doesn't untap during its controller's untap step if it attacked during its controller's last turn. + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new DontUntapIfAttackedLastTurnEnchantedEffect()), new AttackedLastTurnWatcher()); + } + + public TangleKelp(final TangleKelp card) { + super(card); + } + + @Override + public TangleKelp copy() { + return new TangleKelp(this); + } +} + +class DontUntapIfAttackedLastTurnEnchantedEffect extends ContinuousRuleModifyingEffectImpl { + + public DontUntapIfAttackedLastTurnEnchantedEffect() { + super(Duration.WhileOnBattlefield, Outcome.Detriment, false, true); + staticText = "Enchanted creature doesn't untap during its controller's untap step if it attacked during its controller's last turn"; + } + + public DontUntapIfAttackedLastTurnEnchantedEffect(final DontUntapIfAttackedLastTurnEnchantedEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public DontUntapIfAttackedLastTurnEnchantedEffect copy() { + return new DontUntapIfAttackedLastTurnEnchantedEffect(this); + } + + @Override + public String getInfoMessage(Ability source, GameEvent event, Game game) { + Permanent enchantment = game.getPermanent(source.getSourceId()); + if (enchantment != null && enchantment.getAttachedTo() != null) { + Permanent enchanted = game.getPermanent(enchantment.getAttachedTo()); + if (enchanted != null) { + return enchanted.getLogName() + " doesn't untap during its controller's untap step (" + enchantment.getLogName() + ')'; + } + } + return null; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.UNTAP; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (game.getTurn().getStepType() == PhaseStep.UNTAP) { + Permanent enchantment = game.getPermanent(source.getSourceId()); + if (enchantment != null && enchantment.getAttachedTo() != null && event.getTargetId().equals(enchantment.getAttachedTo())) { + Permanent permanent = game.getPermanent(enchantment.getAttachedTo()); + if (permanent != null && permanent.getControllerId().equals(game.getActivePlayerId())) { + AttackedLastTurnWatcher watcher = (AttackedLastTurnWatcher) game.getState().getWatchers().get(AttackedLastTurnWatcher.class.getSimpleName()); + if (watcher != null) { + Set attackingCreatures = watcher.getAttackedLastTurnCreatures(permanent.getControllerId()); + MageObjectReference mor = new MageObjectReference(permanent, game); + if (attackingCreatures.contains(mor)) { + return true; + } + } + } + } + } + return false; + } + +} diff --git a/Mage.Sets/src/mage/cards/v/VaultOfCatlacan.java b/Mage.Sets/src/mage/cards/v/VaultOfCatlacan.java new file mode 100644 index 0000000000..372ba6e1da --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VaultOfCatlacan.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.v; + +import java.util.UUID; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.DynamicManaEffect; +import mage.abilities.effects.common.InfoEffect; +import mage.abilities.mana.AnyColorManaAbility; +import mage.abilities.mana.SimpleManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.filter.StaticFilters; + +/** + * + * @author LevelX2 + */ +public class VaultOfCatlacan extends CardImpl { + + public VaultOfCatlacan(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); + + this.addSuperType(SuperType.LEGENDARY); + this.nightCard = true; + + // (Transforms from Storm the Vault.) + Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new InfoEffect("(Transforms from Storm the Vault.)")); + ability.setRuleAtTheTop(true); + this.addAbility(ability); + + // {T}: Add one mana of any color to your mana pool. + this.addAbility(new AnyColorManaAbility()); + + // {T}: Add {U} to your mana pool for each artifact you control. + this.addAbility(new SimpleManaAbility(Zone.BATTLEFIELD, + new DynamicManaEffect(Mana.BlueMana(1), new PermanentsOnBattlefieldCount(StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT)), + new TapSourceCost())); + + } + + public VaultOfCatlacan(final VaultOfCatlacan card) { + super(card); + } + + @Override + public VaultOfCatlacan copy() { + return new VaultOfCatlacan(this); + } +} diff --git a/Mage.Sets/src/mage/sets/Chronicles.java b/Mage.Sets/src/mage/sets/Chronicles.java index 3bc1ca3905..688c6f068e 100644 --- a/Mage.Sets/src/mage/sets/Chronicles.java +++ b/Mage.Sets/src/mage/sets/Chronicles.java @@ -65,9 +65,11 @@ public class Chronicles extends ExpansionSet { cards.add(new SetCardInfo("Ashnod's Transmogrant", 73, Rarity.COMMON, mage.cards.a.AshnodsTransmogrant.class)); cards.add(new SetCardInfo("Axelrod Gunnarson", 107, Rarity.RARE, mage.cards.a.AxelrodGunnarson.class)); cards.add(new SetCardInfo("Azure Drake", 15, Rarity.UNCOMMON, mage.cards.a.AzureDrake.class)); + cards.add(new SetCardInfo("Banshee", 1, Rarity.UNCOMMON, mage.cards.b.Banshee.class)); cards.add(new SetCardInfo("Barl's Cage", 74, Rarity.RARE, mage.cards.b.BarlsCage.class)); cards.add(new SetCardInfo("Beasts of Bogardan", 45, Rarity.UNCOMMON, mage.cards.b.BeastsOfBogardan.class)); cards.add(new SetCardInfo("Blood Moon", 46, Rarity.RARE, mage.cards.b.BloodMoon.class)); + cards.add(new SetCardInfo("Blood of the Martyr", 60, Rarity.UNCOMMON, mage.cards.b.BloodOfTheMartyr.class)); cards.add(new SetCardInfo("Bog Rats", 2, Rarity.COMMON, mage.cards.b.BogRats.class)); cards.add(new SetCardInfo("Book of Rass", 75, Rarity.RARE, mage.cards.b.BookOfRass.class)); cards.add(new SetCardInfo("Boomerang", 16, Rarity.COMMON, mage.cards.b.Boomerang.class)); diff --git a/Mage.Sets/src/mage/sets/MastersEditionIII.java b/Mage.Sets/src/mage/sets/MastersEditionIII.java index 4289a33168..0b0db68cb3 100644 --- a/Mage.Sets/src/mage/sets/MastersEditionIII.java +++ b/Mage.Sets/src/mage/sets/MastersEditionIII.java @@ -66,6 +66,7 @@ public class MastersEditionIII extends ExpansionSet { cards.add(new SetCardInfo("Ashes to Ashes", 58, Rarity.UNCOMMON, mage.cards.a.AshesToAshes.class)); cards.add(new SetCardInfo("Astrolabe", 189, Rarity.COMMON, mage.cards.a.Astrolabe.class)); cards.add(new SetCardInfo("Axelrod Gunnarson", 143, Rarity.UNCOMMON, mage.cards.a.AxelrodGunnarson.class)); + cards.add(new SetCardInfo("Banshee", 59, Rarity.UNCOMMON, mage.cards.b.Banshee.class)); cards.add(new SetCardInfo("Barktooth Warbeard", 144, Rarity.COMMON, mage.cards.b.BarktoothWarbeard.class)); cards.add(new SetCardInfo("Barl's Cage", 190, Rarity.RARE, mage.cards.b.BarlsCage.class)); cards.add(new SetCardInfo("Bartel Runeaxe", 145, Rarity.UNCOMMON, mage.cards.b.BartelRuneaxe.class)); diff --git a/Mage.Sets/src/mage/sets/RivalsOfIxalan.java b/Mage.Sets/src/mage/sets/RivalsOfIxalan.java index 6e42595562..4d6cce9b6d 100644 --- a/Mage.Sets/src/mage/sets/RivalsOfIxalan.java +++ b/Mage.Sets/src/mage/sets/RivalsOfIxalan.java @@ -28,6 +28,7 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.constants.Rarity; import mage.constants.SetType; /** @@ -53,5 +54,10 @@ public class RivalsOfIxalan extends ExpansionSet { this.numBoosterUncommon = 3; this.numBoosterRare = 1; this.ratioBoosterMythic = 8; + + cards.add(new SetCardInfo("Ghalta, Primal Hunger", 130, Rarity.RARE, mage.cards.g.GhaltaPrimalHunger.class)); + cards.add(new SetCardInfo("Silvergill Adept", 53, Rarity.UNCOMMON, mage.cards.s.SilvergillAdept.class)); + cards.add(new SetCardInfo("Storm the Vault", 173, Rarity.RARE, mage.cards.s.StormTheVault.class)); + cards.add(new SetCardInfo("Vault of Catlacan", 173, Rarity.RARE, mage.cards.v.VaultOfCatlacan.class)); } } diff --git a/Mage.Sets/src/mage/sets/TheDark.java b/Mage.Sets/src/mage/sets/TheDark.java index 4561ec2846..f5a5e27688 100644 --- a/Mage.Sets/src/mage/sets/TheDark.java +++ b/Mage.Sets/src/mage/sets/TheDark.java @@ -57,8 +57,10 @@ public class TheDark extends ExpansionSet { cards.add(new SetCardInfo("Apprentice Wizard", 20, Rarity.RARE, mage.cards.a.ApprenticeWizard.class)); cards.add(new SetCardInfo("Ashes to Ashes", 1, Rarity.COMMON, mage.cards.a.AshesToAshes.class)); cards.add(new SetCardInfo("Ball Lightning", 56, Rarity.RARE, mage.cards.b.BallLightning.class)); + cards.add(new SetCardInfo("Banshee", 2, Rarity.RARE, mage.cards.b.Banshee.class)); cards.add(new SetCardInfo("Barl's Cage", 93, Rarity.RARE, mage.cards.b.BarlsCage.class)); cards.add(new SetCardInfo("Blood Moon", 57, Rarity.RARE, mage.cards.b.BloodMoon.class)); + cards.add(new SetCardInfo("Blood of the Martyr", 75, Rarity.RARE, mage.cards.b.BloodOfTheMartyr.class)); cards.add(new SetCardInfo("Bog Imp", 3, Rarity.COMMON, mage.cards.b.BogImp.class)); cards.add(new SetCardInfo("Bog Rats", 4, Rarity.COMMON, mage.cards.b.BogRats.class)); cards.add(new SetCardInfo("Bone Flute", 94, Rarity.UNCOMMON, mage.cards.b.BoneFlute.class)); @@ -80,6 +82,7 @@ public class TheDark extends ExpansionSet { cards.add(new SetCardInfo("Eater of the Dead", 6, Rarity.UNCOMMON, mage.cards.e.EaterOfTheDead.class)); cards.add(new SetCardInfo("Electric Eel", 24, Rarity.UNCOMMON, mage.cards.e.ElectricEel.class)); cards.add(new SetCardInfo("Elves of Deep Shadow", 39, Rarity.UNCOMMON, mage.cards.e.ElvesOfDeepShadow.class)); + cards.add(new SetCardInfo("Eternal Flame", 60, Rarity.RARE, mage.cards.e.EternalFlame.class)); cards.add(new SetCardInfo("Exorcist", 79, Rarity.RARE, mage.cards.e.Exorcist.class)); cards.add(new SetCardInfo("Fellwar Stone", 99, Rarity.UNCOMMON, mage.cards.f.FellwarStone.class)); cards.add(new SetCardInfo("Festival", 81, Rarity.COMMON, mage.cards.f.Festival.class)); @@ -92,8 +95,10 @@ public class TheDark extends ExpansionSet { cards.add(new SetCardInfo("Goblin Caves", 63, Rarity.COMMON, mage.cards.g.GoblinCaves.class)); cards.add(new SetCardInfo("Goblin Digging Team", 64, Rarity.COMMON, mage.cards.g.GoblinDiggingTeam.class)); cards.add(new SetCardInfo("Goblin Hero", 65, Rarity.COMMON, mage.cards.g.GoblinHero.class)); - cards.add(new SetCardInfo("Goblins of the Flarg", 69, Rarity.COMMON, mage.cards.g.GoblinsOfTheFlarg.class)); + cards.add(new SetCardInfo("Goblin Rock Sled", 66, Rarity.COMMON, mage.cards.g.GoblinRockSled.class)); + cards.add(new SetCardInfo("Goblin Shrine", 67, Rarity.COMMON, mage.cards.g.GoblinShrine.class)); cards.add(new SetCardInfo("Goblin Wizard", 68, Rarity.RARE, mage.cards.g.GoblinWizard.class)); + cards.add(new SetCardInfo("Goblins of the Flarg", 69, Rarity.COMMON, mage.cards.g.GoblinsOfTheFlarg.class)); cards.add(new SetCardInfo("Grave Robbers", 8, Rarity.RARE, mage.cards.g.GraveRobbers.class)); cards.add(new SetCardInfo("Hidden Path", 41, Rarity.RARE, mage.cards.h.HiddenPath.class)); cards.add(new SetCardInfo("Holy Light", 83, Rarity.COMMON, mage.cards.h.HolyLight.class)); @@ -133,6 +138,7 @@ public class TheDark extends ExpansionSet { cards.add(new SetCardInfo("Standing Stones", 107, Rarity.UNCOMMON, mage.cards.s.StandingStones.class)); cards.add(new SetCardInfo("Stone Calendar", 108, Rarity.RARE, mage.cards.s.StoneCalendar.class)); cards.add(new SetCardInfo("Sunken City", 35, Rarity.COMMON, mage.cards.s.SunkenCity.class)); + cards.add(new SetCardInfo("Tangle Kelp", 36, Rarity.RARE, mage.cards.t.TangleKelp.class)); cards.add(new SetCardInfo("Tivadar's Crusade", 91, Rarity.UNCOMMON, mage.cards.t.TivadarsCrusade.class)); cards.add(new SetCardInfo("Tormod's Crypt", 109, Rarity.UNCOMMON, mage.cards.t.TormodsCrypt.class)); cards.add(new SetCardInfo("Tower of Coireall", 110, Rarity.RARE, mage.cards.t.TowerOfCoireall.class)); diff --git a/Mage/src/main/java/mage/abilities/common/ControlledCreaturesDealCombatDamagePlayerTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/ControlledCreaturesDealCombatDamagePlayerTriggeredAbility.java new file mode 100644 index 0000000000..0dff8b8c01 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/ControlledCreaturesDealCombatDamagePlayerTriggeredAbility.java @@ -0,0 +1,114 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.abilities.common; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.DamagedPlayerEvent; +import mage.game.events.GameEvent; +import mage.game.events.GameEvent.EventType; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; + +/** + * + * @author LevelX2 + */ +public class ControlledCreaturesDealCombatDamagePlayerTriggeredAbility extends TriggeredAbilityImpl { + + private boolean madeDamage = false; + private Set damagedPlayerIds = new HashSet<>(); + + public ControlledCreaturesDealCombatDamagePlayerTriggeredAbility(Effect effect) { + this(Zone.BATTLEFIELD, effect); + } + + public ControlledCreaturesDealCombatDamagePlayerTriggeredAbility(Zone zone, Effect effect) { + super(zone, effect, false); + } + + public ControlledCreaturesDealCombatDamagePlayerTriggeredAbility(final ControlledCreaturesDealCombatDamagePlayerTriggeredAbility ability) { + super(ability); + this.madeDamage = ability.madeDamage; + this.damagedPlayerIds = new HashSet<>(); + this.damagedPlayerIds.addAll(ability.damagedPlayerIds); + } + + @Override + public ControlledCreaturesDealCombatDamagePlayerTriggeredAbility copy() { + return new ControlledCreaturesDealCombatDamagePlayerTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == EventType.DAMAGED_PLAYER + || event.getType() == EventType.END_COMBAT_STEP_POST + || event.getType() == EventType.ZONE_CHANGE; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (event.getType() == EventType.DAMAGED_PLAYER) { + DamagedPlayerEvent damageEvent = (DamagedPlayerEvent) event; + Permanent p = game.getPermanent(event.getSourceId()); + if (damageEvent.isCombatDamage() && p != null && p.getControllerId().equals(this.getControllerId())) { + madeDamage = true; + damagedPlayerIds.add(event.getPlayerId()); + } + } + if (event.getType() == EventType.END_COMBAT_STEP_POST) { + if (madeDamage) { + Set damagedPlayersCopy = new HashSet<>(); + damagedPlayersCopy.addAll(damagedPlayerIds); + for (Effect effect : this.getEffects()) { + effect.setValue("damagedPlayers", damagedPlayersCopy); + } + damagedPlayerIds.clear(); + madeDamage = false; + return true; + } + } + if (event.getType() == EventType.ZONE_CHANGE && event.getTargetId().equals(getSourceId())) { + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + if (zEvent.getFromZone() == Zone.GRAVEYARD) { + damagedPlayerIds.clear(); + } + } + return false; + } + + @Override + public String getRule() { + return "Whenever one or more creatures you control deal combat damage to a player, " + super.getRule(); + } +} diff --git a/Mage/src/main/java/mage/counters/CounterType.java b/Mage/src/main/java/mage/counters/CounterType.java index 1b7862d19e..82c783d3a7 100644 --- a/Mage/src/main/java/mage/counters/CounterType.java +++ b/Mage/src/main/java/mage/counters/CounterType.java @@ -56,6 +56,7 @@ public enum CounterType { DIVINITY("divinity"), DOOM("doom"), DREAM("dream"), + ECHO("echo"), ELIXIR("elixir"), ENERGY("energy"), EON("eon"), diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java index 84695fd94f..e2fb79b74e 100644 --- a/Mage/src/main/java/mage/filter/StaticFilters.java +++ b/Mage/src/main/java/mage/filter/StaticFilters.java @@ -48,6 +48,7 @@ public final class StaticFilters { public static final FilterPermanent FILTER_OPPONENTS_PERMANENT_ARTIFACT = new FilterPermanent("artifact an opponent controls"); public static final FilterPermanent FILTER_OPPONENTS_PERMANENT_ARTIFACT_OR_CREATURE = new FilterPermanent("artifact or creature an opponent controls"); + public static final FilterPermanent FILTER_CONTROLLED_CREATURE = new FilterControlledCreaturePermanent(); public static final FilterPermanent FILTER_CONTROLLED_A_CREATURE = new FilterControlledCreaturePermanent("a creature you control"); public static final FilterControlledCreaturePermanent FILTER_CONTROLLED_ANOTHER_CREATURE = new FilterControlledCreaturePermanent("another creature"); public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT_NON_LAND = new FilterControlledPermanent("nonland permanent"); diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index 1152e94dc2..73f297ad8f 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -32696,3 +32696,9 @@ Forest|Duel Decks: Mind vs. Might|65|L||Basic Land - Forest|||{T}: Add {G} to yo Chandra, Gremlin Wrangler|Heroes of the Realm|1|M|{2}{R}{R}|Legendary Planeswalker - Chandra|3||+1: Create a 2/2 red Gremlin creature token.$-2:Chandra, Gremlin Wrangler deals X damage to target creature or player, where X is the number of Gremlins you control.| Dungeon Master|Heroes of the Realm|1|M|{2}{W}{U}|Legendary Planeswalker - Dungeon Master|||+1: Target opponent creates a 1/1 black Skeleton creature token with “When this creature dies, each opponent loses 2 life.”$+1: Roll a d20. If you roll a 1, skip your next turn. If you roll a 12 or higher, draw a card.$-6: You get an adventuring party. (Your party is a 3/3 red Fighter with first strike, a 1/1 white Cleric with lifelink, a 2/2 black Rogue with hexproof, and a 1/1 blue Wizard with flying.)| Nira, Hellkite Duelist|Heroes of the Realm|3|M|{W}{U}{B}{R}{G}|Legendary Creature — Dragon|6|6|Flash$Flying, trample, haste$When Nira, Hellkite Duelist enters the battlefield, the next time you would lose the game this turn, instead draw three cards and your life total becomes 5.| +Silvergill Adept|Rivals of Ixalan|53|U|{1}{U}|Creature - Merfolk Wizard|2|1|As an additional cost to cast Silvergill Adept, reveal a Merfolk card from your hand or pay {3}.$When Silvergill Adept enters the battlefield, draw a card.| +Tetzimoc, Primal Death|Rivals of Ixalan|86|R|{4}{B}{B}|Legendary Creature - Elder Dinosaur|6|6|Deathtouch${B}, Reveal Tetzimoc, Primal Death from your hand: Put a prey counter on target creature. Activate this ability only during your turn.$When Tetzimoc, Primal Death enters the battlefield, destroy each creature your opponents control with a prey counter on it.| +Ghalta, Primal Hunger|Rivals of Ixalan|130|R|{10}{G}{G}|Legendary Creature - Elder Dinosaur|12|12|Ghalta, Primal Hunger costs {X} less to cast, where X is the total power of creatures you control.$Trample| +Storm the Vault|Rivals of Ixalan|173|R|{2}{U}{R}|Legendary Enchantment|||Whenever one or more creatures you control deal combat damage to a player, create a colorless Treasure artifact token with "{T}, Sacrifice this artifact: Add one mana of any color to your mana pool."$At the beginning of your end step, if you control five or more artifacts, transform Storm the Vault.| +Vault of Catlacan|Rivals of Ixalan|173|R||Legendary Land|||(Transforms from Storm the Vault.)${T}: Add one mana of any color to your mana pool.${T}: Add {U} to your mana pool for each artifact you control.| +The Immortal Sun|Rivals of Ixalan|180|M|{6}|Legendary Artifact|||Players can't activate loyalty abilities of planeswalkers.$At the beginning of your draw step, draw an additional card.$Spells you cast cost {1} less to cast.$Creatures you control get +1/+1.| \ No newline at end of file