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..26ed83ca988 --- /dev/null +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java @@ -0,0 +1,319 @@ +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 / aftermath + private boolean isFuse = false; + private boolean isAftermath = 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(); + + isFuse = view.getRules().stream().anyMatch(rule -> rule.contains("Fuse")); + isAftermath = view.getRightSplitRules().stream().anyMatch(rule -> rule.contains("Aftermath")); + + // It's easier for rendering to swap the card halves here because for aftermath cards + // they "rotate" in opposite directions making consquence and normal split cards + // have the "right" vs "left" as the top half. + if (!isAftermath()) { + HalfCardProps tmp = leftHalf; + leftHalf = rightHalf; + rightHalf = tmp; + } + } + + private boolean isAftermath() { + return isAftermath; + } + + 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 (isAftermath()) { + 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 (isAftermath()) { + 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 (isAftermath()) { + 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 getAftermathHalfContext(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 (isAftermath()) { + drawSplitHalfFrame(getUnmodifiedHalfContext(g), leftHalf, (int)(leftHalf.ch * TYPE_LINE_Y_FRAC)); + drawSplitHalfFrame(getAftermathHalfContext(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 394df55cfe8..7753654af9e 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 @@ -116,7 +116,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/DestinedLead.java b/Mage.Sets/src/mage/cards/d/DestinedLead.java new file mode 100644 index 00000000000..b7808914601 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DestinedLead.java @@ -0,0 +1,86 @@ +/* + * 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.effects.Effect; +import mage.abilities.effects.common.combat.MustBeBlockedByAllTargetEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.AftermathAbility; +import mage.abilities.keyword.IndestructibleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.SplitCard; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * + * @author stravant + */ + + +public class DestinedLead extends SplitCard { + + public DestinedLead(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.INSTANT}, new CardType[]{CardType.SORCERY},"{1}{B}","{3}{G}",false); + + // Destined + // Target creature gets +1/+0 and gains indestructible until end of turn. + getLeftHalfCard().getSpellAbility().addTarget(new TargetCreaturePermanent()); + Effect effect = new BoostTargetEffect(1, 0, Duration.EndOfTurn); + effect.setText("Target creature gets +1/+0"); + getLeftHalfCard().getSpellAbility().addEffect(effect); + + effect = new GainAbilityTargetEffect(IndestructibleAbility.getInstance(), Duration.EndOfTurn); + effect.setText("and gains indestructible until end of turn"); + getLeftHalfCard().getSpellAbility().addEffect(effect); + + // to + + // Lead + // All creatures able to block target creature this turn must do so. + ((CardImpl)(getRightHalfCard())).addAbility(new AftermathAbility()); + getRightHalfCard().getSpellAbility().addTarget(new TargetCreaturePermanent()); + getRightHalfCard().getSpellAbility().addEffect(new MustBeBlockedByAllTargetEffect(Duration.EndOfTurn)); + } + + public DestinedLead(final DestinedLead card) { + super(card); + } + + @Override + public DestinedLead copy() { + return new DestinedLead(this); + } +} + 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/cards/o/OnwardVictory.java b/Mage.Sets/src/mage/cards/o/OnwardVictory.java new file mode 100644 index 00000000000..3dc2f5b1a1d --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OnwardVictory.java @@ -0,0 +1,99 @@ +/* + * 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.o; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.dynamicvalue.common.TargetPermanentPowerCount; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.*; +import mage.abilities.effects.common.combat.MustBeBlockedByAllTargetEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.AftermathAbility; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.abilities.keyword.IndestructibleAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.SplitCard; +import mage.constants.CardType; +import mage.constants.Duration; +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 mage.target.common.TargetCreaturePermanent; + +import java.util.Set; +import java.util.UUID; + +/** + * + * @author stravant + */ + + +public class OnwardVictory extends SplitCard { + + public OnwardVictory(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.INSTANT}, new CardType[]{CardType.SORCERY},"{2}{R}","{2}{W}",false); + + // Onward + // Target creature gets +X/+0 until end of turn where X is its power. + getLeftHalfCard().getSpellAbility().addTarget(new TargetCreaturePermanent()); + getLeftHalfCard().getSpellAbility().addEffect(new BoostTargetEffect(new TargetPermanentPowerCount(), new StaticValue(0), Duration.EndOfTurn, true)); + + // to + + // Victory + // Target creature gains double strike until end of turn. + ((CardImpl)(getRightHalfCard())).addAbility(new AftermathAbility()); + Effect effect = new GainAbilityTargetEffect(DoubleStrikeAbility.getInstance(), Duration.EndOfTurn); + getRightHalfCard().getSpellAbility().addEffect(effect); + getRightHalfCard().getSpellAbility().addTarget(new TargetCreaturePermanent()); + } + + public OnwardVictory(final OnwardVictory card) { + super(card); + } + + @Override + public OnwardVictory copy() { + return new OnwardVictory(this); + } +} + diff --git a/Mage.Sets/src/mage/cards/p/PreparedFight.java b/Mage.Sets/src/mage/cards/p/PreparedFight.java new file mode 100644 index 00000000000..386fed956cc --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PreparedFight.java @@ -0,0 +1,109 @@ +/* + * 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.p; + +import mage.abilities.Ability; +import mage.abilities.condition.common.OpponentControlsPermanentCondition; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.*; +import mage.abilities.effects.common.combat.MustBeBlockedByAllTargetEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.AftermathAbility; +import mage.abilities.keyword.IndestructibleAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.SplitCard; +import mage.constants.*; +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.filter.predicate.permanent.ControllerPredicate; +import mage.game.Game; +import mage.players.Player; +import mage.target.Target; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.Set; +import java.util.UUID; + +/** + * + * @author stravant + */ + + +public class PreparedFight extends SplitCard { + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature you don't control"); + static { + filter.add(new ControllerPredicate(TargetController.NOT_YOU)); + } + + public PreparedFight(UUID ownerId, CardSetInfo setInfo) { + super(ownerId,setInfo,new CardType[]{CardType.INSTANT}, new CardType[]{CardType.SORCERY},"{1}{W}","{3}{G}",false); + + // Prepared + // Untap target creature. It gets +2/+2 and gains lifelink until end of turn. + getLeftHalfCard().getSpellAbility().addTarget(new TargetCreaturePermanent()); + Effect effect = new UntapTargetEffect(); + effect.setText("Untap target creature."); + getLeftHalfCard().getSpellAbility().addEffect(effect); + effect = new BoostTargetEffect(2, 2, Duration.EndOfTurn); + effect.setText("It gets +2/+2"); + getLeftHalfCard().getSpellAbility().addEffect(effect); + effect = new GainAbilityTargetEffect(LifelinkAbility.getInstance(), Duration.EndOfTurn); + effect.setText("and gains lifelink until end of turn."); + getLeftHalfCard().getSpellAbility().addEffect(effect); + + // to + + // Fight + // Target creature you control fights target creature you don't control. + ((CardImpl)(getRightHalfCard())).addAbility(new AftermathAbility()); + getRightHalfCard().getSpellAbility().addEffect(new FightTargetsEffect()); + getRightHalfCard().getSpellAbility().addTarget(new TargetControlledCreaturePermanent()); + Target target = new TargetCreaturePermanent(filter); + getRightHalfCard().getSpellAbility().addTarget(target); + } + + public PreparedFight(final PreparedFight card) { + super(card); + } + + @Override + public PreparedFight copy() { + return new PreparedFight(this); + } +} diff --git a/Mage.Sets/src/mage/sets/Amonkhet.java b/Mage.Sets/src/mage/sets/Amonkhet.java index 162a6ed4659..8d87bbae9f4 100644 --- a/Mage.Sets/src/mage/sets/Amonkhet.java +++ b/Mage.Sets/src/mage/sets/Amonkhet.java @@ -77,9 +77,11 @@ public class Amonkhet extends ExpansionSet { cards.add(new SetCardInfo("Crocodile of the Crossing", 162, Rarity.UNCOMMON, mage.cards.c.CrocodileOfTheCrossing.class)); cards.add(new SetCardInfo("Cursed Minotaur", 85, Rarity.COMMON, mage.cards.c.CursedMinotaur.class)); cards.add(new SetCardInfo("Decision Paralysis", 50, Rarity.COMMON, mage.cards.d.DecisionParalysis.class)); + cards.add(new SetCardInfo("Destined // Lead", 217, Rarity.UNCOMMON, mage.cards.d.DestinedLead.class)); cards.add(new SetCardInfo("Djeru's Resolve", 11, Rarity.COMMON, mage.cards.d.DjerusResolve.class)); cards.add(new SetCardInfo("Drake Haven", 51, Rarity.RARE, mage.cards.d.DrakeHaven.class)); cards.add(new SetCardInfo("Dune Beetle", 89, Rarity.COMMON, mage.cards.d.DuneBeetle.class)); + cards.add(new SetCardInfo("Dusk // Dawn", 210, Rarity.RARE, mage.cards.d.DuskDawn.class)); cards.add(new SetCardInfo("Essence Scatter", 52, Rarity.COMMON, mage.cards.e.EssenceScatter.class)); cards.add(new SetCardInfo("Exemplar of Strength", 165, Rarity.UNCOMMON, mage.cards.e.ExemplarOfStrength.class)); cards.add(new SetCardInfo("Fetid Pools", 243, Rarity.RARE, mage.cards.f.FetidPools.class)); @@ -115,11 +117,13 @@ public class Amonkhet extends ExpansionSet { cards.add(new SetCardInfo("Mountain", 265, Rarity.LAND, mage.cards.basiclands.Mountain.class, new CardGraphicInfo(null, true))); cards.add(new SetCardInfo("Mountain", 266, Rarity.LAND, mage.cards.basiclands.Mountain.class, new CardGraphicInfo(null, true))); cards.add(new SetCardInfo("Oketra's Monument", 233, Rarity.UNCOMMON, mage.cards.o.OketrasMonument.class)); + cards.add(new SetCardInfo("Onward // Victory", 218, Rarity.UNCOMMON, mage.cards.o.OnwardVictory.class)); cards.add(new SetCardInfo("Oracle's Vault", 234, Rarity.RARE, mage.cards.o.OraclesVault.class)); cards.add(new SetCardInfo("Plains", 250, Rarity.LAND, mage.cards.basiclands.Plains.class, new CardGraphicInfo(null, true))); cards.add(new SetCardInfo("Plains", 255, Rarity.LAND, mage.cards.basiclands.Plains.class, new CardGraphicInfo(null, true))); cards.add(new SetCardInfo("Plains", 256, Rarity.LAND, mage.cards.basiclands.Plains.class, new CardGraphicInfo(null, true))); cards.add(new SetCardInfo("Plains", 257, Rarity.LAND, mage.cards.basiclands.Plains.class, new CardGraphicInfo(null, true))); + cards.add(new SetCardInfo("Prepared // Fight", 220, Rarity.RARE, mage.cards.p.PreparedFight.class)); cards.add(new SetCardInfo("Prowling Serpopard", 180, Rarity.RARE, mage.cards.p.ProwlingSerpopard.class)); cards.add(new SetCardInfo("Renewed Faith", 25, Rarity.UNCOMMON, mage.cards.r.RenewedFaith.class)); cards.add(new SetCardInfo("Rhonas's Monument", 236, Rarity.UNCOMMON, mage.cards.r.RhonassMonument.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..6dd662ff135 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/akh/DuskDawnTest.java @@ -0,0 +1,85 @@ +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() { + //Cast dusk from hand + addCard(Zone.BATTLEFIELD, playerB, "Watchwolf"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.HAND, playerA, "Dusk // Dawn"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dusk"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTappedCount("Plains", true, 4); // check that we paid the right side's mana + assertPermanentCount(playerB, "Watchwolf", 0); + assertGraveyardCount(playerB, "Watchwolf", 1); + assertGraveyardCount(playerA, "Dusk // Dawn", 1); + } + + @Test + public void testCastDuskFromGraveyardFail() { + //Fail to cast dusk from graveyard + addCard(Zone.BATTLEFIELD, playerB, "Watchwolf"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + addCard(Zone.GRAVEYARD, playerA, "Dusk // Dawn"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dusk"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, "Watchwolf", 1); + assertGraveyardCount(playerB, "Watchwolf", 0); + assertGraveyardCount(playerA, "Dusk // Dawn", 1); + } + + @Test + public void testCastDawnFromGraveyard() { + addCard(Zone.GRAVEYARD, playerA, "Dusk // Dawn"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.GRAVEYARD, playerA, "Devoted Hero"); + addCard(Zone.GRAVEYARD, playerA, "Watchwolf"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dawn"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // Dusk dawn should have been cast and exiled + // devoted hero should be in the hand + // watchwolf should still be in the yard + assertHandCount(playerA, "Devoted Hero", 1); + assertGraveyardCount(playerA, "Devoted Hero", 0); + assertGraveyardCount(playerA, "Watchwolf", 1); + assertExileCount(playerA, "Dusk // Dawn", 1); + assertGraveyardCount(playerA, "Dusk // Dawn", 0); + } + + @Test + public void testCastDawnFail() { + // Fail to cast dawn from hand + addCard(Zone.HAND, playerA, "Dusk // Dawn"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.GRAVEYARD, playerA, "Devoted Hero"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dawn"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + // Dusk dawn shouldn't have been cast and devoted hero should still be in the yard + assertHandCount(playerA, "Dusk // Dawn", 1); + assertGraveyardCount(playerA, "Devoted Hero", 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..c704939285a --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/AftermathAbility.java @@ -0,0 +1,227 @@ +/* + * 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.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.*; +import mage.cards.Card; +import mage.cards.SplitCardHalf; +import mage.cards.SplitCardHalfImpl; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.stack.Spell; +import mage.players.Player; +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 AftermathCastFromGraveyard()); + addEffect(new AftermathCantCastFromHand()); + addEffect(new AftermathExileAsResolvesFromGraveyard()); + } + + 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); + } + + private static String msb(UUID id) { + return Integer.toUnsignedString((int)id.getMostSignificantBits(), 16); + } + + @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; + } +} + +class AftermathExileAsResolvesFromGraveyard extends ReplacementEffectImpl { + + AftermathExileAsResolvesFromGraveyard() { + super(Duration.WhileOnStack, Outcome.Detriment); + this.staticText = "Exile it afterwards."; + } + + AftermathExileAsResolvesFromGraveyard(AftermathExileAsResolvesFromGraveyard effect) { + super(effect); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean applies(GameEvent evt, Ability source, Game game) { + ZoneChangeEvent event = (ZoneChangeEvent) evt; + if (event.getFromZone() == Zone.STACK && event.getToZone() != Zone.EXILED) { + // Moving something from stack to somewhere else + + // Get the source id, getting the whole split card's ID, because + // that's the card that is changing zones in the event, but + // source.getSourceId is only the split card half. + // If branch so that we also support putting Aftermath on + // non-split cards for... whatever reason, in case somebody + // wants to do that in the future. + UUID sourceId = source.getSourceId(); + Card sourceCard = game.getCard(source.getSourceId()); + if (sourceCard != null && sourceCard instanceof SplitCardHalf) { + sourceCard = ((SplitCardHalf) sourceCard).getParentCard(); + sourceId = sourceCard.getId(); + } + + if (event.getTargetId() == sourceId) { + // Moving this spell from stack to yard + Spell spell = game.getStack().getSpell(source.getSourceId()); + if (spell != null && spell.getFromZone() == Zone.GRAVEYARD) { + // And this spell was cast from the graveyard, so we need to exile it + return true; + } + } + } + return false; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + UUID sourceId = source.getSourceId(); + Card sourceCard = game.getCard(source.getSourceId()); + if (sourceCard != null && sourceCard instanceof SplitCardHalf) { + sourceCard = ((SplitCardHalf) sourceCard).getParentCard(); + sourceId = sourceCard.getId(); + } + + if (sourceCard != null) { + Player player = game.getPlayer(sourceCard.getOwnerId()); + if (player != null) { + return player.moveCardToExileWithInfo(sourceCard, null, "", sourceId, game, ((ZoneChangeEvent)event).getFromZone(), true); + } + } + return false; + } + + @Override + public AftermathExileAsResolvesFromGraveyard copy() { + return new AftermathExileAsResolvesFromGraveyard(this); + } + +} \ 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/SplitCard.java b/Mage/src/main/java/mage/cards/SplitCard.java index 8a29fad7876..6785f213bbe 100644 --- a/Mage/src/main/java/mage/cards/SplitCard.java +++ b/Mage/src/main/java/mage/cards/SplitCard.java @@ -28,8 +28,11 @@ package mage.cards; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.UUID; + +import com.sun.deploy.util.ArrayUtil; import mage.abilities.Abilities; import mage.abilities.AbilitiesImpl; import mage.abilities.Ability; @@ -49,10 +52,14 @@ public abstract class SplitCard extends CardImpl { protected Card rightHalfCard; public SplitCard(UUID ownerId, CardSetInfo setInfo, CardType[] cardTypes, String costsLeft, String costsRight, boolean fused) { - super(ownerId, setInfo, cardTypes, costsLeft + costsRight, (fused ? SpellAbilityType.SPLIT_FUSED : SpellAbilityType.SPLIT)); + this(ownerId, setInfo, cardTypes, cardTypes, costsLeft, costsRight, fused); + } + + public SplitCard(UUID ownerId, CardSetInfo setInfo, CardType[] typesLeft, CardType[] typesRight, String costsLeft, String costsRight, boolean fused) { + super(ownerId, setInfo, CardType.mergeTypes(typesLeft, typesRight), costsLeft + costsRight, (fused ? SpellAbilityType.SPLIT_FUSED : SpellAbilityType.SPLIT)); String[] names = setInfo.getName().split(" // "); - leftHalfCard = new SplitCardHalfImpl(this.getOwnerId(), new CardSetInfo(names[0], setInfo.getExpansionSetCode(), setInfo.getCardNumber(), setInfo.getRarity(), setInfo.getGraphicInfo()), cardTypes, costsLeft, this, SpellAbilityType.SPLIT_LEFT); - rightHalfCard = new SplitCardHalfImpl(this.getOwnerId(), new CardSetInfo(names[1], setInfo.getExpansionSetCode(), setInfo.getCardNumber(), setInfo.getRarity(), setInfo.getGraphicInfo()), cardTypes, costsRight, this, SpellAbilityType.SPLIT_RIGHT); + leftHalfCard = new SplitCardHalfImpl(this.getOwnerId(), new CardSetInfo(names[0], setInfo.getExpansionSetCode(), setInfo.getCardNumber(), setInfo.getRarity(), setInfo.getGraphicInfo()), typesLeft, costsLeft, this, SpellAbilityType.SPLIT_LEFT); + rightHalfCard = new SplitCardHalfImpl(this.getOwnerId(), new CardSetInfo(names[1], setInfo.getExpansionSetCode(), setInfo.getCardNumber(), setInfo.getRarity(), setInfo.getGraphicInfo()), typesRight, costsRight, this, SpellAbilityType.SPLIT_RIGHT); this.splitCard = true; } @@ -139,6 +146,14 @@ public abstract class SplitCard extends CardImpl { return allAbilites; } + /** + * Currently only gets the fuse SpellAbility if there is one, but generally gets + * any abilities on a split card as a whole, and not on either half individually. + **/ + public Abilities getSharedAbilities() { + return super.getAbilities(); + } + @Override public Abilities getAbilities(Game game) { Abilities allAbilites = new AbilitiesImpl<>(); diff --git a/Mage/src/main/java/mage/cards/SplitCardHalf.java b/Mage/src/main/java/mage/cards/SplitCardHalf.java index a0593f22d53..6baee1c2949 100644 --- a/Mage/src/main/java/mage/cards/SplitCardHalf.java +++ b/Mage/src/main/java/mage/cards/SplitCardHalf.java @@ -15,4 +15,6 @@ public interface SplitCardHalf extends Card { SplitCardHalf copy(); void setParentCard(SplitCard card); + + SplitCard getParentCard(); } diff --git a/Mage/src/main/java/mage/cards/SplitCardHalfImpl.java b/Mage/src/main/java/mage/cards/SplitCardHalfImpl.java index 0dcb22f2d66..bf8e5a40eee 100644 --- a/Mage/src/main/java/mage/cards/SplitCardHalfImpl.java +++ b/Mage/src/main/java/mage/cards/SplitCardHalfImpl.java @@ -82,4 +82,8 @@ public class SplitCardHalfImpl extends CardImpl implements SplitCardHalf { this.splitCardParent = card; } + @Override + public SplitCard getParentCard() { + return this.splitCardParent; + } } diff --git a/Mage/src/main/java/mage/cards/mock/MockSplitCardHalf.java b/Mage/src/main/java/mage/cards/mock/MockSplitCardHalf.java index c6d677679d4..633b630bdbf 100644 --- a/Mage/src/main/java/mage/cards/mock/MockSplitCardHalf.java +++ b/Mage/src/main/java/mage/cards/mock/MockSplitCardHalf.java @@ -55,4 +55,9 @@ public class MockSplitCardHalf extends MockCard implements SplitCardHalf { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } + @Override + public SplitCard getParentCard() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + } diff --git a/Mage/src/main/java/mage/cards/repository/CardRepository.java b/Mage/src/main/java/mage/cards/repository/CardRepository.java index 030fefbf9fe..1131a20ea2a 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 = 74; private final TreeSet landTypes = new TreeSet<>(); private Dao cardDao; private Set classNames; diff --git a/Mage/src/main/java/mage/constants/CardType.java b/Mage/src/main/java/mage/constants/CardType.java index fbbf0f7ed85..29e19bc8cb5 100644 --- a/Mage/src/main/java/mage/constants/CardType.java +++ b/Mage/src/main/java/mage/constants/CardType.java @@ -1,5 +1,9 @@ package mage.constants; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashSet; + /** * * @author North @@ -26,4 +30,15 @@ public enum CardType { return text; } + /** + * Returns all of the card types from two lists of card types. + * Duplicates are eliminated. + */ + public static CardType[] mergeTypes(CardType[] a, CardType[] b) { + EnumSet cardTypes = EnumSet.noneOf(CardType.class); + cardTypes.addAll(Arrays.asList(a)); + cardTypes.addAll(Arrays.asList(b)); + return cardTypes.toArray(new CardType[0]); + } + } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 92b406257bf..dae06986196 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1246,29 +1246,30 @@ public abstract class PlayerImpl implements Player, Serializable { return useable; } - @Override - public LinkedHashMap getUseableActivatedAbilities(MageObject object, Zone zone, Game game) { - LinkedHashMap useable = new LinkedHashMap<>(); + // Get the usable activated abilities for a *single card object*, that is, either a card or half of a split card. + // Also called on the whole split card but only passing the fuse ability and other whole-split-card shared abilities + // as candidates. + private void getUseableActivatedAbilitiesHalfImpl(MageObject object, Zone zone, Game game, Abilities candidateAbilites, LinkedHashMap output) { boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game); ManaOptions availableMana = null; -// ManaOptions availableMana = getManaAvailable(game); // can only be activated if mana calculation works flawless otherwise player can't play spells they could play if calculation would work correctly -// availableMana.addMana(manaPool.getMana()); - for (Ability ability : object.getAbilities()) { + // ManaOptions availableMana = getManaAvailable(game); // can only be activated if mana calculation works flawless otherwise player can't play spells they could play if calculation would work correctly + // availableMana.addMana(manaPool.getMana()); + for (Ability ability : candidateAbilites) { if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { if (ability.getZone().match(zone)) { if (ability instanceof ActivatedAbility) { if (ability instanceof ActivatedManaAbilityImpl) { if (((ActivatedAbility) ability).canActivate(playerId, game)) { - useable.put(ability.getId(), (ActivatedAbility) ability); + output.put(ability.getId(), (ActivatedAbility) ability); } } else if (canPlay(((ActivatedAbility) ability), availableMana, object, game)) { - useable.put(ability.getId(), (ActivatedAbility) ability); + output.put(ability.getId(), (ActivatedAbility) ability); } } else if (ability instanceof AlternativeSourceCosts) { if (object.isLand()) { for (Ability ability2 : object.getAbilities().copy()) { if (ability2 instanceof PlayLandAbility) { - useable.put(ability2.getId(), (ActivatedAbility) ability2); + output.put(ability2.getId(), (ActivatedAbility) ability2); } } } @@ -1278,19 +1279,19 @@ public abstract class PlayerImpl implements Player, Serializable { } if (zone != Zone.HAND) { if (Zone.GRAVEYARD == zone && canPlayCardsFromGraveyard()) { - for (ActivatedAbility ability : object.getAbilities().getPlayableAbilities(Zone.HAND)) { + for (ActivatedAbility ability : candidateAbilites.getPlayableAbilities(Zone.HAND)) { if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { if (ability.getManaCosts().isEmpty() && ability.getCosts().isEmpty() && ability instanceof SpellAbility) { continue; // You can't play spells from graveyard that have no costs } if (ability.canActivate(playerId, game)) { - useable.put(ability.getId(), ability); + output.put(ability.getId(), ability); } } } } if (zone != Zone.BATTLEFIELD && game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this.getId(), game)) { - for (Ability ability : object.getAbilities()) { + for (Ability ability : candidateAbilites) { if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { if (ability.getManaCosts().isEmpty() && ability.getCosts().isEmpty() && ability instanceof SpellAbility && !(Objects.equals(ability.getSourceId(), getCastSourceIdWithAlternateMana()))) { continue; // You can't play spells that have no costs, unless you can play them without paying their mana costs @@ -1298,12 +1299,25 @@ public abstract class PlayerImpl implements Player, Serializable { ability.setControllerId(this.getId()); if (ability instanceof ActivatedAbility && ability.getZone().match(Zone.HAND) && ((ActivatedAbility) ability).canActivate(playerId, game)) { - useable.put(ability.getId(), (ActivatedAbility) ability); + output.put(ability.getId(), (ActivatedAbility) ability); } } } } } + } + + @Override + public LinkedHashMap getUseableActivatedAbilities(MageObject object, Zone zone, Game game) { + LinkedHashMap useable = new LinkedHashMap<>(); + if (object instanceof SplitCard) { + SplitCard splitCard = (SplitCard) object; + getUseableActivatedAbilitiesHalfImpl(splitCard.getLeftHalfCard(), zone, game, splitCard.getLeftHalfCard().getAbilities(), useable); + getUseableActivatedAbilitiesHalfImpl(splitCard.getRightHalfCard(), zone, game, splitCard.getRightHalfCard().getAbilities(), useable); + getUseableActivatedAbilitiesHalfImpl(splitCard, zone, game, splitCard.getSharedAbilities(), useable); + } else { + getUseableActivatedAbilitiesHalfImpl(object, zone, game, object.getAbilities(), useable); + } getOtherUseableActivatedAbilities(object, zone, game, useable); return useable; @@ -2596,6 +2610,23 @@ public abstract class PlayerImpl implements Player, Serializable { return false; } + private void getPlayableFromGraveyardCard(Game game, Card card, Abilities candidateAbilities, ManaOptions availableMana, List output) { + boolean asThoughtCast = game.getContinuousEffects().asThough(card.getId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this.getId(), game); + for (ActivatedAbility ability : candidateAbilities.getActivatedAbilities(Zone.ALL)) { + boolean possible = false; + if (ability.getZone().match(Zone.GRAVEYARD)) { + possible = true; + } else if (ability.getZone().match(Zone.HAND) && (ability instanceof SpellAbility || ability instanceof PlayLandAbility)) { + if (asThoughtCast || canPlayCardsFromGraveyard()) { + possible = true; + } + } + if (possible && canPlay(ability, availableMana, card, game)) { + output.add(ability); + } + } + } + @Override public List getPlayable(Game game, boolean hidden) { List playable = new ArrayList<>(); @@ -2636,20 +2667,17 @@ public abstract class PlayerImpl implements Player, Serializable { } } for (Card card : graveyard.getUniqueCards(game)) { - boolean asThoughtCast = game.getContinuousEffects().asThough(card.getId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this.getId(), game); - for (ActivatedAbility ability : card.getAbilities().getActivatedAbilities(Zone.ALL)) { - boolean possible = false; - if (ability.getZone().match(Zone.GRAVEYARD)) { - possible = true; - } else if (ability.getZone().match(Zone.HAND) && (ability instanceof SpellAbility || ability instanceof PlayLandAbility)) { - if (asThoughtCast || canPlayCardsFromGraveyard()) { - possible = true; - } - } - if (possible && canPlay(ability, availableMana, card, game)) { - playable.add(ability); - } + // Handle split cards in graveyard to support Aftermath + if (card instanceof SplitCard) { + SplitCard splitCard = (SplitCard) card; + getPlayableFromGraveyardCard(game, splitCard.getLeftHalfCard(), splitCard.getLeftHalfCard().getAbilities(), availableMana, playable); + getPlayableFromGraveyardCard(game, splitCard.getRightHalfCard(), splitCard.getRightHalfCard().getAbilities(), availableMana, playable); + getPlayableFromGraveyardCard(game, splitCard, splitCard.getSharedAbilities(), availableMana, playable); + } else { + getPlayableFromGraveyardCard(game, card, card.getAbilities(), availableMana, playable); } + + // Other activated abilities LinkedHashMap useable = new LinkedHashMap<>(); getOtherUseableActivatedAbilities(card, Zone.GRAVEYARD, game, useable); for (Ability ability : useable.values()) {