package org.mage.plugins.card; import mage.cards.MagePermanent; import mage.cards.action.ActionCallback; 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; import net.xeoh.plugins.base.annotations.PluginImplementation; import net.xeoh.plugins.base.annotations.events.Init; import net.xeoh.plugins.base.annotations.events.PluginLoaded; import net.xeoh.plugins.base.annotations.meta.Author; import org.apache.log4j.Logger; 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; import org.mage.plugins.card.images.ImageCache; import org.mage.plugins.card.info.CardInfoPaneImpl; import javax.swing.*; import java.awt.*; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.util.*; import java.util.List; import java.util.concurrent.TimeUnit; /** * {@link CardPlugin} implementation. * * @author nantuko * @version 0.1 01.11.2010 Mage permanents. Sorting card layout. * @version 0.6 17.07.2011 #sortPermanents got option to display non-land * permanents in one pile * @version 0.7 29.07.2011 face down cards support */ @PluginImplementation @Author(name = "nantuko") public class CardPluginImpl implements CardPlugin { private static final Logger LOGGER = Logger.getLogger(CardPluginImpl.class); private static final int GUTTER_Y = 15; private static final int GUTTER_X = 5; static final float EXTRA_CARD_SPACING_X = 0.04f; private static final float CARD_SPACING_Y = 0.03f; private static final float STACK_SPACING_X = 0.07f; private static final float STACK_SPACING_Y = 0.10f; private static final float ATTACHMENT_SPACING_Y = 0.13f; private static final int landStackMax = 5; // private int cardWidthMin = 50, cardWidthMax = Constants.CARD_SIZE_FULL.width; private int cardWidthMin = (int) GUISizeHelper.battlefieldCardMinDimension.getWidth(); private int cardWidthMax = (int) GUISizeHelper.battlefieldCardMaxDimension.getWidth(); private static final boolean stackVertical = false; private int playAreaWidth, playAreaHeight; private int cardWidth, cardHeight; private int extraCardSpacingX, cardSpacingX, cardSpacingY; private int stackSpacingX, stackSpacingY, attachmentSpacingY; private List<Row> rows = new ArrayList<>(); public CardPluginImpl() { setGUISize(); } @Init public void init() { } @PluginLoaded public void newPlugin(CardPlugin plugin) { LOGGER.info(plugin.toString() + " has been loaded."); } @Override public String toString() { return "[Card plugin, version 0.7]"; } @Override public void changeGUISize() { setGUISize(); } private void setGUISize() { cardWidthMin = (int) GUISizeHelper.battlefieldCardMinDimension.getWidth(); cardWidthMax = (int) GUISizeHelper.battlefieldCardMaxDimension.getWidth(); } /** * Temporary card rendering shim. Split card rendering isn't implemented * yet, so use old component based rendering for the split cards. */ private CardPanel makePanel(CardView view, UUID gameId, boolean loadImage, ActionCallback callback, boolean isFoil, Dimension dimension) { String fallback = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_RENDERING_FALLBACK, "false"); if (fallback.equals("true")) { return new CardPanelComponentImpl(view, gameId, loadImage, callback, isFoil, dimension); } else { return new CardPanelRenderImpl(view, gameId, loadImage, callback, isFoil, dimension); } } @Override public MagePermanent getMagePermanent(PermanentView permanent, Dimension dimension, UUID gameId, ActionCallback callback, boolean canBeFoil, boolean loadImage) { CardPanel cardPanel = makePanel(permanent, gameId, loadImage, callback, false, dimension); boolean implemented = permanent.getRarity() != Rarity.NA; cardPanel.setShowCastingCost(implemented); return cardPanel; } @Override public MagePermanent getMageCard(CardView cardView, Dimension dimension, UUID gameId, ActionCallback callback, boolean canBeFoil, boolean loadImage) { CardPanel cardPanel = makePanel(cardView, gameId, loadImage, callback, false, dimension); boolean implemented = cardView.getRarity() != null && cardView.getRarity() != Rarity.NA; cardPanel.setShowCastingCost(implemented); return cardPanel; } @Override public int sortPermanents(Map<String, JComponent> ui, Collection<MagePermanent> permanents, boolean nonPermanentsOwnRow, boolean topPanel) { //TODO: add caching //requires to find out is position have been changed that includes: //adding/removing permanents, type change if (ui == null) { throw new RuntimeException("Error: no components"); } JComponent component = ui.get("battlefieldPanel"); if (component == null) { throw new RuntimeException("Error: battlefieldPanel is missing"); } JLayeredPane battlefieldPanel = (JLayeredPane) component; JComponent jPanel = ui.get("jPanel"); Row rowAllLands = new Row(); outerLoop: // for (MagePermanent permanent : permanents) { if (!permanent.isLand() || permanent.isCreature()) { continue; } int insertIndex = -1; // Find already added lands with the same name. for (int i = 0, n = rowAllLands.size(); i < n; i++) { Stack stack = rowAllLands.get(i); MagePermanent firstPanel = stack.get(0); if (firstPanel.getOriginal().getName().equals(permanent.getOriginal().getName())) { if (!empty(firstPanel.getOriginalPermanent().getAttachments())) { // Put this land to the left of lands with the same name and attachments. insertIndex = i; break; } List<CounterView> counters = firstPanel.getOriginalPermanent().getCounters(); if (counters != null && !counters.isEmpty()) { // don't put to first panel if it has counters insertIndex = i; break; } if (!empty(permanent.getOriginalPermanent().getAttachments()) || stack.size() == landStackMax) { // If this land has attachments or the stack is full, put it to the right. insertIndex = i + 1; continue; } counters = permanent.getOriginalPermanent().getCounters(); if (counters != null && !counters.isEmpty()) { // if a land has counter, put it to the right insertIndex = i + 1; continue; } // Add to stack. stack.add(0, permanent); continue outerLoop; } if (insertIndex != -1) { break; } } Stack stack = new Stack(); if (permanent.getOriginalPermanent().getAttachments() != null) { stack.setMaxAttachedCount(permanent.getOriginalPermanent().getAttachments().size()); } stack.add(permanent); rowAllLands.add(insertIndex == -1 ? rowAllLands.size() : insertIndex, stack); } Row rowAllCreatures = new Row(permanents, RowType.creature); Row rowAllOthers = new Row(permanents, RowType.other); Row rowAllAttached = new Row(permanents, RowType.attached); boolean othersOnTheRight = true; if (nonPermanentsOwnRow) { othersOnTheRight = false; rowAllCreatures.addAll(rowAllOthers); rowAllOthers.clear(); } cardWidth = cardWidthMax; Rectangle rect = battlefieldPanel.getVisibleRect(); playAreaWidth = rect.width; playAreaHeight = rect.height; while (true) { rows.clear(); // calculate values based on the card size that is changing with every iteration cardHeight = Math.round(cardWidth * CardPanel.ASPECT_RATIO); extraCardSpacingX = Math.round(cardWidth * EXTRA_CARD_SPACING_X); cardSpacingX = cardHeight - cardWidth + extraCardSpacingX; cardSpacingY = Math.round(cardHeight * CARD_SPACING_Y); stackSpacingX = stackVertical ? 0 : Math.round(cardWidth * STACK_SPACING_X); stackSpacingY = Math.round(cardHeight * STACK_SPACING_Y); attachmentSpacingY = Math.round(cardHeight * ATTACHMENT_SPACING_Y); // clone data Row creatures = (Row) rowAllCreatures.clone(); Row lands = (Row) rowAllLands.clone(); Row others = (Row) rowAllOthers.clone(); // Wrap all creatures and lands. int addOthersIndex; if (topPanel) { wrap(lands, rows, -1); wrap(others, rows, rows.size()); addOthersIndex = rows.size(); wrap(creatures, rows, addOthersIndex); } else { wrap(creatures, rows, -1); addOthersIndex = rows.size(); wrap(lands, rows, rows.size()); wrap(others, rows, rows.size()); } // Store the current rows and others. List<Row> storedRows = new ArrayList<>(rows.size()); for (Row row : rows) { storedRows.add((Row) row.clone()); } Row storedOthers = (Row) others.clone(); // Fill in all rows with others. for (Row row : rows) { fillRow(others, rows, row); } // Stop if everything fits, otherwise revert back to the stored values. if (creatures.isEmpty() && lands.isEmpty() && others.isEmpty()) { break; } rows = storedRows; others = storedOthers; // Try to put others on their own row(s) and fill in the rest. wrap(others, rows, addOthersIndex); for (Row row : rows) { fillRow(others, rows, row); } // If that still doesn't fit, scale down. if (creatures.isEmpty() && lands.isEmpty() && others.isEmpty()) { break; } //FIXME: -1 is too slow. why not binary search? cardWidth -= 3; } // Get size of all the rows. int x, y = GUTTER_Y; int maxRowWidth = 0; for (Row row : rows) { int rowBottom = 0; x = GUTTER_X; for (int stackIndex = 0, stackCount = row.size(); stackIndex < stackCount; stackIndex++) { Stack stack = row.get(stackIndex); rowBottom = Math.max(rowBottom, y + stack.getHeight()); x += stack.getWidth(); } y = rowBottom; maxRowWidth = Math.max(maxRowWidth, x); } // Position all card panels. y = GUTTER_Y; for (Row row : rows) { int rowBottom = 0; x = GUTTER_X; for (int stackIndex = 0, stackCount = row.size(); stackIndex < stackCount; stackIndex++) { Stack stack = row.get(stackIndex); // Align others to the right. if (othersOnTheRight && RowType.other.isType(stack.get(0))) { x = playAreaWidth - GUTTER_X + extraCardSpacingX; for (int i = stackIndex, n = row.size(); i < n; i++) { x -= row.get(i).getWidth(); } } for (int panelIndex = 0, panelCount = stack.size(); panelIndex < panelCount; panelIndex++) { MagePermanent panel = stack.get(panelIndex); int stackPosition = panelCount - panelIndex - 1; if (jPanel != null) { jPanel.setComponentZOrder(panel, panelIndex); } int panelX = x + (stackPosition * stackSpacingX); int panelY = y + (stackPosition * stackSpacingY); try { // may cause: // java.lang.IllegalArgumentException: illegal component position 26 should be less then 26 battlefieldPanel.moveToFront(panel); } catch (Exception e) { e.printStackTrace(); } panel.setCardBounds(panelX, panelY, cardWidth, cardHeight); } rowBottom = Math.max(rowBottom, y + stack.getHeight()); x += stack.getWidth(); } y = rowBottom; } // we need this only for defining card size // attached permanents will be handled separately for (Stack stack : rowAllAttached) { for (MagePermanent panel : stack) { panel.setCardBounds(0, 0, cardWidth, cardHeight); } } return y; } private boolean empty(List<?> list) { return list == null || list.isEmpty(); } private int wrap(Row sourceRow, List<Row> rows, int insertIndex) { // The cards are sure to fit (with vertical scrolling) at the minimum card width. boolean allowHeightOverflow = cardWidth == cardWidthMin; Row currentRow = new Row(); for (int i = 0, n = sourceRow.size() - 1; i <= n; i++) { Stack stack = sourceRow.get(i); // If the row is not empty and this stack doesn't fit, add the row. int rowWidth = currentRow.getWidth(); if (!currentRow.isEmpty() && rowWidth + stack.getWidth() > playAreaWidth) { // Stop processing if the row is too wide or tall. if (!allowHeightOverflow && rowWidth > playAreaWidth) { break; } if (!allowHeightOverflow && getRowsHeight(rows) + sourceRow.getHeight() > playAreaHeight) { break; } rows.add(insertIndex == -1 ? rows.size() : insertIndex, currentRow); currentRow = new Row(); } currentRow.add(stack); } // Add the last row if it is not empty and it fits. if (!currentRow.isEmpty()) { int rowWidth = currentRow.getWidth(); if (allowHeightOverflow || rowWidth <= playAreaWidth) { if (allowHeightOverflow || getRowsHeight(rows) + sourceRow.getHeight() <= playAreaHeight) { rows.add(insertIndex == -1 ? rows.size() : insertIndex, currentRow); } } } // Remove the wrapped stacks from the source row. for (Row row : rows) { for (Stack stack : row) { sourceRow.remove(stack); } } return insertIndex; } private void fillRow(Row sourceRow, List<Row> rows, Row row) { int rowWidth = row.getWidth(); while (!sourceRow.isEmpty()) { Stack stack = sourceRow.get(0); rowWidth += stack.getWidth(); if (rowWidth > playAreaWidth) { break; } if (stack.getHeight() > row.getHeight() && getRowsHeight(rows) - row.getHeight() + stack.getHeight() > playAreaHeight) { break; } row.add(sourceRow.remove(0)); } } private int getRowsHeight(List<Row> rows) { int height = 0; for (Row row : rows) { height += row.getHeight(); } return height - cardSpacingY + GUTTER_Y * 2; } private enum RowType { land, creature, other, attached; public boolean isType(MagePermanent card) { switch (this) { case land: return card.isLand(); case creature: return card.isCreature(); case other: return !card.isLand() && !card.isCreature(); case attached: return card.getOriginalPermanent().isAttachedToPermanent(); default: throw new RuntimeException("Unhandled type: " + this); } } } private class Row extends ArrayList<Stack> { private static final long serialVersionUID = 1L; public Row() { super(16); } public Row(Collection<MagePermanent> permanents, RowType type) { this(); addAll(permanents, type); } private void addAll(Collection<MagePermanent> permanents, RowType type) { for (MagePermanent permanent : permanents) { if (!type.isType(permanent)) { continue; } // all attached permanents are grouped separately later if (type != RowType.attached && RowType.attached.isType(permanent)) { continue; } Stack stack = new Stack(); stack.add(permanent); if (permanent.getOriginalPermanent().getAttachments() != null) { stack.setMaxAttachedCount(permanent.getOriginalPermanent().getAttachments().size()); } add(stack); } } @Override public boolean addAll(Collection<? extends Stack> c) { boolean changed = super.addAll(c); c.clear(); return changed; } private int getWidth() { if (isEmpty()) { return 0; } int width = 0; for (Stack stack : this) { width += stack.getWidth(); } return width + GUTTER_X * 2 - extraCardSpacingX; } private int getHeight() { if (isEmpty()) { return 0; } int height = 0; for (Stack stack : this) { height = Math.max(height, stack.getHeight()); } return height; } } private class Stack extends ArrayList<MagePermanent> { private static final long serialVersionUID = 1L; /** * Max attached object count attached to single permanent in the stack. */ private int maxAttachedCount = 0; public Stack() { super(8); } private int getWidth() { return cardWidth + (size() - 1) * stackSpacingX + cardSpacingX; } private int getHeight() { return cardHeight + (size() - 1) * stackSpacingY + cardSpacingY + attachmentSpacingY * maxAttachedCount; } public int getMaxAttachedCount() { return maxAttachedCount; } public void setMaxAttachedCount(int maxAttachedCount) { this.maxAttachedCount = maxAttachedCount; } } /** * 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. */ @Override public void downloadSymbols(String imagesPath) { final DownloadGui g = new DownloadGui(new Downloader()); Iterable<DownloadJob> it = new GathererSymbols(imagesPath); for (DownloadJob job : it) { g.getDownloader().add(job); } it = new GathererSets(imagesPath); for (DownloadJob job : it) { g.getDownloader().add(job); } it = new CardFrames(imagesPath); for (DownloadJob job : it) { g.getDownloader().add(job); } it = new DirectLinksForDownload(imagesPath); for (DownloadJob job : it) { g.getDownloader().add(job); } JDialog d = new JDialog((Frame) null, "Download pictures", false); d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); d.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { g.getDownloader().dispose(); ManaSymbols.loadImages(); } }); d.setLayout(new BorderLayout()); d.add(g); d.pack(); d.setVisible(true); } @Override public void onAddCard(MagePermanent card, int count) { if (card != null) { Animation.showCard(card, count > 0 ? count : 1); try { while ((card).getAlpha() + 0.05f < 1) { TimeUnit.MILLISECONDS.sleep(30); } } catch (Exception e) { e.printStackTrace(); } } } @Override public void onRemoveCard(MagePermanent card, int count) { if (card != null) { Animation.hideCard(card, count > 0 ? count : 1); try { while ((card).getAlpha() - 0.05f > 0) { TimeUnit.MILLISECONDS.sleep(30); } } catch (Exception e) { e.printStackTrace(); } } } @Override public JComponent getCardInfoPane() { return new CardInfoPaneImpl(); } @Override public BufferedImage getOriginalImage(CardView card) { return ImageCache.getImageOriginal(card); } }