package org.mage.card.arcane; import com.google.common.base.Function; import com.google.common.collect.MapMaker; import mage.cards.action.ActionCallback; import mage.client.dialog.PreferencesDialog; import mage.client.util.ImageCaches; import mage.client.util.ImageHelper; import mage.components.ImagePanel; import mage.components.ImagePanelStyle; import mage.constants.AbilityType; 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 javax.swing.*; 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 * * @author arcane, nantuko, noxx, stravant */ @SuppressWarnings({"unchecked", "rawtypes"}) public class CardPanelComponentImpl extends CardPanel { private static final long serialVersionUID = -3272134219262184411L; private static final Logger LOGGER = Logger.getLogger(CardPanelComponentImpl.class); private static final int WIDTH_LIMIT = 90; // card width limit to create smaller counter private static final float ROUNDED_CORNER_SIZE = 0.1f; private static final float BLACK_BORDER_SIZE = 0.03f; private static final int TEXT_GLOW_SIZE = 6; private static final float TEXT_GLOW_INTENSITY = 3f; public final ScaledImagePanel imagePanel; public ImagePanel overlayPanel; public JPanel iconPanel; private JButton typeButton; public JPanel counterPanel; private JLabel loyaltyCounterLabel; private JLabel plusCounterLabel; private JLabel otherCounterLabel; private JLabel minusCounterLabel; private int loyaltyCounter; private int plusCounter; private int otherCounter; private int minusCounter; private int lastCardWidth; private final GlowText titleText; private final GlowText ptText; private boolean hasImage = false; private boolean displayTitleAnyway; private final static Map<Key, BufferedImage> IMAGE_CACHE; static class Key { final int width; final int height; final int cardWidth; final int cardHeight; final int cardXOffset; final int cardYOffset; final boolean hasImage; final boolean isSelected; final boolean isChoosable; final boolean isPlayable; final boolean canAttack; public Key(int width, int height, int cardWidth, int cardHeight, int cardXOffset, int cardYOffset, boolean hasImage, boolean isSelected, boolean isChoosable, boolean isPlayable, boolean canAttack) { this.width = width; this.height = height; this.cardWidth = cardWidth; this.cardHeight = cardHeight; this.cardXOffset = cardXOffset; this.cardYOffset = cardYOffset; this.hasImage = hasImage; this.isSelected = isSelected; this.isChoosable = isChoosable; this.isPlayable = isPlayable; this.canAttack = canAttack; } @Override public int hashCode() { int hash = 3; hash = 19 * hash + this.width; hash = 19 * hash + this.height; hash = 19 * hash + this.cardWidth; hash = 19 * hash + this.cardHeight; hash = 19 * hash + this.cardXOffset; hash = 19 * hash + this.cardYOffset; hash = 19 * hash + (this.hasImage ? 1 : 0); hash = 19 * hash + (this.isSelected ? 1 : 0); hash = 19 * hash + (this.isChoosable ? 1 : 0); hash = 19 * hash + (this.isPlayable ? 1 : 0); hash = 19 * hash + (this.canAttack ? 1 : 0); return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Key other = (Key) obj; if (this.width != other.width) { return false; } if (this.height != other.height) { return false; } if (this.cardWidth != other.cardWidth) { return false; } if (this.cardHeight != other.cardHeight) { return false; } if (this.cardXOffset != other.cardXOffset) { return false; } if (this.cardYOffset != other.cardYOffset) { return false; } if (this.hasImage != other.hasImage) { return false; } if (this.isSelected != other.isSelected) { return false; } if (this.isChoosable != other.isChoosable) { return false; } if (this.isPlayable != other.isPlayable) { return false; } if (this.canAttack != other.canAttack) { return false; } return true; } } static { IMAGE_CACHE = ImageCaches.register(new MapMaker().softValues().makeComputingMap((Function<Key, BufferedImage>) key -> createImage(key))); } public CardPanelComponentImpl(CardView newGameCard, UUID gameId, final boolean loadImage, ActionCallback callback, final boolean foil, Dimension dimension) { // Call to super super(newGameCard, gameId, loadImage, callback, foil, dimension); // Counter panel if (!newGameCard.isAbility()) { // panel to show counters on the card counterPanel = new JPanel(); counterPanel.setLayout(null); counterPanel.setOpaque(false); add(counterPanel); plusCounterLabel = new JLabel(""); plusCounterLabel.setToolTipText("+1/+1"); counterPanel.add(plusCounterLabel); minusCounterLabel = new JLabel(""); minusCounterLabel.setToolTipText("-1/-1"); counterPanel.add(minusCounterLabel); loyaltyCounterLabel = new JLabel(""); loyaltyCounterLabel.setToolTipText("loyalty"); counterPanel.add(loyaltyCounterLabel); otherCounterLabel = new JLabel(""); counterPanel.add(otherCounterLabel); counterPanel.setVisible(false); } // Ability icon if (newGameCard.isAbility()) { if (newGameCard.getAbilityType() == AbilityType.TRIGGERED) { setTypeIcon(ImageManagerImpl.instance.getTriggeredAbilityImage(), "Triggered Ability"); } else if (newGameCard.getAbilityType() == AbilityType.ACTIVATED) { setTypeIcon(ImageManagerImpl.instance.getActivatedAbilityImage(), "Activated Ability"); } } // Token icon if (this.gameCard.isToken()) { setTypeIcon(ImageManagerImpl.instance.getTokenIconImage(), "Token Permanent"); } displayTitleAnyway = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_SHOW_CARD_NAMES, "true").equals("true"); // Title Text titleText = new GlowText(); setText(gameCard); // int fontSize = (int) cardHeight / 11; // titleText.setFont(getFont().deriveFont(Font.BOLD, fontSize)); titleText.setForeground(Color.white); titleText.setGlow(Color.black, TEXT_GLOW_SIZE, TEXT_GLOW_INTENSITY); titleText.setWrap(true); add(titleText); // PT Text ptText = new GlowText(); if (gameCard.isCreature()) { ptText.setText(gameCard.getPower() + '/' + gameCard.getToughness()); } else if (gameCard.isPlanesWalker()) { ptText.setText(gameCard.getLoyalty()); } // ptText.setFont(getFont().deriveFont(Font.BOLD, fontSize)); ptText.setForeground(Color.white); ptText.setGlow(Color.black, TEXT_GLOW_SIZE, TEXT_GLOW_INTENSITY); add(ptText); // Sickness overlay BufferedImage sickness = ImageManagerImpl.instance.getSicknessImage(); overlayPanel = new ImagePanel(sickness, ImagePanelStyle.SCALED); overlayPanel.setOpaque(false); add(overlayPanel); // Imagel panel imagePanel = new ScaledImagePanel(); imagePanel.setBorder(BorderFactory.createLineBorder(Color.white)); add(imagePanel); // Do we need to load? if (loadImage) { initialDraw(); } else { // Nothing to do } } private void setTypeIcon(BufferedImage bufferedImage, String toolTipText) { iconPanel = new JPanel(); iconPanel.setLayout(null); iconPanel.setOpaque(false); add(iconPanel); typeButton = new JButton(""); typeButton.setLocation(2, 2); typeButton.setSize(25, 25); iconPanel.setVisible(true); typeButton.setIcon(new ImageIcon(bufferedImage)); if (toolTipText != null) { typeButton.setToolTipText(toolTipText); } iconPanel.add(typeButton); } @Override public void cleanUp() { super.cleanUp(); this.counterPanel = null; } private void setText(CardView card) { titleText.setText(!displayTitleAnyway && hasImage ? "" : card.getName()); } private void setImage(BufferedImage srcImage) { synchronized (imagePanel) { if (srcImage != null) { imagePanel.setImage(srcImage); } else { imagePanel.clearImage(); } repaint(); } doLayout(); } @Override public void transferResources(final CardPanel panelAbstract) { if (panelAbstract instanceof CardPanelComponentImpl) { CardPanelComponentImpl panel = (CardPanelComponentImpl)panelAbstract; synchronized (panel.imagePanel) { if (panel.imagePanel.hasImage()) { setImage(panel.imagePanel.getSrcImage()); } } } } @Override public void setSelected(boolean isSelected) { super.setSelected(isSelected); if (isSelected) { this.titleText.setGlowColor(Color.green); } else { this.titleText.setGlowColor(Color.black); } } @Override protected void paintCard(Graphics2D g2d) { float alpha = getAlpha(); if (alpha != 1.0f) { AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha); g2d.setComposite(composite); } g2d.drawImage( IMAGE_CACHE.get( new Key(getWidth(), getHeight(), getCardWidth(), getCardHeight(), getCardXOffset(), getCardYOffset(), hasImage, isSelected(), isChoosable(), gameCard.isPlayable(), gameCard.isCanAttack())), 0, 0, null); g2d.dispose(); } private static BufferedImage createImage(Key key) { int cardWidth = key.cardWidth; int cardHeight = key.cardHeight; int cardXOffset = key.cardXOffset; int cardYOffset = key.cardYOffset; BufferedImage image = GraphicsUtilities.createCompatibleTranslucentImage(key.width, key.height); Graphics2D g2d = image.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); if (!key.hasImage) { g2d.setColor(new Color(30, 200, 200, 120)); } else { g2d.setColor(new Color(0, 0, 0, 0)); } int cornerSize = Math.max(4, Math.round(cardWidth * ROUNDED_CORNER_SIZE)); g2d.fillRoundRect(cardXOffset, cardYOffset, cardWidth, cardHeight, cornerSize, cornerSize); if (key.isSelected) { g2d.setColor(Color.green); g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); } else if (key.isChoosable) { g2d.setColor(new Color(250, 250, 0, 230)); g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); } else if (key.isPlayable) { g2d.setColor(new Color(153, 102, 204, 200)); //g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); g2d.fillRoundRect(cardXOffset, cardYOffset, cardWidth, cardHeight, cornerSize, cornerSize); } if (key.canAttack) { g2d.setColor(new Color(0, 0, 255, 230)); g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); } //TODO:uncomment /* if (gameCard.isAttacking()) { g2d.setColor(new Color(200,10,10,200)); g2d.fillRoundRect(cardXOffset+1, cardYOffset+1, cardWidth-2, cardHeight-2, cornerSize, cornerSize); }*/ g2d.dispose(); return image; } @Override protected void paintChildren(Graphics g) { super.paintChildren(g); if (getShowCastingCost() && !isAnimationPanel() && getCardWidth() < 200 && getCardWidth() > 60) { String manaCost = ManaSymbols.getStringManaCost(gameCard.getManaCost()); int width = getWidth(manaCost); if (hasImage) { ManaSymbols.draw(g, manaCost, getCardXOffset() + getCardWidth() - width - 5, getCardYOffset() + 5, getSymbolWidth()); } else { ManaSymbols.draw(g, manaCost, getCardXOffset() + 8, getCardHeight() - 9, getSymbolWidth()); } } } private int getWidth(String manaCost) { int width = 0; manaCost = manaCost.replace("\\", ""); StringTokenizer tok = new StringTokenizer(manaCost, " "); while (tok.hasMoreTokens()) { tok.nextToken(); width += getSymbolWidth(); } return width; } @Override public void doLayout() { super.doLayout(); int cardWidth = getCardWidth(); int cardHeight = getCardHeight(); int cardXOffset = getCardXOffset(); int cardYOffset = getCardYOffset(); int borderSize = Math.round(cardWidth * BLACK_BORDER_SIZE); imagePanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize); imagePanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2); if (hasSickness() && gameCard.isCreature() && isPermanent()) { overlayPanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize); overlayPanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2); } else { overlayPanel.setVisible(false); } if (iconPanel != null) { iconPanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize); iconPanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2); } if (counterPanel != null) { counterPanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize); counterPanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2); int size = cardWidth > WIDTH_LIMIT ? 40 : 20; minusCounterLabel.setLocation(counterPanel.getWidth() - size, counterPanel.getHeight() - size * 2); minusCounterLabel.setSize(size, size); plusCounterLabel.setLocation(5, counterPanel.getHeight() - size * 2); plusCounterLabel.setSize(size, size); loyaltyCounterLabel.setLocation(counterPanel.getWidth() - size, counterPanel.getHeight() - size); loyaltyCounterLabel.setSize(size, size); otherCounterLabel.setLocation(5, counterPanel.getHeight() - size); otherCounterLabel.setSize(size, size); } int fontHeight = Math.round(cardHeight * (27f / 680)); boolean showText = (!isAnimationPanel() && fontHeight < 12); titleText.setVisible(showText); ptText.setVisible(showText); if (showText) { int fontSize = cardHeight / 11; titleText.setFont(getFont().deriveFont(Font.BOLD, fontSize)); int titleX = Math.round(cardWidth * (20f / 480)); int titleY = Math.round(cardHeight * (9f / 680)) + getTextOffset(); titleText.setBounds(cardXOffset + titleX, cardYOffset + titleY, cardWidth - titleX, cardHeight - titleY); ptText.setFont(getFont().deriveFont(Font.BOLD, fontSize)); Dimension ptSize = ptText.getPreferredSize(); ptText.setSize(ptSize.width, ptSize.height); int ptX = Math.round(cardWidth * (420f / 480)) - ptSize.width / 2; int ptY = Math.round(cardHeight * (675f / 680)) - ptSize.height; int offsetX = Math.round((CARD_SIZE_FULL.width - cardWidth) / 10.0f); ptText.setLocation(cardXOffset + ptX - TEXT_GLOW_SIZE / 2 - offsetX, cardYOffset + ptY - TEXT_GLOW_SIZE / 2); } } @Override public String toString() { return gameCard.toString(); } @Override public void setCardBounds(int x, int y, int cardWidth, int cardHeight) { // Call to super super.setCardBounds(x, y, cardWidth, cardHeight); // Update image if (imagePanel != null && imagePanel.getSrcImage() != null) { updateArtImage(); } } @Override public void setAlpha(float alpha) { super.setAlpha(alpha); // Update components if (alpha == 0) { this.ptText.setVisible(false); this.titleText.setVisible(false); } else if (alpha == 1.0f) { this.ptText.setVisible(true); this.titleText.setVisible(true); } } /////////////////////////////////////////////////////////// // Image updating code private int updateArtImageStamp; @Override public void updateArtImage() { tappedAngle = isTapped() ? CardPanel.TAPPED_ANGLE : 0; flippedAngle = isFlipped() ? CardPanel.FLIPPED_ANGLE : 0; //final CardView gameCard = this.gameCard; final int stamp = ++updateArtImageStamp; Util.threadPool.submit(() -> { try { final BufferedImage srcImage; if (gameCard.isFaceDown()) { srcImage = getFaceDownImage(); } else if (getCardWidth() > THUMBNAIL_SIZE_FULL.width) { srcImage = ImageCache.getImage(gameCard, getCardWidth(), getCardHeight()); } else { srcImage = ImageCache.getThumbnail(gameCard); } UI.invokeLater(() -> { if (stamp == updateArtImageStamp) { hasImage = srcImage != null; setText(gameCard); setImage(srcImage); } }); } catch (Exception e) { e.printStackTrace(); } catch (Error err) { err.printStackTrace(); } }); } private BufferedImage getFaceDownImage() { if (isPermanent()) { if (((PermanentView) gameCard).isMorphed()) { return ImageCache.getMorphImage(); } else { return ImageCache.getManifestImage(); } } else if (this.gameCard instanceof StackAbilityView) { return ImageCache.getMorphImage(); } else { return ImageCache.loadImage(new TFile(DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename)); } } @Override public void showCardTitle() { displayTitleAnyway = true; setText(gameCard); } @Override public void update(CardView card) { // Super super.update(card); // Update card text if (card.isCreature() && card.isPlanesWalker()) { ptText.setText(card.getPower() + '/' + card.getToughness() + " (" + card.getLoyalty() + ')'); } else if (card.isCreature()) { ptText.setText(card.getPower() + '/' + card.getToughness()); } else if (card.isPlanesWalker()) { ptText.setText(card.getLoyalty()); } else { ptText.setText(""); } setText(card); // Summoning Sickness overlay if (hasSickness() && card.isCreature() && isPermanent()) { overlayPanel.setVisible(true); } else { overlayPanel.setVisible(false); } // Update counters panel if (counterPanel != null) { updateCounters(card); } // Finally, queue a repaint repaint(); } private void updateCounters(CardView card) { if (card.getCounters() != null && !card.getCounters().isEmpty()) { String name = ""; if (lastCardWidth != getCardWidth()) { lastCardWidth = getCardWidth(); plusCounter = 0; minusCounter = 0; otherCounter = 0; loyaltyCounter = 0; } plusCounterLabel.setVisible(false); minusCounterLabel.setVisible(false); loyaltyCounterLabel.setVisible(false); otherCounterLabel.setVisible(false); for (CounterView counterView : card.getCounters()) { if (counterView.getCount() == 0) { continue; } switch (counterView.getName()) { case "+1/+1": if (counterView.getCount() != plusCounter) { plusCounter = counterView.getCount(); plusCounterLabel.setIcon(getCounterImageWithAmount(plusCounter, ImageManagerImpl.instance.getCounterImageGreen(), getCardWidth())); } plusCounterLabel.setVisible(true); break; case "-1/-1": if (counterView.getCount() != minusCounter) { minusCounter = counterView.getCount(); minusCounterLabel.setIcon(getCounterImageWithAmount(minusCounter, ImageManagerImpl.instance.getCounterImageRed(), getCardWidth())); } minusCounterLabel.setVisible(true); break; case "loyalty": if (counterView.getCount() != loyaltyCounter) { loyaltyCounter = counterView.getCount(); loyaltyCounterLabel.setIcon(getCounterImageWithAmount(loyaltyCounter, ImageManagerImpl.instance.getCounterImageViolet(), getCardWidth())); } loyaltyCounterLabel.setVisible(true); break; default: if (name.isEmpty()) { // only first other counter is shown name = counterView.getName(); otherCounter = counterView.getCount(); otherCounterLabel.setToolTipText(name); otherCounterLabel.setIcon(getCounterImageWithAmount(otherCounter, ImageManagerImpl.instance.getCounterImageGrey(), getCardWidth())); otherCounterLabel.setVisible(true); } } } counterPanel.setVisible(true); } else { plusCounterLabel.setVisible(false); minusCounterLabel.setVisible(false); loyaltyCounterLabel.setVisible(false); otherCounterLabel.setVisible(false); counterPanel.setVisible(false); } } private static ImageIcon getCounterImageWithAmount(int amount, BufferedImage image, int cardWidth) { int factor = cardWidth > WIDTH_LIMIT ? 2 : 1; int xOffset = amount > 9 ? 2 : 5; int fontSize = factor == 1 ? amount < 10 ? 12 : amount < 100 ? 10 : amount < 1000 ? 7 : 6 : amount < 10 ? 19 : amount < 100 ? 15 : amount < 1000 ? 12 : amount < 10000 ? 9 : 8; BufferedImage newImage; if (cardWidth > WIDTH_LIMIT) { newImage = ImageManagerImpl.deepCopy(image); } else { newImage = ImageHelper.getResizedImage(image, 20, 20); } Graphics graphics = newImage.getGraphics(); graphics.setColor(Color.BLACK); graphics.setFont(new Font("Arial Black", amount > 100 ? Font.PLAIN : Font.BOLD, fontSize)); graphics.drawString(Integer.toString(amount), xOffset * factor, 11 * factor); return new ImageIcon(newImage); } @Override public Image getImage() { if (this.hasImage) { if (gameCard.isFaceDown()) { return getFaceDownImage(); } else { return ImageCache.getImageOriginal(gameCard); } } return null; } }