From a96a7f89f501ff978a1ccb3b5d57fcc8e67f6b27 Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Mon, 3 Apr 2017 04:15:25 -0600 Subject: [PATCH 1/4] Work in progress changes for Amonket Aftermath ability. * Card Rendering has support for Aftermath Split card frames * Card Rendering has support for Split cards * Aftermath ability work in progress --- .../mage/card/arcane/CardPanelRenderImpl.java | 12 +- .../org/mage/card/arcane/CardRenderer.java | 55 ++- .../mage/card/arcane/CardRendererFactory.java | 20 ++ .../mage/card/arcane/CardRendererUtils.java | 4 +- .../mage/card/arcane/ModernCardRenderer.java | 79 ++--- .../card/arcane/ModernSplitCardRenderer.java | 324 ++++++++++++++++++ .../org/mage/card/arcane/TextboxRule.java | 3 + .../mage/card/arcane/TextboxRuleParser.java | 1 + .../org/mage/plugins/card/CardPluginImpl.java | 2 +- Mage.Sets/src/mage/cards/d/DuskDawn.java | 123 +++++++ Mage.Sets/src/mage/sets/Amonkhet.java | 2 + .../test/cards/single/akh/DuskDawnTest.java | 79 +++++ .../abilities/keyword/AftermathAbility.java | 154 +++++++++ Mage/src/main/java/mage/cards/CardImpl.java | 6 +- .../mage/cards/repository/CardRepository.java | 2 +- 15 files changed, 798 insertions(+), 68 deletions(-) create mode 100644 Mage.Client/src/main/java/org/mage/card/arcane/CardRendererFactory.java create mode 100644 Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java create mode 100644 Mage.Sets/src/mage/cards/d/DuskDawn.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/akh/DuskDawnTest.java create mode 100644 Mage/src/main/java/mage/abilities/keyword/AftermathAbility.java 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; From 18663f0a7ac2b3ff127938a330600941aa2d1fef Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Tue, 4 Apr 2017 00:29:54 -0600 Subject: [PATCH 2/4] Amonket Aftermath ability and card frame changes Completed * Aftermath Ability implementation complete (At least until we see comprehensive rules that contradict the way I assumed it will work) * Aftermath Card Frame rendering complete * Normal Split and Fuse Split card frame rendering complete * Amonket Split card CMC changes NOT made, but left for a separate commit --- .../card/arcane/ModernSplitCardRenderer.java | 31 +++--- Mage.Sets/src/mage/cards/d/DestinedLead.java | 101 ++++++++++++++++++ Mage.Sets/src/mage/sets/Amonkhet.java | 1 + .../abilities/keyword/AftermathAbility.java | 95 ++++++++++++++-- Mage/src/main/java/mage/cards/SplitCard.java | 21 +++- .../main/java/mage/cards/SplitCardHalf.java | 2 + .../java/mage/cards/SplitCardHalfImpl.java | 4 + .../mage/cards/mock/MockSplitCardHalf.java | 5 + .../mage/cards/repository/CardRepository.java | 2 +- .../main/java/mage/constants/CardType.java | 17 +++ .../main/java/mage/players/PlayerImpl.java | 40 ++++--- 11 files changed, 278 insertions(+), 41 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/d/DestinedLead.java 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 index a446d4818d7..5e449b7702d 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java @@ -35,9 +35,9 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { private int dividerAt; private int dividerSize; - // Is fuse / consequence + // Is fuse / aftermath private boolean isFuse = false; - private boolean isConsequence = false; + private boolean isAftermath = false; public ModernSplitCardRenderer(CardView view, boolean isTransformed) { super(view, isTransformed); @@ -54,26 +54,31 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { 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; } } + for (String rule: view.getRightSplitRules()) { + if (rule.contains("Aftermath")) { + isAftermath = true; + break; + } + } - // It's easier for rendering to swap the card halves here because for consequence cards + // 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 (!isConsequence()) { + if (!isAftermath()) { HalfCardProps tmp = leftHalf; leftHalf = rightHalf; rightHalf = tmp; } } - private boolean isConsequence() { - return isConsequence; + private boolean isAftermath() { + return isAftermath; } private boolean isFuse() { @@ -86,7 +91,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { super.layout(cardWidth, cardHeight); // Decide size of divider - if (isConsequence()) { + if (isAftermath()) { dividerSize = borderWidth; dividerAt = (int)(cardHeight*0.54); } else { @@ -104,7 +109,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { rightHalf.h = cardHeight - rightHalf.y - borderWidth*3; // Content width / height (Exchanged from width / height if the card part is rotated) - if (isConsequence()) { + if (isAftermath()) { leftHalf.cw = leftHalf.w; leftHalf.ch = leftHalf.h; } else { @@ -190,7 +195,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { @Override protected void drawArt(Graphics2D g) { if (artImage != null && !cardView.isFaceDown()) { - if (isConsequence()) { + 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, @@ -273,7 +278,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { return g2; } - private Graphics2D getConsequenceHalfContext(Graphics2D g) { + private Graphics2D getAftermathHalfContext(Graphics2D g) { Graphics2D g2 = (Graphics2D)g.create(); g2.translate(rightHalf.x, rightHalf.y); g2.rotate(Math.PI / 2); @@ -299,9 +304,9 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { @Override protected void drawFrame(Graphics2D g) { - if (isConsequence()) { + if (isAftermath()) { drawSplitHalfFrame(getUnmodifiedHalfContext(g), leftHalf, (int)(leftHalf.ch * TYPE_LINE_Y_FRAC)); - drawSplitHalfFrame(getConsequenceHalfContext(g), rightHalf, (rightHalf.ch - boxHeight) / 2); + 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)); 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..44c708b8bc6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DestinedLead.java @@ -0,0 +1,101 @@ +/* + * 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.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.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 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(LifelinkAbility.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/sets/Amonkhet.java b/Mage.Sets/src/mage/sets/Amonkhet.java index 6845178e17f..d449648cde2 100644 --- a/Mage.Sets/src/mage/sets/Amonkhet.java +++ b/Mage.Sets/src/mage/sets/Amonkhet.java @@ -53,5 +53,6 @@ public class Amonkhet extends ExpansionSet { this.numBoosterRare = 1; this.ratioBoosterMythic = 8; cards.add(new SetCardInfo("Dusk // Dawn", 210, Rarity.RARE, mage.cards.d.DuskDawn.class)); + cards.add(new SetCardInfo("Destined // Lead", 217, Rarity.UNCOMMON, mage.cards.d.DestinedLead.class)); } } diff --git a/Mage/src/main/java/mage/abilities/keyword/AftermathAbility.java b/Mage/src/main/java/mage/abilities/keyword/AftermathAbility.java index 1f231523b2b..c704939285a 100644 --- a/Mage/src/main/java/mage/abilities/keyword/AftermathAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/AftermathAbility.java @@ -27,23 +27,19 @@ */ package mage.abilities.keyword; +import mage.MageObject; 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.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 mage.target.targetpointer.FixedTarget; -import org.junit.After; - import java.util.UUID; /** @@ -57,8 +53,9 @@ import java.util.UUID; */ public class AftermathAbility extends SimpleStaticAbility { public AftermathAbility() { - super(Zone.ALL, new AftermathCantCastFromHand()); - addEffect(new AftermathCastFromGraveyard()); + super(Zone.ALL, new AftermathCastFromGraveyard()); + addEffect(new AftermathCantCastFromHand()); + addEffect(new AftermathExileAsResolvesFromGraveyard()); } public AftermathAbility(final AftermathAbility ability) { @@ -101,9 +98,13 @@ class AftermathCastFromGraveyard extends AsThoughEffectImpl { 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()) && + 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) { @@ -151,4 +152,76 @@ class AftermathCantCastFromHand extends ContinuousRuleModifyingEffectImpl { } 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/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 8d308ecf843..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 = 71; + 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..337febbb50b 100644 --- a/Mage/src/main/java/mage/constants/CardType.java +++ b/Mage/src/main/java/mage/constants/CardType.java @@ -1,5 +1,7 @@ package mage.constants; +import java.util.HashSet; + /** * * @author North @@ -26,4 +28,19 @@ 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) { + HashSet cardTypes = new HashSet<>(); + for (CardType t: a) { + cardTypes.add(t); + } + for (CardType t: b) { + cardTypes.add(t); + } + 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 e22021f7294..f30ed2bcbbd 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1245,29 +1245,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); } } } @@ -1277,19 +1278,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 @@ -1297,12 +1298,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; From fd73fd39af3a3bec76379d2929e84ce4c063d629 Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Tue, 4 Apr 2017 17:07:59 -0600 Subject: [PATCH 3/4] Addressed Feedback and added cards * Addressed feedback on Pull Request #3053 * Fixed a copy-paste bug in Destined // Lead * Added two new Aftermath Split cards that were revealed today --- .../card/arcane/ModernSplitCardRenderer.java | 14 +-- Mage.Sets/src/mage/cards/d/DestinedLead.java | 17 +-- Mage.Sets/src/mage/cards/o/OnwardVictory.java | 99 ++++++++++++++++ Mage.Sets/src/mage/cards/p/PreparedFight.java | 109 ++++++++++++++++++ Mage.Sets/src/mage/sets/Amonkhet.java | 2 + .../main/java/mage/constants/CardType.java | 12 +- 6 files changed, 218 insertions(+), 35 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/o/OnwardVictory.java create mode 100644 Mage.Sets/src/mage/cards/p/PreparedFight.java 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 index 5e449b7702d..26ed83ca988 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java @@ -54,18 +54,8 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { rightHalf.name = cardView.getRightSplitName(); leftHalf.name = cardView.getLeftSplitName(); - for (String rule: view.getRules()) { - if (rule.contains("Fuse")) { - isFuse = true; - break; - } - } - for (String rule: view.getRightSplitRules()) { - if (rule.contains("Aftermath")) { - isAftermath = true; - break; - } - } + 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 diff --git a/Mage.Sets/src/mage/cards/d/DestinedLead.java b/Mage.Sets/src/mage/cards/d/DestinedLead.java index 44c708b8bc6..b7808914601 100644 --- a/Mage.Sets/src/mage/cards/d/DestinedLead.java +++ b/Mage.Sets/src/mage/cards/d/DestinedLead.java @@ -28,34 +28,19 @@ 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.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.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; /** @@ -76,7 +61,7 @@ public class DestinedLead extends SplitCard { effect.setText("Target creature gets +1/+0"); getLeftHalfCard().getSpellAbility().addEffect(effect); - effect = new GainAbilityTargetEffect(LifelinkAbility.getInstance(), Duration.EndOfTurn); + effect = new GainAbilityTargetEffect(IndestructibleAbility.getInstance(), Duration.EndOfTurn); effect.setText("and gains indestructible until end of turn"); getLeftHalfCard().getSpellAbility().addEffect(effect); 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 d449648cde2..d485a52634d 100644 --- a/Mage.Sets/src/mage/sets/Amonkhet.java +++ b/Mage.Sets/src/mage/sets/Amonkhet.java @@ -54,5 +54,7 @@ public class Amonkhet extends ExpansionSet { this.ratioBoosterMythic = 8; cards.add(new SetCardInfo("Dusk // Dawn", 210, Rarity.RARE, mage.cards.d.DuskDawn.class)); cards.add(new SetCardInfo("Destined // Lead", 217, Rarity.UNCOMMON, mage.cards.d.DestinedLead.class)); + cards.add(new SetCardInfo("Onward // Victory", 218, Rarity.UNCOMMON, mage.cards.o.OnwardVictory.class)); + cards.add(new SetCardInfo("Prepared // Fight", 220, Rarity.RARE, mage.cards.p.PreparedFight.class)); } } diff --git a/Mage/src/main/java/mage/constants/CardType.java b/Mage/src/main/java/mage/constants/CardType.java index 337febbb50b..29e19bc8cb5 100644 --- a/Mage/src/main/java/mage/constants/CardType.java +++ b/Mage/src/main/java/mage/constants/CardType.java @@ -1,5 +1,7 @@ package mage.constants; +import java.util.Arrays; +import java.util.EnumSet; import java.util.HashSet; /** @@ -33,13 +35,9 @@ public enum CardType { * Duplicates are eliminated. */ public static CardType[] mergeTypes(CardType[] a, CardType[] b) { - HashSet cardTypes = new HashSet<>(); - for (CardType t: a) { - cardTypes.add(t); - } - for (CardType t: b) { - cardTypes.add(t); - } + EnumSet cardTypes = EnumSet.noneOf(CardType.class); + cardTypes.addAll(Arrays.asList(a)); + cardTypes.addAll(Arrays.asList(b)); return cardTypes.toArray(new CardType[0]); } From 7a6b8a1540bc997e45e978478842ede77bdd0fdf Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Tue, 4 Apr 2017 18:28:30 -0600 Subject: [PATCH 4/4] Final Aftermath implementation stuff * Fixed PlayerImpl::getPlayable() to support aftermath (Needs PLAY_FROM_NOT_OWN_HAND_ZONE at the granularity of each card half rather than the whole card) * Added tests for Dusk // Dawn to make sure there are no regressions on Aftermath. --- .../test/cards/single/akh/DuskDawnTest.java | 92 ++++++++++--------- .../main/java/mage/players/PlayerImpl.java | 40 +++++--- 2 files changed, 76 insertions(+), 56 deletions(-) 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 index dd40cd4c054..6dd662ff135 100644 --- 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 @@ -13,67 +13,73 @@ public class DuskDawnTest extends CardTestPlayerBase { @Test public void testCastDusk() { - addCard(Zone.BATTLEFIELD, playerB, "Tarmogoyf"); - addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + //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 // 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 testCastDawnFail() { + 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"); - } - - /* - @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); + setStopAt(1, PhaseStep.END_TURN); execute(); - assertLife(playerA, 20); - assertLife(playerB, 19); - assertPermanentCount(playerA, "Hope of Ghirapur", 0); + // 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 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"); + 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"); - 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); + setStopAt(1, PhaseStep.END_TURN); execute(); - assertLife(playerA, 18); - assertLife(playerB, 19); - assertPermanentCount(playerA, "Hope of Ghirapur", 1); + // 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/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index f30ed2bcbbd..1630782d5d4 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -2609,6 +2609,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<>(); @@ -2649,20 +2666,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()) {