package org.mage.card.arcane;
import mage.cards.MagePermanent;
import mage.cards.TextPopup;
import mage.cards.action.ActionCallback;
import mage.cards.action.TransferData;
import mage.client.plugins.adapters.MageActionCallback;
import mage.client.plugins.impl.Plugins;
import mage.client.util.audio.AudioManager;
import mage.constants.CardType;
import mage.constants.EnlargeMode;
import mage.constants.SuperType;
import mage.view.AbilityView;
import mage.view.CardView;
import mage.view.PermanentView;
import mage.view.StackAbilityView;
import org.apache.log4j.Logger;
import org.mage.plugins.card.utils.impl.ImageManagerImpl;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Main class for drawing Mage card object.
*
* @author arcane, nantuko, noxx
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public abstract class CardPanel extends MagePermanent implements MouseListener, MouseMotionListener, MouseWheelListener, ComponentListener {
private static final long serialVersionUID = -3272134219262184410L;
private static final Logger LOGGER = Logger.getLogger(CardPanel.class);
public static final double TAPPED_ANGLE = Math.PI / 2;
public static final double FLIPPED_ANGLE = Math.PI;
public static final float ASPECT_RATIO = 3.5f / 2.5f;
public static final int POPUP_X_GAP = 1; // prevent tooltip window from blinking
public static final Rectangle CARD_SIZE_FULL = new Rectangle(101, 149);
private static final float ROT_CENTER_TO_TOP_CORNER = 1.0295630140987000315797369464196f;
private static final float ROT_CENTER_TO_BOTTOM_CORNER = 0.7071067811865475244008443621048f;
public CardView gameCard;
public CardView updateCard;
// for two faced cards
public CardView temporary;
public double tappedAngle = 0;
public double flippedAngle = 0;
private final List<MagePermanent> links = new ArrayList<>();
public final JPanel buttonPanel;
private JButton dayNightButton;
private JButton showCopySourceButton;
private boolean displayEnabled = true;
private boolean isAnimationPanel;
private int cardXOffset, cardYOffset, cardWidth, cardHeight;
private int symbolWidth;
private boolean isSelected;
private boolean isChoosable;
private boolean showCastingCost;
private float alpha = 1.0f;
private ActionCallback callback;
protected boolean tooltipShowing;
protected final TextPopup tooltipText;
protected UUID gameId;
private TransferData data = new TransferData();
private boolean isPermanent;
private boolean hasSickness;
private String zone;
public double transformAngle = 1;
private boolean transformed;
private boolean animationInProgress = false;
private JPanel cardArea;
private int yTextOffset = 10;
// if this is set, it's opened if the user right clicks on the card panel
private JPopupMenu popupMenu;
public CardPanel(CardView newGameCard, UUID gameId, final boolean loadImage, ActionCallback callback, final boolean foil, Dimension dimension) {
// Store away params
this.gameCard = newGameCard;
this.callback = callback;
this.gameId = gameId;
// Gather info about the card
this.isPermanent = this.gameCard instanceof PermanentView && !this.gameCard.inViewerOnly();
if (isPermanent) {
this.hasSickness = ((PermanentView) this.gameCard).hasSummoningSickness();
}
// Set to requested size
this.setCardBounds(0, 0, dimension.width, dimension.height);
// Create button panel for Transform and Show Source (copied cards)
buttonPanel = new JPanel();
buttonPanel.setLayout(null);
buttonPanel.setOpaque(false);
buttonPanel.setVisible(true);
add(buttonPanel);
// Both card rendering implementations have a transform button
if (this.gameCard.canTransform()) {
// Create the day night button
dayNightButton = new JButton("");
dayNightButton.setSize(32, 32);
dayNightButton.setToolTipText("This permanent is a double faced card. To see the back face card, push this button or turn mouse wheel down while hovering with the mouse pointer over the permanent.");
BufferedImage day = ImageManagerImpl.instance.getDayImage();
dayNightButton.setIcon(new ImageIcon(day));
dayNightButton.addActionListener(e -> {
// if card is being rotated, ignore action performed
// if card is tapped, no visual transforming is possible (implementation limitation)
// if card is permanent, it will be rotated by Mage, so manual rotate should be possible
if (animationInProgress || isTapped() || isPermanent) {
return;
}
Animation.transformCard(CardPanel.this, CardPanel.this, true);
});
// Add it
buttonPanel.add(dayNightButton);
}
// Both card rendering implementations have a view copy source button
if (this.gameCard instanceof PermanentView) {
// Create the show source button
showCopySourceButton = new JButton("");
showCopySourceButton.setSize(32, 32);
showCopySourceButton.setToolTipText("This permanent is copying a target. To see original card, push this button or turn mouse wheel down while hovering with the mouse pointer over the permanent.");
showCopySourceButton.setVisible(((PermanentView) this.gameCard).isCopy());
showCopySourceButton.setIcon(new ImageIcon(ImageManagerImpl.instance.getCopyInformIconImage()));
showCopySourceButton.addActionListener(e -> {
ActionCallback callback1 = Plugins.instance.getActionCallback();
((MageActionCallback) callback1).enlargeCard(EnlargeMode.COPY);
});
// Add it
buttonPanel.add(showCopySourceButton);
}
// JPanel setup
setBackground(Color.black);
setOpaque(false);
// JPanel event listeners
addMouseListener(this);
addMouseMotionListener(this);
addMouseWheelListener(this);
addComponentListener(this);
// Tooltip for card details hover
String cardType = getType(newGameCard);
tooltipText = new TextPopup();
tooltipText.setText(getText(cardType, newGameCard));
// Animation setup
tappedAngle = isTapped() ? CardPanel.TAPPED_ANGLE : 0;
flippedAngle = isFlipped() ? CardPanel.FLIPPED_ANGLE : 0;
}
@Override
public void doLayout() {
// Position transform and show source buttons
buttonPanel.setLocation(cardXOffset, cardYOffset);
buttonPanel.setSize(cardWidth, cardHeight);
int x = cardWidth / 20;
int y = cardHeight / 10;
if (dayNightButton != null) {
dayNightButton.setLocation(x, y);
y += 25;
y += 5;
}
if (showCopySourceButton != null) {
showCopySourceButton.setLocation(x, y);
}
}
public final void initialDraw() {
// Kick off
if (gameCard.isTransformed()) {
// this calls updateImage
toggleTransformed();
} else {
updateArtImage();
}
}
public void setIsPermanent(boolean isPermanent) {
this.isPermanent = isPermanent;
}
public void cleanUp() {
if (dayNightButton != null) {
for (ActionListener al : dayNightButton.getActionListeners()) {
dayNightButton.removeActionListener(al);
}
}
for (MouseListener ml : this.getMouseListeners()) {
this.removeMouseListener(ml);
}
for (MouseMotionListener ml : this.getMouseMotionListeners()) {
this.removeMouseMotionListener(ml);
}
for (MouseWheelListener ml : this.getMouseWheelListeners()) {
this.removeMouseWheelListener(ml);
}
// this holds reference to ActionCallback forever so set it to null to prevent
this.callback = null;
this.data = null;
}
// Copy the graphical resources of another CardPanel over to this one,
// if possible (may not be possible if they have different implementations)
// Used when cards are moving between zones
public abstract void transferResources(CardPanel panel);
@Override
public void setZone(String zone) {
this.zone = zone;
}
@Override
public String getZone() {
return zone;
}
public void setDisplayEnabled(boolean displayEnabled) {
this.displayEnabled = displayEnabled;
}
public boolean isDisplayEnabled() {
return displayEnabled;
}
public void setAnimationPanel(boolean isAnimationPanel) {
this.isAnimationPanel = isAnimationPanel;
}
public boolean isAnimationPanel() {
return this.isAnimationPanel;
}
@Override
public void setSelected(boolean isSelected) {
this.isSelected = isSelected;
}
public boolean isSelected() {
return this.isSelected;
}
@Override
public List<MagePermanent> getLinks() {
return links;
}
@Override
public void setChoosable(boolean isChoosable) {
this.isChoosable = isChoosable;
}
public boolean isChoosable() {
return this.isChoosable;
}
public boolean hasSickness() {
return this.hasSickness;
}
public boolean isPermanent() {
return this.isPermanent;
}
@Override
public void setCardAreaRef(JPanel cardArea) {
this.cardArea = cardArea;
}
public void setShowCastingCost(boolean showCastingCost) {
this.showCastingCost = showCastingCost;
}
public boolean getShowCastingCost() {
return this.showCastingCost;
}
/**
* Overridden by different card rendering styles
*
* @param g
*/
protected abstract void paintCard(Graphics2D g);
@Override
public void paint(Graphics g) {
if (!displayEnabled) {
return;
}
if (!isValid()) {
super.validate();
}
Graphics2D g2d = (Graphics2D) g;
if (transformAngle < 1) {
float edgeOffset = (cardWidth + cardXOffset) / 2f;
g2d.translate(edgeOffset * (1 - transformAngle), 0);
g2d.scale(transformAngle, 1);
}
if (tappedAngle + flippedAngle > 0) {
g2d = (Graphics2D) g2d.create();
float edgeOffset = cardWidth / 2f;
double angle = tappedAngle + (Math.abs(flippedAngle - FLIPPED_ANGLE) < 0.001 ? 0 : flippedAngle);
g2d.rotate(angle, cardXOffset + edgeOffset, cardYOffset + cardHeight - edgeOffset);
}
super.paint(g2d);
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) (g.create());
// Deferr to subclasses
paintCard(g2d);
// Done, dispose of the context
g2d.dispose();
}
@Override
public String toString() {
return gameCard.toString();
}
@Override
public void setCardBounds(int x, int y, int cardWidth, int cardHeight) {
if (cardWidth == this.cardWidth && cardHeight == this.cardHeight) {
setBounds(x - cardXOffset, y - cardYOffset, getWidth(), getHeight());
return;
}
this.cardWidth = cardWidth;
this.symbolWidth = cardWidth / 7;
this.cardHeight = cardHeight;
if (this.isPermanent) {
int rotCenterX = Math.round(cardWidth / 2f);
int rotCenterY = cardHeight - rotCenterX;
int rotCenterToTopCorner = Math.round(cardWidth * CardPanel.ROT_CENTER_TO_TOP_CORNER);
int rotCenterToBottomCorner = Math.round(cardWidth * CardPanel.ROT_CENTER_TO_BOTTOM_CORNER);
int xOffset = getXOffset(cardWidth);
int yOffset = getYOffset(cardWidth, cardHeight);
cardXOffset = -xOffset;
cardYOffset = -yOffset;
int width = -xOffset + rotCenterX + rotCenterToTopCorner;
int height = -yOffset + rotCenterY + rotCenterToBottomCorner;
setBounds(x + xOffset, y + yOffset, width, height);
} else {
cardXOffset = 0;
cardYOffset = 0;
int width = cardXOffset * 2 + cardWidth;
int height = cardYOffset * 2 + cardHeight;
setBounds(x - cardXOffset, y - cardYOffset, width, height);
}
}
public int getXOffset(int cardWidth) {
if (this.isPermanent) {
int rotCenterX = Math.round(cardWidth / 2f);
int rotCenterToBottomCorner = Math.round(cardWidth * CardPanel.ROT_CENTER_TO_BOTTOM_CORNER);
int xOffset = rotCenterX - rotCenterToBottomCorner;
return xOffset;
} else {
return cardXOffset;
}
}
public final int getYOffset(int cardWidth, int cardHeight) {
if (this.isPermanent) {
int rotCenterX = Math.round(cardWidth / 2f);
int rotCenterY = cardHeight - rotCenterX;
int rotCenterToTopCorner = Math.round(cardWidth * CardPanel.ROT_CENTER_TO_TOP_CORNER);
int yOffset = rotCenterY - rotCenterToTopCorner;
return yOffset;
} else {
return cardYOffset;
}
}
public final int getCardX() {
return getX() + cardXOffset;
}
public final int getCardY() {
return getY() + cardYOffset;
}
public final int getCardWidth() {
return cardWidth;
}
public final int getCardHeight() {
return cardHeight;
}
public final int getSymbolWidth() {
return symbolWidth;
}
public final Point getCardLocation() {
Point p = getLocation();
p.x += cardXOffset;
p.y += cardYOffset;
return p;
}
public final CardView getCard() {
return this.gameCard;
}
@Override
public void setAlpha(float alpha) {
this.alpha = alpha;
}
@Override
public final float getAlpha() {
return alpha;
}
public final int getCardXOffset() {
return cardXOffset;
}
public final int getCardYOffset() {
return cardYOffset;
}
@Override
public final boolean isTapped() {
if (isPermanent) {
return ((PermanentView) gameCard).isTapped();
}
return false;
}
@Override
public final boolean isFlipped() {
if (isPermanent) {
return ((PermanentView) gameCard).isFlipped();
}
return false;
}
@Override
public final boolean isTransformed() {
if (isPermanent) {
if (gameCard.isTransformed()) {
return !this.transformed;
} else {
return this.transformed;
}
} else {
return this.transformed;
}
}
@Override
public void onBeginAnimation() {
animationInProgress = true;
}
@Override
public void onEndAnimation() {
animationInProgress = false;
}
/**
* Inheriting classes should implement update(CardView card) by using this.
* However, they should ALSO call repaint() after the superclass call to
* this function, that can't be done here as the overriders may need to do
* things both before and after this call before repainting.
*
* @param card
*/
@Override
public void update(CardView card) {
this.updateCard = card;
// Animation update
if (isPermanent && (card instanceof PermanentView)) {
boolean needsTapping = isTapped() != ((PermanentView) card).isTapped();
boolean needsFlipping = isFlipped() != ((PermanentView) card).isFlipped();
if (needsTapping || needsFlipping) {
Animation.tapCardToggle(this, this, needsTapping, needsFlipping);
}
if (needsTapping && ((PermanentView) card).isTapped()) {
AudioManager.playTapPermanent();
}
boolean needsTranforming = isTransformed() != card.isTransformed();
if (needsTranforming) {
Animation.transformCard(this, this, card.isTransformed());
}
}
// Update panel attributes
this.isChoosable = card.isChoosable();
this.isSelected = card.isSelected();
// Update art?
boolean mustUpdateArt
= (!gameCard.getName().equals(card.getName()))
|| (gameCard.isFaceDown() != card.isFaceDown());
// Set the new card
this.gameCard = card;
// Update tooltip text
String cardType = getType(card);
tooltipText.setText(getText(cardType, card));
// Update the image
if (mustUpdateArt) {
updateArtImage();
}
// Update transform circle
if (card.canTransform()) {
BufferedImage transformIcon;
if (isTransformed() || card.isTransformed()) {
transformIcon = ImageManagerImpl.instance.getNightImage();
} else {
transformIcon = ImageManagerImpl.instance.getDayImage();
}
if (dayNightButton != null) {
dayNightButton.setVisible(!isPermanent);
dayNightButton.setIcon(new ImageIcon(transformIcon));
}
}
}
@Override
public boolean contains(int x, int y) {
return containsThis(x, y, true);
}
public boolean containsThis(int x, int y, boolean root) {
Point component = getLocation();
int cx = getCardX() - component.x;
int cy = getCardY() - component.y;
int cw = cardWidth;
int ch = cardHeight;
if (isTapped()) {
cy = ch - cw + cx;
ch = cw;
cw = cardHeight;
}
return x >= cx && x <= cx + cw && y >= cy && y <= cy + ch;
}
@Override
public CardView getOriginal() {
return this.gameCard;
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
if (gameCard.hideInfo()) {
return;
}
if (!tooltipShowing) {
synchronized (this) {
if (!tooltipShowing) {
TransferData transferData = getTransferDataForMouseEntered();
if (this.isShowing()) {
tooltipShowing = true;
callback.mouseEntered(e, transferData);
}
}
}
}
}
@Override
public void mouseDragged(MouseEvent e) {
data.component = this;
callback.mouseDragged(e, data);
}
@Override
public void mouseMoved(MouseEvent e) {
if (gameCard.hideInfo()) {
return;
}
data.component = this;
callback.mouseMoved(e, data);
}
@Override
public void mouseExited(MouseEvent e) {
if (gameCard.hideInfo()) {
return;
}
if (this.contains(e.getPoint())) {
return;
}
if (tooltipShowing) {
synchronized (this) {
if (tooltipShowing) {
tooltipShowing = false;
data.component = this;
data.card = this.gameCard;
data.popupText = tooltipText;
callback.mouseExited(e, data);
}
}
}
}
@Override
public void mousePressed(MouseEvent e) {
data.component = this;
data.card = this.gameCard;
data.gameId = this.gameId;
callback.mousePressed(e, data);
}
@Override
public void mouseReleased(MouseEvent e) {
callback.mouseReleased(e, data);
}
/**
* Prepares data to be sent to action callback on client side.
*
* @return
*/
private TransferData getTransferDataForMouseEntered() {
data.component = this;
data.card = this.gameCard;
data.popupText = tooltipText;
data.gameId = this.gameId;
data.locationOnScreen = data.component.getLocationOnScreen(); // we need this for popup
data.popupOffsetX = isTapped() ? cardHeight + cardXOffset + POPUP_X_GAP : cardWidth + cardXOffset + POPUP_X_GAP;
data.popupOffsetY = 40;
return data;
}
protected final String getType(CardView card) {
StringBuilder sbType = new StringBuilder();
for (SuperType superType : card.getSuperTypes()) {
sbType.append(superType.toString()).append(' ');
}
for (CardType cardType : card.getCardTypes()) {
sbType.append(cardType.toString()).append(' ');
}
if (!card.getSubTypes().isEmpty()) {
sbType.append("- ");
for (String subType : card.getSubTypes()) {
sbType.append(subType).append(' ');
}
}
return sbType.toString().trim();
}
protected final String getText(String cardType, CardView card) {
StringBuilder sb = new StringBuilder();
if (card instanceof StackAbilityView || card instanceof AbilityView) {
for (String rule : card.getRules()) {
sb.append('\n').append(rule);
}
} else {
sb.append(card.getName());
if (!card.getManaCost().isEmpty()) {
sb.append('\n').append(card.getManaCost());
}
sb.append('\n').append(cardType);
if (card.getColor().hasColor()) {
sb.append('\n').append(card.getColor().toString());
}
if (card.isCreature()) {
sb.append('\n').append(card.getPower()).append('/').append(card.getToughness());
} else if (card.isPlanesWalker()) {
sb.append('\n').append(card.getLoyalty());
}
if (card.getRules() == null) {
card.overrideRules(new ArrayList<>());
}
for (String rule : card.getRules()) {
sb.append('\n').append(rule);
}
if (card.getExpansionSetCode() != null && !card.getExpansionSetCode().isEmpty()) {
sb.append('\n').append(card.getCardNumber()).append(" - ");
sb.append(card.getExpansionSetCode()).append(" - ");
sb.append(card.getRarity().toString());
}
}
return sb.toString();
}
@Override
public void update(PermanentView card) {
this.hasSickness = card.hasSummoningSickness();
this.showCopySourceButton.setVisible(card.isCopy());
update((CardView) card);
}
@Override
public PermanentView getOriginalPermanent() {
if (isPermanent) {
return (PermanentView) this.gameCard;
}
throw new IllegalStateException("Is not permanent.");
}
@Override
public void updateCallback(ActionCallback callback, UUID gameId) {
this.callback = callback;
this.gameId = gameId;
}
public void setTransformed(boolean transformed) {
this.transformed = transformed;
}
@Override
public void toggleTransformed() {
this.transformed = !this.transformed;
if (transformed) {
if (dayNightButton != null) { // if transformbable card is copied, button can be null
BufferedImage night = ImageManagerImpl.instance.getNightImage();
dayNightButton.setIcon(new ImageIcon(night));
}
if (this.gameCard.getSecondCardFace() == null) {
LOGGER.error("no second side for card to transform!");
return;
}
if (!isPermanent) { // use only for custom transformation (when pressing day-night button)
this.temporary = this.gameCard;
update(this.gameCard.getSecondCardFace());
}
} else {
if (dayNightButton != null) { // if transformbable card is copied, button can be null
BufferedImage day = ImageManagerImpl.instance.getDayImage();
dayNightButton.setIcon(new ImageIcon(day));
}
if (!isPermanent) { // use only for custom transformation (when pressing day-night button)
update(this.temporary);
this.temporary = null;
}
}
String temp = this.gameCard.getAlternateName();
this.gameCard.setAlternateName(this.gameCard.getOriginalName());
this.gameCard.setOriginalName(temp);
updateArtImage();
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (gameCard.hideInfo()) {
return;
}
data.component = this;
callback.mouseWheelMoved(e, data);
}
public JPanel getCardArea() {
return cardArea;
}
@Override
public void componentResized(ComponentEvent ce) {
doLayout();
// this update removes the isChoosable mark from targetCardsInLibrary
// so only done for permanents because it's needed to redraw counters in different size, if window size was changed
// no perfect solution yet (maybe also other not wanted effects for PermanentView objects)
if (updateCard != null && (updateCard instanceof PermanentView)) {
update(updateCard);
}
}
@Override
public void componentMoved(ComponentEvent ce) {
}
@Override
public void componentShown(ComponentEvent ce) {
}
@Override
public void componentHidden(ComponentEvent ce) {
}
@Override
public void setTextOffset(int yOffset) {
yTextOffset = yOffset;
}
public int getTextOffset() {
return yTextOffset;
}
@Override
public JPopupMenu getPopupMenu() {
return popupMenu;
}
@Override
public void setPopupMenu(JPopupMenu popupMenu) {
this.popupMenu = popupMenu;
}
}