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 dab74a0751c..11912aade90 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 @@ -217,12 +217,16 @@ public class CardPanelRenderImpl extends CardPanel { } } + // Map of generated images private final static Map IMAGE_CACHE = new MapMaker().softValues().makeMap(); // The art image for the card, loaded in from the disk private BufferedImage artImage; + // Factory to generate card appropriate views + private CardRendererFactory cardRendererFactory = new CardRendererFactory(); + // The rendered card image, with or without the art image loaded yet // = null while invalid private BufferedImage cardImage; @@ -233,7 +237,7 @@ public class CardPanelRenderImpl extends CardPanel { super(newGameCard, gameId, loadImage, callback, foil, dimension); // Renderer - cardRenderer = new ModernCardRenderer(gameCard, isTransformed()); + cardRenderer = cardRendererFactory.create(gameCard, isTransformed()); // Draw the parts initialDraw(); @@ -269,6 +273,10 @@ public class CardPanelRenderImpl extends CardPanel { g.drawImage(cardImage, getCardXOffset(), getCardYOffset(), null); } + /** + * Create an appropriate card renderer for the + */ + /** * Render the card to a new BufferedImage at it's current dimensions * @@ -359,7 +367,7 @@ public class CardPanelRenderImpl extends CardPanel { // Update renderer cardImage = null; - cardRenderer = new ModernCardRenderer(gameCard, isTransformed()); + cardRenderer = cardRendererFactory.create(gameCard, isTransformed()); cardRenderer.setArtImage(artImage); // Repaint diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardRenderer.java index 08ab2f30349..d20580a53e7 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardRenderer.java @@ -15,8 +15,11 @@ 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 @@ -121,20 +124,24 @@ public abstract class CardRenderer { this.cardView = card; this.isTransformed = isTransformed; + parseRules(card.getRules(), textboxKeywords, textboxRules); + } + + protected void parseRules(List stringRules, ArrayList keywords, ArrayList rules) { // Translate the textbox text - for (String rule : card.getRules()) { + 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(card, rule); + TextboxRule tbRule = TextboxRuleParser.parse(cardView, rule); if (tbRule.type == TextboxRuleType.SIMPLE_KEYWORD) { - textboxKeywords.add(tbRule); + keywords.add(tbRule); } else if (tbRule.text.isEmpty()) { // Nothing to do, rule is empty } else { - textboxRules.add(tbRule); + rules.add(tbRule); } } } @@ -254,6 +261,46 @@ public abstract class CardRenderer { } } + protected void drawArtIntoRect(Graphics2D g, int x, int y, int w, int h, Rectangle2D artRect, boolean noAspectAdjust) { + // 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 (noAspectAdjust) { + // 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); + if (noAspectAdjust) { + g.drawImage(subImg, + borderWidth, borderWidth, + cardWidth - 2 * borderWidth, cardHeight - 2 * borderWidth, + null); + } else { + 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); diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererFactory.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererFactory.java new file mode 100644 index 00000000000..f738321cb36 --- /dev/null +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererFactory.java @@ -0,0 +1,20 @@ +package org.mage.card.arcane; + +import mage.view.CardView; + +/** + * Created by StravantUser on 2017-03-30. + */ +public class CardRendererFactory { + public CardRendererFactory() { + + } + + public CardRenderer create(CardView card, boolean isTransformed) { + if (card.isSplitCard()) { + return new ModernSplitCardRenderer(card, isTransformed); + } else { + return new ModernCardRenderer(card, isTransformed); + } + } +} diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererUtils.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererUtils.java index e5b6242dd53..89a6a5d173a 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererUtils.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardRendererUtils.java @@ -57,7 +57,7 @@ public final class CardRendererUtils { // Draw a rounded box with a 2-pixel border // Used on various card parts. - public static void drawRoundedBox(Graphics2D g, int x, int y, int w, int h, int bevel, Paint border, Color fill) { + public static void drawRoundedBox(Graphics2D g, int x, int y, int w, int h, int bevel, Paint border, Paint fill) { g.setColor(new Color(0, 0, 0, 150)); g.drawOval(x - 1, y - 1, bevel * 2, h); g.setPaint(border); @@ -67,7 +67,7 @@ public final class CardRendererUtils { g.drawOval(x + 1 + w - bevel * 2, y + 1, bevel * 2 - 3, h - 3); g.drawRect(x + bevel, y, w - 2 * bevel, h - 1); g.drawRect(x + 1 + bevel, y + 1, w - 2 * bevel - 2, h - 3); - g.setColor(fill); + g.setPaint(fill); g.fillOval(x + 2, y + 2, bevel * 2 - 4, h - 4); g.fillOval(x + 2 + w - bevel * 2, y + 2, bevel * 2 - 4, h - 4); g.fillRect(x + bevel, y + 2, w - 2 * bevel, h - 4); 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 fd843418975..da54a60e780 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 @@ -346,45 +346,10 @@ public class ModernCardRenderer extends CardRenderer { @Override protected void drawArt(Graphics2D g) { if (artImage != null && !cardView.isFaceDown()) { - Rectangle2D artRect = getArtRect(); - - // 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 = contentWidth - 2; - double targetHeight = typeLineY - totalContentInset - boxHeight; - double targetAspect = targetWidth / targetHeight; - if (useInventionFrame()) { - // 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); - if (useInventionFrame()) { - g.drawImage(subImg, - borderWidth, borderWidth, - cardWidth - 2 * borderWidth, cardHeight - 2 * borderWidth, - null); - } else { - g.drawImage(subImg, - totalContentInset + 1, totalContentInset + boxHeight, - (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 - } + drawArtIntoRect(g, + totalContentInset + 1, totalContentInset + boxHeight, + contentWidth - 2, typeLineY - totalContentInset - boxHeight, + getArtRect(), useInventionFrame()); } } @@ -478,17 +443,17 @@ public class ModernCardRenderer extends CardRenderer { int nameOffset = drawTransformationCircle(g, borderPaint); // Draw the name line - drawNameLine(g, + drawNameLine(g, cardView.getName(), manaCostString, totalContentInset + nameOffset, totalContentInset, contentWidth - nameOffset, boxHeight); // Draw the type line - drawTypeLine(g, + drawTypeLine(g, getCardTypeLine(), totalContentInset, typeLineY, contentWidth, boxHeight); // Draw the textbox rules - drawRulesText(g, + drawRulesText(g, textboxKeywords, textboxRules, totalContentInset + 2, typeLineY + boxHeight + 2, contentWidth - 4, cardHeight - typeLineY - boxHeight - 4 - borderWidth * 3); @@ -497,13 +462,13 @@ public class ModernCardRenderer extends CardRenderer { } // Draw the name line - protected void drawNameLine(Graphics2D g, int x, int y, int w, int h) { + protected void drawNameLine(Graphics2D g, String baseName, String manaCost, int x, int y, int w, int h) { // Width of the mana symbols int manaCostWidth; if (cardView.isAbility()) { manaCostWidth = 0; } else { - manaCostWidth = CardRendererUtils.getManaCostWidth(manaCostString, boxTextHeight); + manaCostWidth = CardRendererUtils.getManaCostWidth(manaCost, boxTextHeight); } // Available width for name. Add a little bit of slop so that one character @@ -519,7 +484,7 @@ public class ModernCardRenderer extends CardRenderer { nameStr = "Morph: " + cardView.getName(); } } else { - nameStr = cardView.getName(); + nameStr = baseName; } if (!nameStr.isEmpty()) { AttributedString str = new AttributedString(nameStr); @@ -541,12 +506,12 @@ public class ModernCardRenderer extends CardRenderer { // Draw the mana symbols if (!cardView.isAbility() && !cardView.isFaceDown()) { - ManaSymbols.draw(g, manaCostString, x + w - manaCostWidth, y + boxTextOffset, boxTextHeight); + ManaSymbols.draw(g, manaCost, x + w - manaCostWidth, y + boxTextOffset, boxTextHeight); } } // Draw the type line (color indicator, types, and expansion symbol) - protected void drawTypeLine(Graphics2D g, int x, int y, int w, int h) { + protected void drawTypeLine(Graphics2D g, String baseTypeLine, int x, int y, int w, int h) { // Draw expansion symbol int expansionSymbolWidth; if (PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_RENDERING_SET_SYMBOL, "false").equals("false")) { @@ -561,7 +526,7 @@ public class ModernCardRenderer extends CardRenderer { // Draw type line text int availableWidth = w - expansionSymbolWidth + 1; - String types = getCardTypeLine(); + String types = baseTypeLine; g.setFont(boxTextFont); // Replace "Legendary" in type line if there's not enough space @@ -583,7 +548,7 @@ public class ModernCardRenderer extends CardRenderer { if (breakIndex > 0) { TextLayout layout = measure.getLayout(0, breakIndex); g.setColor(getBoxTextColor()); - layout.draw(g, x, y + boxTextOffset + boxTextHeight - 1); + layout.draw(g, x, y + (h - boxTextHeight) / 2 + boxTextHeight - 1); } } } @@ -760,13 +725,13 @@ public class ModernCardRenderer extends CardRenderer { return layout; } - protected void drawRulesText(Graphics2D g, int x, int y, int w, int h) { + protected void drawRulesText(Graphics2D g, ArrayList keywords, ArrayList rules, int x, int y, int w, int h) { // Gather all rules to render - List allRules = new ArrayList<>(textboxRules); + List allRules = new ArrayList<>(rules); // Add the keyword rule if there are any keywords - if (!textboxKeywords.isEmpty()) { - String keywordRulesString = getKeywordRulesString(); + if (!keywords.isEmpty()) { + String keywordRulesString = getKeywordRulesString(keywords); TextboxRule keywordsRule = new TextboxRule(keywordRulesString, new ArrayList<>()); allRules.add(0, keywordsRule); } @@ -828,11 +793,11 @@ public class ModernCardRenderer extends CardRenderer { } // Get the first line of the textbox, the keyword string - private String getKeywordRulesString() { + private static String getKeywordRulesString(ArrayList keywords) { StringBuilder builder = new StringBuilder(); - for (int i = 0; i < textboxKeywords.size(); ++i) { - builder.append(textboxKeywords.get(i).text); - if (i != textboxKeywords.size() - 1) { + for (int i = 0; i < keywords.size(); ++i) { + builder.append(keywords.get(i).text); + if (i != keywords.size() - 1) { builder.append(", "); } } diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java new file mode 100644 index 00000000000..a446d4818d7 --- /dev/null +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java @@ -0,0 +1,324 @@ +package org.mage.card.arcane; + +import mage.ObjectColor; +import mage.abilities.costs.mana.ManaCosts; +import mage.constants.CardType; +import mage.view.CardView; + +import java.awt.*; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by StravantUser on 2017-03-30. + */ +public class ModernSplitCardRenderer extends ModernCardRenderer { + + private class HalfCardProps { + int x, y, w, h, cw, ch; + + String name; + String manaCostString; + ObjectColor color; + ArrayList rules = new ArrayList<>(); + ArrayList keywords = new ArrayList<>(); + } + + private static ArrayList ONLY_LAND_TYPE = new ArrayList() {{add(CardType.LAND);}}; + + // Right and left halves of the card content + private HalfCardProps rightHalf = new HalfCardProps(); + private HalfCardProps leftHalf = new HalfCardProps(); + + // Where and how big is the divider between the card halves + private int dividerAt; + private int dividerSize; + + // Is fuse / consequence + private boolean isFuse = false; + private boolean isConsequence = false; + + public ModernSplitCardRenderer(CardView view, boolean isTransformed) { + super(view, isTransformed); + + rightHalf.manaCostString = ManaSymbols.getStringManaCost(cardView.getRightSplitCosts().getSymbols()); + leftHalf.manaCostString = ManaSymbols.getStringManaCost(cardView.getLeftSplitCosts().getSymbols()); + + rightHalf.color = getColorFromManaCostHack(cardView.getRightSplitCosts()); + leftHalf.color = getColorFromManaCostHack(cardView.getLeftSplitCosts()); + + parseRules(view.getRightSplitRules(), rightHalf.keywords, rightHalf.rules); + parseRules(view.getLeftSplitRules(), leftHalf.keywords, leftHalf.rules); + + rightHalf.name = cardView.getRightSplitName(); + leftHalf.name = cardView.getLeftSplitName(); + + isConsequence = cardView.getName().equalsIgnoreCase("fire // ice"); + for (String rule: view.getRules()) { + if (rule.contains("Fuse")) { + isFuse = true; + break; + } + } + + // It's easier for rendering to swap the card halves here because for consequence cards + // they "rotate" in opposite directions making consquence and normal split cards + // have the "right" vs "left" as the top half. + if (!isConsequence()) { + HalfCardProps tmp = leftHalf; + leftHalf = rightHalf; + rightHalf = tmp; + } + } + + private boolean isConsequence() { + return isConsequence; + } + + private boolean isFuse() { + return isFuse; + } + + @Override + protected void layout(int cardWidth, int cardHeight) { + // Pass to parent + super.layout(cardWidth, cardHeight); + + // Decide size of divider + if (isConsequence()) { + dividerSize = borderWidth; + dividerAt = (int)(cardHeight*0.54); + } else { + int availHeight = cardHeight - totalContentInset - 3*borderWidth; + dividerSize = borderWidth*2; + dividerAt = (int)(totalContentInset + availHeight * 0.5 - borderWidth); + } + + // Decide size of each halves box + rightHalf.x = leftHalf.x = totalContentInset; + rightHalf.w = leftHalf.w = cardWidth - 2*totalContentInset; + leftHalf.y = totalContentInset; + leftHalf.h = dividerAt - totalContentInset; + rightHalf.y = dividerAt + dividerSize; + rightHalf.h = cardHeight - rightHalf.y - borderWidth*3; + + // Content width / height (Exchanged from width / height if the card part is rotated) + if (isConsequence()) { + leftHalf.cw = leftHalf.w; + leftHalf.ch = leftHalf.h; + } else { + leftHalf.cw = leftHalf.h; + leftHalf.ch = leftHalf.w; + } + rightHalf.cw = rightHalf.h; + rightHalf.ch = rightHalf.w; + + // Fuse space + if (isFuse()) { + rightHalf.ch -= boxHeight; + leftHalf.ch -= boxHeight; + } + } + + // Ugly hack used here because the card database doesn't actually store color + // for each half of split cards separately. + private ObjectColor getColorFromManaCostHack(ManaCosts costs) { + ObjectColor c = new ObjectColor(); + List symbols = costs.getSymbols(); + for (String symbol: symbols) { + if (symbol.contains("W")) { + c.setWhite(true); + } else if (symbol.contains("U")) { + c.setBlue(true); + } else if (symbol.contains("B")) { + c.setBlack(true); + } else if (symbol.contains("R")) { + c.setRed(true); + } else if (symbol.contains("G")) { + c.setGreen(true); + } + } + return c; + } + + @Override + protected void drawBackground(Graphics2D g) { + if (cardView.isFaceDown()) { + drawCardBack(g); + } else { + { // Left half background (top of the card) + // Set texture to paint the left with + g.setPaint(getBackgroundPaint(leftHalf.color, cardView.getCardTypes(), cardView.getSubTypes())); + + // Draw main part (most of card) + g.fillRoundRect( + borderWidth, borderWidth, + cardWidth - 2*borderWidth, leftHalf.h + contentInset - borderWidth - 2*cornerRadius + (cornerRadius - 1), + cornerRadius - 1, cornerRadius - 1); + + // Draw the M15 rounded "swoosh" at the bottom + g.fillRoundRect( + borderWidth, dividerAt - borderWidth - 4*cornerRadius, + cardWidth - 2*borderWidth, cornerRadius * 4, + cornerRadius * 2, cornerRadius * 2); + + // Draw the cutout into the "swoosh" for the textbox to lie over + g.fillRect( + borderWidth + contentInset, dividerAt - 2*borderWidth, + cardWidth - borderWidth * 2 - contentInset * 2, borderWidth * 2); + } + + { // Right half background (bottom half of the card) + // Set texture to paint the right with + g.setPaint(getBackgroundPaint(rightHalf.color, cardView.getCardTypes(), cardView.getSubTypes())); + + // Draw the M15 rounded "swoosh"es at the top and bottom + g.fillRoundRect( + borderWidth, dividerAt + dividerSize + borderWidth, + cardWidth - 2*borderWidth, rightHalf.h - 2*borderWidth, + cornerRadius*2, cornerRadius*2); + + // Draw the cutout into the "swoosh" for the textbox to lie over + g.fillRect( + borderWidth + contentInset, dividerAt + dividerSize, + cardWidth - borderWidth * 2 - contentInset * 2, rightHalf.h); + } + } + } + + @Override + protected void drawArt(Graphics2D g) { + if (artImage != null && !cardView.isFaceDown()) { + if (isConsequence()) { + Rectangle2D topRect = new Rectangle2D.Double(0.075, 0.113, 0.832, 0.227); + int topLineY = (int) (leftHalf.ch * TYPE_LINE_Y_FRAC); + drawArtIntoRect(g, + leftHalf.x, leftHalf.y + boxHeight, leftHalf.cw, topLineY - boxHeight, + topRect, false); + + Rectangle2D bottomRect = new Rectangle2D.Double(0.546, 0.562, 0.272, 0.346); + int bottomLineY = (rightHalf.ch - boxHeight) / 2; + drawArtIntoRect(g, + rightHalf.x + rightHalf.w - bottomLineY, rightHalf.y, bottomLineY - boxHeight, rightHalf.h, + bottomRect, false); + + } else { + Rectangle2D topRect = new Rectangle2D.Double(0.152, 0.058, 0.386, 0.400); + int topLineY = (int) (leftHalf.ch * TYPE_LINE_Y_FRAC); + drawArtIntoRect(g, + leftHalf.x + boxHeight, leftHalf.y, topLineY - boxHeight, leftHalf.h, + topRect, false); + + Rectangle2D bottomRect = new Rectangle2D.Double(0.152, 0.539, 0.386, 0.400); + int bottomLineY = (int) (rightHalf.ch * TYPE_LINE_Y_FRAC); + drawArtIntoRect(g, + rightHalf.x + boxHeight, rightHalf.y, bottomLineY - boxHeight, rightHalf.h, + bottomRect, false); + } + } + } + + protected void drawSplitHalfFrame(Graphics2D g, HalfCardProps half, int typeLineY) { + // Get the border paint + Color boxColor = getBoxColor(half.color, cardView.getCardTypes(), isTransformed); + Paint textboxPaint = getTextboxPaint(half.color, cardView.getCardTypes(), cardWidth); + Paint borderPaint = getBorderPaint(half.color, cardView.getCardTypes(), cardWidth); + + // Draw main frame + g.setPaint(borderPaint); + g.drawRect( + 0, 0, + half.cw - 1, half.ch - 1); + + // Background of textbox + g.setPaint(textboxPaint); + g.fillRect( + 1, typeLineY, + half.cw - 2, half.ch - typeLineY - 1); + + // Draw the name line box + CardRendererUtils.drawRoundedBox(g, + -borderWidth, 0, + half.cw + 2 * borderWidth, boxHeight, + contentInset, + borderPaint, boxColor); + + // Draw the type line box + CardRendererUtils.drawRoundedBox(g, + -borderWidth, typeLineY, + half.cw + 2 * borderWidth, boxHeight - 4, + contentInset, + borderPaint, boxColor); + + // Draw the name line + drawNameLine(g, half.name, half.manaCostString, + 0, 0, + half.cw, boxHeight); + + // Draw the type line + drawTypeLine(g, getCardTypeLine(), + 0, typeLineY, + half.cw, boxHeight - 4); + + // Draw the textbox rules + drawRulesText(g, half.keywords, half.rules, + 2, typeLineY + boxHeight + 2 - 4, + half.cw - 4, half.ch - typeLineY - boxHeight); + } + + private Graphics2D getUnmodifiedHalfContext(Graphics2D g) { + Graphics2D g2 = (Graphics2D)g.create(); + g2.translate(leftHalf.x, leftHalf.y); + return g2; + } + + private Graphics2D getConsequenceHalfContext(Graphics2D g) { + Graphics2D g2 = (Graphics2D)g.create(); + g2.translate(rightHalf.x, rightHalf.y); + g2.rotate(Math.PI / 2); + g2.translate(0, -rightHalf.w); + return g2; + } + + private Graphics2D getLeftHalfContext(Graphics2D g) { + Graphics2D g2 = (Graphics2D)g.create(); + g2.translate(leftHalf.x, leftHalf.y); + g2.rotate(-Math.PI / 2); + g2.translate(-leftHalf.cw, 0); + return g2; + } + + private Graphics2D getRightHalfContext(Graphics2D g) { + Graphics2D g2 = (Graphics2D)g.create(); + g2.translate(rightHalf.x, rightHalf.y); + g2.rotate(-Math.PI / 2); + g2.translate(-rightHalf.cw, 0); + return g2; + } + + @Override + protected void drawFrame(Graphics2D g) { + if (isConsequence()) { + drawSplitHalfFrame(getUnmodifiedHalfContext(g), leftHalf, (int)(leftHalf.ch * TYPE_LINE_Y_FRAC)); + drawSplitHalfFrame(getConsequenceHalfContext(g), rightHalf, (rightHalf.ch - boxHeight) / 2); + } else { + drawSplitHalfFrame(getLeftHalfContext(g), leftHalf, (int)(leftHalf.ch * TYPE_LINE_Y_FRAC)); + drawSplitHalfFrame(getRightHalfContext(g), rightHalf, (int)(rightHalf.ch * TYPE_LINE_Y_FRAC)); + if (isFuse()) { + Graphics2D g2 = getRightHalfContext(g); + int totalFuseBoxWidth = rightHalf.cw * 2 + 2 * borderWidth + dividerSize; + Paint boxColor = getTextboxPaint(cardView.getColor(), ONLY_LAND_TYPE, totalFuseBoxWidth); + Paint borderPaint = getBorderPaint(cardView.getColor(), ONLY_LAND_TYPE, totalFuseBoxWidth); + CardRendererUtils.drawRoundedBox(g2, + -borderWidth, rightHalf.ch, + totalFuseBoxWidth, boxHeight, + contentInset, + borderPaint, boxColor); + drawNameLine(g2, "Fuse (You may cast both halves from your hand)", "", + 0, rightHalf.ch, + totalFuseBoxWidth - 2*borderWidth, boxHeight); + } + } + } +} diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRule.java b/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRule.java index 093f5418462..9dafbcb32bd 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRule.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRule.java @@ -74,6 +74,9 @@ public class TextboxRule { private final List regions; protected TextboxRule(String text, List regions, TextboxRuleType type) { + if (text.isEmpty()) { + throw new IllegalArgumentException("Empty rule"); + } this.text = text; this.type = type; this.regions = regions; diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRuleParser.java b/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRuleParser.java index 63c91b1490c..004ed7c7843 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRuleParser.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRuleParser.java @@ -13,6 +13,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import mage.view.CardView; import org.apache.log4j.Logger; +import org.apache.log4j.jmx.LoggerDynamicMBean; /** * 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 cc75bff9cfc..44e4f2183d1 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 @@ -115,7 +115,7 @@ public class CardPluginImpl implements CardPlugin { */ 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 (view.isSplitCard() || fallback.equals("true")) { + if (fallback.equals("true")) { return new CardPanelComponentImpl(view, gameId, loadImage, callback, isFoil, dimension); } else { return new CardPanelRenderImpl(view, gameId, loadImage, callback, isFoil, dimension); diff --git a/Mage.Sets/src/mage/cards/d/DuskDawn.java b/Mage.Sets/src/mage/cards/d/DuskDawn.java new file mode 100644 index 00000000000..ff9ddace2f3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DuskDawn.java @@ -0,0 +1,123 @@ +/* + * 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.d; + +import mage.abilities.Ability; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.*; +import mage.abilities.keyword.AftermathAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.SplitCard; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.Filter; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.game.Game; +import mage.players.Player; + +import java.util.Set; +import java.util.UUID; + +/** + * + * @author stravant + */ + + +public class DuskDawn extends SplitCard { + private static final FilterCreaturePermanent filterCreatures3orGreater = new FilterCreaturePermanent("creatures with power greater than or equal to 3"); + static { + filterCreatures3orGreater.add(new PowerPredicate(Filter.ComparisonType.GreaterThan, 2)); + } + + public DuskDawn(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{2}{W}{W}","{3}{W}{W}",false); + + // Dusk + // Destroy all creatures with power 3 or greater. + Effect destroy = new DestroyAllEffect(filterCreatures3orGreater); + destroy.setText("Destroy all creatures with power greater than or equal to 3."); + getLeftHalfCard().getSpellAbility().addEffect(destroy); + + // Dawn + // Return all creature cards with power less than or equal to 2 from your graveyard to your hand. + ((CardImpl)(getRightHalfCard())).addAbility(new AftermathAbility()); + getRightHalfCard().getSpellAbility().addEffect(new DawnEffect()); + + } + + public DuskDawn(final DuskDawn card) { + super(card); + } + + @Override + public DuskDawn copy() { + return new DuskDawn(this); + } +} + +class DawnEffect extends OneShotEffect { + + private static final FilterCard filter2orLess = new FilterCreatureCard("creatures with power less than or equal to 2"); + static { + filter2orLess.add(new PowerPredicate(Filter.ComparisonType.LessThan, 3)); + } + + DawnEffect() { + super(Outcome.Benefit); + this.staticText = "Return all creature cards with power 2 or less from your graveyard to your hand."; + } + + DawnEffect(final DawnEffect effect) { + super(effect); + } + + @Override + public DawnEffect copy() { + return new DawnEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player != null) { + Set cards = player.getGraveyard().getCards(filter2orLess, game); + player.moveCards(cards, Zone.HAND, source, game); + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/Amonkhet.java b/Mage.Sets/src/mage/sets/Amonkhet.java index ccc9987dc15..6845178e17f 100644 --- a/Mage.Sets/src/mage/sets/Amonkhet.java +++ b/Mage.Sets/src/mage/sets/Amonkhet.java @@ -28,6 +28,7 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.constants.Rarity; import mage.constants.SetType; /** @@ -51,5 +52,6 @@ public class Amonkhet extends ExpansionSet { this.numBoosterUncommon = 3; this.numBoosterRare = 1; this.ratioBoosterMythic = 8; + cards.add(new SetCardInfo("Dusk // Dawn", 210, Rarity.RARE, mage.cards.d.DuskDawn.class)); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/DuskDawnTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/DuskDawnTest.java new file mode 100644 index 00000000000..dd40cd4c054 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/DuskDawnTest.java @@ -0,0 +1,79 @@ +package org.mage.test.cards.single.akh; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author Quercitron + */ +public class DuskDawnTest extends CardTestPlayerBase { + + @Test + public void testCastDusk() { + addCard(Zone.BATTLEFIELD, playerB, "Tarmogoyf"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + addCard(Zone.HAND, playerA, "Dusk // Dawn"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dusk // Dawn"); + } + + @Test + public void testCastDawnFail() { + + } + + @Test + public void testCastDawnFromGraveyard() { + + } + + /* + @Test + public void testThatNoncreatureSpellsCannotBeCast() { + addCard(Zone.BATTLEFIELD, playerA, "Hope of Ghirapur"); + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.HAND, playerB, "Shock"); + + attack(1, playerA, "Hope of Ghirapur"); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Sacrifice", playerB); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Shock", playerA); + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 19); + assertPermanentCount(playerA, "Hope of Ghirapur", 0); + } + + // Test that ability cannot be activated if after damage Hope of Ghirapur was removed + // from the battlefield and returned back. + @Test + public void testWhenHopeOfGhirapurWasRemovedAndReturnedBack() { + addCard(Zone.BATTLEFIELD, playerA, "Hope of Ghirapur"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.HAND, playerA, "Cloudshift"); + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.HAND, playerB, "Shock"); + + attack(1, playerA, "Hope of Ghirapur"); + castSpell(1, PhaseStep.END_COMBAT, playerA, "Cloudshift", "Hope of Ghirapur"); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Sacrifice", playerB); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Shock", playerA); + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 18); + assertLife(playerB, 19); + assertPermanentCount(playerA, "Hope of Ghirapur", 1); + } + */ + +} diff --git a/Mage/src/main/java/mage/abilities/keyword/AftermathAbility.java b/Mage/src/main/java/mage/abilities/keyword/AftermathAbility.java new file mode 100644 index 00000000000..1f231523b2b --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/AftermathAbility.java @@ -0,0 +1,154 @@ +/* + * Copyright 2011 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.keyword; + +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.Costs; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.effects.*; +import mage.cards.Card; +import mage.cards.SplitCard; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; +import org.junit.After; + +import java.util.UUID; + +/** + * Aftermath + * + * TODO: Implement once we get details on the comprehensive rules meaning of the ability + * + * Current text is a shell copied from Flashback + * + * @author stravant + */ +public class AftermathAbility extends SimpleStaticAbility { + public AftermathAbility() { + super(Zone.ALL, new AftermathCantCastFromHand()); + addEffect(new AftermathCastFromGraveyard()); + } + + public AftermathAbility(final AftermathAbility ability) { + super(ability); + } + + @Override + public AftermathAbility copy() { + return new AftermathAbility(this); + } + + @Override + public String getRule(boolean all) { + if (all) { + return "Aftermath (Cast this card only from your graveyard. Exile it afterwards.)"; + } else { + return "Aftermath"; + } + } +} + +class AftermathCastFromGraveyard extends AsThoughEffectImpl { + + public AftermathCastFromGraveyard() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit); + staticText = "Cast {this} from your graveyard"; + } + + public AftermathCastFromGraveyard(final AftermathCastFromGraveyard effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public AftermathCastFromGraveyard copy() { + return new AftermathCastFromGraveyard(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + if (objectId.equals(source.getSourceId()) && + affectedControllerId.equals(source.getControllerId())) { + Card card = game.getCard(source.getSourceId()); + if (card != null && game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { + return true; + } + } + return false; + } +} + +class AftermathCantCastFromHand extends ContinuousRuleModifyingEffectImpl { + + public AftermathCantCastFromHand() { + super(Duration.EndOfGame, Outcome.Detriment); + staticText = ", but not from anywhere else"; + } + + public AftermathCantCastFromHand (final AftermathCantCastFromHand effect) { + super(effect); + } + + @Override + public AftermathCantCastFromHand copy() { + return new AftermathCantCastFromHand(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CAST_SPELL; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Card card = game.getCard(event.getSourceId()); + if (card != null && card.getId().equals(source.getSourceId())) { + Zone zone = game.getState().getZone(card.getId()); + if (zone != null && (zone != Zone.GRAVEYARD)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index bf458ac1fae..befa16158ee 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -284,7 +284,11 @@ public abstract class CardImpl extends MageObjectImpl implements Card { return all; } - protected void addAbility(Ability ability) { + /** + * Public in order to support adding abilities to SplitCardHalf's + * @param ability + */ + public void addAbility(Ability ability) { ability.setSourceId(this.getId()); abilities.add(ability); for (Ability subAbility : ability.getSubAbilities()) { diff --git a/Mage/src/main/java/mage/cards/repository/CardRepository.java b/Mage/src/main/java/mage/cards/repository/CardRepository.java index 030fefbf9fe..8d308ecf843 100644 --- a/Mage/src/main/java/mage/cards/repository/CardRepository.java +++ b/Mage/src/main/java/mage/cards/repository/CardRepository.java @@ -61,7 +61,7 @@ public enum CardRepository { // raise this if db structure was changed private static final long CARD_DB_VERSION = 50; // raise this if new cards were added to the server - private static final long CARD_CONTENT_VERSION = 70; + private static final long CARD_CONTENT_VERSION = 71; private final TreeSet landTypes = new TreeSet<>(); private Dao cardDao; private Set classNames;