/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.mage.card.arcane;
import mage.cards.ArtRect;
import mage.client.dialog.PreferencesDialog;
import mage.constants.AbilityType;
import mage.constants.CardType;
import mage.constants.SuperType;
import mage.view.CardView;
import mage.view.CounterView;
import mage.view.PermanentView;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.RasterFormatException;
import java.util.ArrayList;
import java.util.List;
/**
* @author stravant@gmail.com
*
* Common base class for card renderers for each card frame / card type.
*
* Follows the template method pattern to implement a new renderer, implement
* the following methods (they are called in the following order):
*
* * drawBorder() Draws the outermost border of the card, white border or black
* border
*
* * drawBackground() Draws the background texture / color of the card
*
* * drawArt() Draws the card's art
*
* * drawFrame() Draws the card frame (over the art and background)
*
* * drawOverlays() Draws summoning sickness and possible other overlays
*
* * drawCounters() Draws counters on the card, such as +1/+1 and -1/-1
* counters
*
* Predefined methods that the implementations can use:
*
* * drawRules(font, bounding box)
*
* * drawNameLine(font, bounding box)
*
* * drawTypeLine(font, bounding box)
*
*/
public abstract class CardRenderer {
///////////////////////////////////////////////////////////////////////////
// Common layout metrics between all cards
// The card to be rendered
protected final CardView cardView;
// Is the card transformed?
protected final boolean isTransformed;
// The card image
protected BufferedImage artImage;
///////////////////////////////////////////////////////////////////////////
// Common layout metrics between all cards
// Polygons for counters
private static final Polygon PLUS_COUNTER_POLY = new Polygon(new int[]{
0, 5, 10, 10, 5, 0
}, new int[]{
3, 0, 3, 10, 9, 10
}, 6);
private static final Polygon MINUS_COUNTER_POLY = new Polygon(new int[]{
0, 5, 10, 10, 5, 0
}, new int[]{
0, 1, 0, 7, 10, 7
}, 6);
private static final Polygon TIME_COUNTER_POLY = new Polygon(new int[]{
0, 10, 8, 10, 0, 2
}, new int[]{
0, 0, 5, 10, 10, 5
}, 6);
private static final Polygon OTHER_COUNTER_POLY = new Polygon(new int[]{
1, 9, 9, 1
}, new int[]{
1, 1, 9, 9
}, 4);
// Paint for a card back
public static final Paint BG_TEXTURE_CARDBACK = new Color(153, 102, 51);
// The size of the card
protected int cardWidth;
protected int cardHeight;
// Is it selectable / selected
protected boolean isChoosable;
protected boolean isSelected;
// Radius of the corners of the cards
protected static final float CORNER_RADIUS_FRAC = 0.1f; //x cardWidth
protected static final int CORNER_RADIUS_MIN = 3;
protected int cornerRadius;
// The inset of the actual card from the black / white border around it
protected static final float BORDER_WIDTH_FRAC = 0.03f; //x cardWidth
protected static final float BORDER_WIDTH_MIN = 2;
protected int borderWidth;
// The parsed text of the card
protected final ArrayList<TextboxRule> textboxRules = new ArrayList<>();
protected final ArrayList<TextboxRule> textboxKeywords = new ArrayList<>();
// The Construtor
// The constructor should prepare all of the things that it can
// without knowing the dimensions that the card will be rendered at.
// Then, the CardRenderer can be called on multiple times to render the
// card at various sizes (for instance, during animation)
public CardRenderer(CardView card, boolean isTransformed) {
// Set base parameters
this.cardView = card;
this.isTransformed = isTransformed;
if (card.getArtRect() == ArtRect.SPLIT_FUSED) {
parseRules(card.getLeftSplitRules(), textboxKeywords, textboxRules);
parseRules(card.getRightSplitRules(), textboxKeywords, textboxRules);
} else {
parseRules(card.getRules(), textboxKeywords, textboxRules);
}
}
protected void parseRules(List<String> stringRules, ArrayList<TextboxRule> keywords, ArrayList<TextboxRule> rules) {
// Translate the textbox text
for (String rule : stringRules) {
// Kill reminder text
if (PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_RENDERING_REMINDER_TEXT, "false").equals("false")) {
rule = CardRendererUtils.killReminderText(rule).trim();
}
if (!rule.isEmpty()) {
TextboxRule tbRule = TextboxRuleParser.parse(cardView, rule);
if (tbRule.type == TextboxRuleType.SIMPLE_KEYWORD) {
keywords.add(tbRule);
} else if (tbRule.text.isEmpty()) {
// Nothing to do, rule is empty
} else {
rules.add(tbRule);
}
}
}
}
private static int getBorderWidth(int cardWidth) {
return (int) Math.max(
BORDER_WIDTH_MIN,
BORDER_WIDTH_FRAC * cardWidth);
}
// Layout operation
// Calculate common layout metrics that will be used by several
// of the operations in the template method.
protected void layout(int cardWidth, int cardHeight) {
// Store the dimensions for the template methods to use
this.cardWidth = cardWidth;
this.cardHeight = cardHeight;
// Corner radius and border width
cornerRadius = (int) Math.max(
CORNER_RADIUS_MIN,
CORNER_RADIUS_FRAC * cardWidth);
borderWidth = getBorderWidth(cardWidth);
}
/**
* How far does a card have to be spaced down from a rendered card to show
* it's entire name line? This function is a bit of a hack, as different
* card faces need slightly different spacing, but we need it in a static
* context so that spacing is consistent in GY / deck views etc.
*
* @param cardWidth
* @return
*/
public static int getCardTopHeight(int cardWidth) {
// Constants copied over from ModernCardRenderer and tweaked
float BOX_HEIGHT_FRAC = 0.065f; // x cardHeight
int BOX_HEIGHT_MIN = 16;
int boxHeight = (int) Math.max(
BOX_HEIGHT_MIN,
BOX_HEIGHT_FRAC * cardWidth * 1.4f);
int borderWidth = getBorderWidth(cardWidth);
return 2 * borderWidth + boxHeight;
}
// The Draw Method
// The draw method takes the information caculated by the constructor
// and uses it to draw to a concrete size of card and graphics.
public void draw(Graphics2D g, CardPanelAttributes attribs) {
// Pre template method layout, to calculate shared layout info
layout(attribs.cardWidth, attribs.cardHeight);
isSelected = attribs.isSelected;
isChoosable = attribs.isChoosable;
// Call the template methods
drawBorder(g);
drawBackground(g);
drawArt(g);
drawFrame(g);
if (!cardView.isAbility()) {
drawOverlays(g);
drawCounters(g);
}
}
// Template methods to be implemented by sub classes
// For instance, for the Modern vs Old border card frames
protected abstract void drawBorder(Graphics2D g);
protected abstract void drawBackground(Graphics2D g);
protected abstract void drawArt(Graphics2D g);
protected abstract void drawFrame(Graphics2D g);
// Template methods that are possible to override, but unlikely to be
// overridden.
// Draw the card back
protected void drawCardBack(Graphics2D g) {
g.setPaint(BG_TEXTURE_CARDBACK);
g.fillRect(borderWidth, borderWidth,
cardWidth - 2 * borderWidth, cardHeight - 2 * borderWidth);
}
// Draw summoning sickness overlay, and possibly other overlays
protected void drawOverlays(Graphics2D g) {
if (cardView.isCreature() && cardView instanceof PermanentView) {
if (((PermanentView) cardView).hasSummoningSickness()) {
int x1 = (int) (0.2 * cardWidth);
int x2 = (int) (0.8 * cardWidth);
int y1 = (int) (0.2 * cardHeight);
int y2 = (int) (0.8 * cardHeight);
int xPoints[] = {
x1, x2, x1, x2
};
int yPoints[] = {
y1, y1, y2, y2
};
g.setColor(new Color(255, 255, 255, 200));
g.setStroke(new BasicStroke(7));
g.drawPolygon(xPoints, yPoints, 4);
g.setColor(new Color(0, 0, 0, 200));
g.setStroke(new BasicStroke(5));
g.drawPolygon(xPoints, yPoints, 4);
g.setStroke(new BasicStroke(1));
int[] xPoints2 = {
x1, x2, cardWidth / 2
};
int[] yPoints2 = {
y1, y1, cardHeight / 2
};
g.setColor(new Color(0, 0, 0, 100));
g.fillPolygon(xPoints2, yPoints2, 3);
}
}
}
protected void drawArtIntoRect(Graphics2D g, int x, int y, int w, int h, Rectangle2D artRect, boolean shouldPreserveAspect) {
// Perform a process to make sure that the art is scaled uniformly to fill the frame, cutting
// off the minimum amount necessary to make it completely fill the frame without "squashing" it.
double fullCardImgWidth = artImage.getWidth();
double fullCardImgHeight = artImage.getHeight();
double artWidth = artRect.getWidth() * fullCardImgWidth;
double artHeight = artRect.getHeight() * fullCardImgHeight;
double targetWidth = w;
double targetHeight = h;
double targetAspect = targetWidth / targetHeight;
if (!shouldPreserveAspect) {
// No adjustment to art
} else if (targetAspect * artHeight < artWidth) {
// Trim off some width
artWidth = targetAspect * artHeight;
} else {
// Trim off some height
artHeight = artWidth / targetAspect;
}
try {
BufferedImage subImg
= artImage.getSubimage(
(int) (artRect.getX() * fullCardImgWidth), (int) (artRect.getY() * fullCardImgHeight),
(int) artWidth, (int) artHeight);
g.drawImage(subImg,
x, y,
(int) targetWidth, (int) targetHeight,
null);
} catch (RasterFormatException e) {
// At very small card sizes we may encounter a problem with rounding error making the rect not fit
}
}
// Draw +1/+1 and other counters
protected void drawCounters(Graphics2D g) {
int xPos = (int) (0.65 * cardWidth);
int yPos = (int) (0.15 * cardHeight);
if (cardView.getCounters() != null) {
for (CounterView v : cardView.getCounters()) {
// Don't render loyalty, we do that in the bottom corner
if (!v.getName().equals("loyalty")) {
Polygon p;
switch (v.getName()) {
case "+1/+1":
p = PLUS_COUNTER_POLY;
break;
case "-1/-1":
p = MINUS_COUNTER_POLY;
break;
case "time":
p = TIME_COUNTER_POLY;
break;
default:
p = OTHER_COUNTER_POLY;
break;
}
double scale = (0.1 * 0.25 * cardWidth);
Graphics2D g2 = (Graphics2D) g.create();
g2.translate(xPos, yPos);
g2.scale(scale, scale);
g2.setColor(Color.white);
g2.fillPolygon(p);
g2.setColor(Color.black);
g2.drawPolygon(p);
g2.setFont(new Font("Arial", Font.BOLD, 7));
String cstr = String.valueOf(v.getCount());
int strW = g2.getFontMetrics().stringWidth(cstr);
g2.drawString(cstr, 5 - strW / 2, 8);
g2.dispose();
yPos += ((int) (0.30 * cardWidth));
}
}
}
}
// Draw an expansion symbol, right justified, in a given region
// Return the width of the drawn symbol
protected int drawExpansionSymbol(Graphics2D g, int x, int y, int w, int h) {
// Draw the expansion symbol
Image setSymbol = ManaSymbols.getSetSymbolImage(cardView.getExpansionSetCode(), cardView.getRarity().getCode());
int setSymbolWidth;
if (setSymbol == null) {
// Don't draw anything when we don't have a set symbol
return 0;
/*
// Just draw the as a code
String code = cardView.getExpansionSetCode();
code = (code != null) ? code.toUpperCase() : "";
FontMetrics metrics = g.getFontMetrics();
setSymbolWidth = metrics.stringWidth(code);
if (cardView.getRarity() == Rarity.COMMON) {
g.setColor(Color.white);
} else {
g.setColor(Color.black);
}
g.fillRoundRect(
x + w - setSymbolWidth - 1, y + 2,
setSymbolWidth+2, h - 5,
5, 5);
g.setColor(getRarityColor());
g.drawString(code, x + w - setSymbolWidth, y + h - 3);
*/
} else {
// Draw the set symbol
int height = setSymbol.getHeight(null);
int scale = 1;
if (height != -1) {
while (height > h + 2) {
scale *= 2;
height /= 2;
}
}
setSymbolWidth = setSymbol.getWidth(null) / scale;
g.drawImage(setSymbol,
x + w - setSymbolWidth, y + (h - height) / 2,
setSymbolWidth, height,
null);
}
return setSymbolWidth;
}
private Color getRarityColor() {
switch (cardView.getRarity()) {
case RARE:
return new Color(255, 191, 0);
case UNCOMMON:
return new Color(192, 192, 192);
case MYTHIC:
return new Color(213, 51, 11);
case SPECIAL:
return new Color(204, 0, 255);
case BONUS:
return new Color(129, 228, 228);
case COMMON:
default:
return Color.black;
}
}
// Get a string representing the type line
protected String getCardTypeLine() {
if (cardView.isAbility()) {
if (cardView.getAbilityType() == AbilityType.TRIGGERED) {
return "Triggered Ability";
} else if (cardView.getAbilityType() == AbilityType.ACTIVATED) {
return "Activated Ability";
} else if (cardView.getAbilityType() == null) {
// TODO: Triggered abilities waiting to be put onto the stack have abilityType = null. Figure out why
return "Triggered Ability";
} else {
return "??? Ability";
}
} else {
StringBuilder sbType = new StringBuilder();
for (SuperType superType : cardView.getSuperTypes()) {
sbType.append(superType).append(' ');
}
for (CardType cardType : cardView.getCardTypes()) {
sbType.append(cardType.toString()).append(' ');
}
if (!cardView.getSubTypes().isEmpty()) {
sbType.append("- ");
for (String subType : cardView.getSubTypes()) {
sbType.append(subType).append(' ');
}
}
return sbType.toString();
}
}
// Set the card art image (CardPanel will give it to us when it
// is loaded and ready)
public void setArtImage(Image image) {
artImage = CardRendererUtils.toBufferedImage(image);
}
}