diff --git a/.gitignore b/.gitignore index d084fe143d0..61d6b76f856 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ syntax: glob Mage.Client/*.dck Mage.Client/db Mage.Client/gamelogs -Mage.Client/mageclient.log +Mage.Client/*.log Mage.Client/plugins/images Mage.Client/plugins/plugin.data Mage.Client/target diff --git a/Mage.Client/src/main/java/mage/client/cards/Card.java b/Mage.Client/src/main/java/mage/client/cards/Card.java index e984b305920..cb90ad56f40 100644 --- a/Mage.Client/src/main/java/mage/client/cards/Card.java +++ b/Mage.Client/src/main/java/mage/client/cards/Card.java @@ -562,7 +562,7 @@ public class Card extends MagePermanent implements MouseMotionListener, MouseLis } @Override - public void setTextOffset(int yOffset) { + public void setCardCaptionTopOffset(int yOffsetPercent) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } diff --git a/Mage.Client/src/main/java/mage/client/cards/CardArea.java b/Mage.Client/src/main/java/mage/client/cards/CardArea.java index 5fa3840253e..cb63d2e6ea9 100644 --- a/Mage.Client/src/main/java/mage/client/cards/CardArea.java +++ b/Mage.Client/src/main/java/mage/client/cards/CardArea.java @@ -53,7 +53,7 @@ public class CardArea extends JPanel implements MouseListener { private boolean reloaded = false; private final javax.swing.JLayeredPane cardArea; private final javax.swing.JScrollPane scrollPane; - private int yTextOffset; + private int yCardCaptionOffsetPercent = 0; // card caption offset (use for moving card caption view center, below mana icons -- for more good UI) private Dimension cardDimension; private int verticalCardOffset; @@ -68,8 +68,6 @@ public class CardArea extends JPanel implements MouseListener { setGUISize(); cardArea = new JLayeredPane(); scrollPane.setViewportView(cardArea); - yTextOffset = 10; - } public void cleanUp() { @@ -103,10 +101,10 @@ public class CardArea extends JPanel implements MouseListener { this.reloaded = true; cardArea.removeAll(); if (showCards != null && showCards.size() < 10) { - yTextOffset = 10; + yCardCaptionOffsetPercent = 8; // TODO: need to test loadCardsFew(showCards, bigCard, gameId); } else { - yTextOffset = 0; + yCardCaptionOffsetPercent = 0; loadCardsMany(showCards, bigCard, gameId); } cardArea.revalidate(); @@ -118,8 +116,10 @@ public class CardArea extends JPanel implements MouseListener { public void loadCardsNarrow(CardsView showCards, BigCard bigCard, UUID gameId) { this.reloaded = true; cardArea.removeAll(); - yTextOffset = 0; + + yCardCaptionOffsetPercent = 0; // TODO: need to test loadCardsMany(showCards, bigCard, gameId); + cardArea.revalidate(); this.revalidate(); @@ -152,7 +152,10 @@ public class CardArea extends JPanel implements MouseListener { cardArea.moveToFront(cardPanel); cardPanel.update(card); cardPanel.setCardBounds(rectangle.x, rectangle.y, cardDimension.width, cardDimension.height); - cardPanel.setTextOffset(yTextOffset); + + // new card have same settings as current view + cardPanel.setCardCaptionTopOffset(yCardCaptionOffsetPercent); + cardPanel.showCardTitle(); } diff --git a/Mage.Client/src/main/java/mage/client/cards/DragCardGrid.java b/Mage.Client/src/main/java/mage/client/cards/DragCardGrid.java index c93f0fecb9b..f96fc2f863b 100644 --- a/Mage.Client/src/main/java/mage/client/cards/DragCardGrid.java +++ b/Mage.Client/src/main/java/mage/client/cards/DragCardGrid.java @@ -1726,7 +1726,7 @@ public class DragCardGrid extends JPanel implements DragCardSource, DragCardTarg // Create the card view final MageCard cardPanel = Plugins.instance.getMageCard(card, lastBigCard, new Dimension(getCardWidth(), getCardHeight()), null, true, true); cardPanel.update(card); - cardPanel.setTextOffset(0); + cardPanel.setCardCaptionTopOffset(0); // Remove mouse wheel listeners so that scrolling works // Scrolling works on all areas without cards or by using the scroll bar, that's enough diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java index 3e27b580ae5..2488110b58a 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java @@ -236,6 +236,7 @@ public class MageBook extends JComponent { for (int i = 0; i < Math.min(conf.CARDS_PER_PAGE / 2, size); i++) { Card card = cards.get(i).getMockCard(); addCard(new CardView(card), bigCard, null, rectangle); + rectangle = CardPosition.translatePosition(i, rectangle, conf); } @@ -341,6 +342,8 @@ public class MageBook extends JComponent { cardImg.update(card); cardImg.setCardBounds(rectangle.x, rectangle.y, cardDimensions.frameWidth, cardDimensions.frameHeight); + cardImg.setCardCaptionTopOffset(8); // card caption below real card caption to see full name even with mana icons + boolean implemented = card.getRarity() != Rarity.NA; GlowText label = new GlowText(); diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java index 4bf8218156f..230728563ce 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanel.java @@ -91,7 +91,8 @@ public abstract class CardPanel extends MagePermanent implements MouseListener, private JPanel cardArea; - private int yTextOffset = 10; + // default offset, e.g. for battlefield + private int yCardCaptionOffsetPercent = 8; // card caption offset (use for moving card caption view center, below mana icons -- for more good UI) // if this is set, it's opened if the user right clicks on the card panel private JPopupMenu popupMenu; @@ -819,12 +820,12 @@ public abstract class CardPanel extends MagePermanent implements MouseListener, } @Override - public void setTextOffset(int yOffset) { - yTextOffset = yOffset; + public void setCardCaptionTopOffset(int yOffsetPercent) { + yCardCaptionOffsetPercent = yOffsetPercent; } - public int getTextOffset() { - return yTextOffset; + public int getCardCaptionTopOffset() { + return yCardCaptionOffsetPercent; } @Override diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelComponentImpl.java b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelComponentImpl.java index c57a41f0430..311e1dd9692 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelComponentImpl.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/CardPanelComponentImpl.java @@ -34,7 +34,7 @@ import static org.mage.plugins.card.constants.Constants.THUMBNAIL_SIZE_FULL; * Class for drawing the mage card object by using a form based JComponent * approach * - * @author arcane, nantuko, noxx, stravant + * @author arcane, nantuko, noxx, stravant, JayDi85 */ @SuppressWarnings({"unchecked", "rawtypes"}) public class CardPanelComponentImpl extends CardPanel { @@ -47,9 +47,14 @@ public class CardPanelComponentImpl extends CardPanel { private static final float ROUNDED_CORNER_SIZE = 0.1f; private static final float BLACK_BORDER_SIZE = 0.03f; + private static final float SELECTION_BORDER_SIZE = 0.03f; private static final int TEXT_GLOW_SIZE = 6; private static final float TEXT_GLOW_INTENSITY = 3f; + // size to show icons and text (help to see full size card without text) + private static final int CARD_MIN_SIZE_FOR_ICONS = 60; + private static final int CARD_MAX_SIZE_FOR_ICONS = 200; + public final ScaledImagePanel imagePanel; public ImagePanel overlayPanel; @@ -177,6 +182,34 @@ public class CardPanelComponentImpl extends CardPanel { IMAGE_CACHE = ImageCaches.register(new MapMaker().softValues().makeComputingMap((Function) key -> createImage(key))); } + static private boolean canShowCardIcons(int cardFullWidth, boolean cardHasImage){ + // cards without images show icons and text always + // TODO: apply "card names on card" setting to icon too? + // TODO: fix card min-max size to hide (compare to settings size, not direct 60 and 200) + return ((cardFullWidth > 60) && (cardFullWidth < 200)) || (!cardHasImage); + } + + private static class CardSizes{ + Rectangle rectFull; + Rectangle rectSelection; + Rectangle rectBorder; + Rectangle rectCard; + + CardSizes(int offsetX, int offsetY, int fullWidth, int fullHeight){ + + int realBorderSizeX = Math.round(fullWidth * BLACK_BORDER_SIZE); + int realBorderSizeY = Math.round(fullWidth * BLACK_BORDER_SIZE); + int realSelectionSizeX = Math.round(fullWidth * SELECTION_BORDER_SIZE); + int realSelectionSizeY = Math.round(fullWidth * SELECTION_BORDER_SIZE); + + // card full size = select border + black border + real card + rectFull = new Rectangle(offsetX, offsetY, fullWidth, fullHeight); + rectSelection = new Rectangle(rectFull.x, rectFull.y, rectFull.width, rectFull.height); + rectBorder = new Rectangle(rectSelection.x + realSelectionSizeX, rectSelection.y + realSelectionSizeY, rectSelection.width - 2 * realSelectionSizeX, rectSelection.height - 2 * realSelectionSizeY); + rectCard = new Rectangle(rectBorder.x + realBorderSizeX, rectBorder.y + realBorderSizeY, rectBorder.width - 2 * realBorderSizeX, rectBorder.height - 2 * realBorderSizeY); + } + } + public CardPanelComponentImpl(CardView newGameCard, UUID gameId, final boolean loadImage, ActionCallback callback, final boolean foil, Dimension dimension) { // Call to super super(newGameCard, gameId, loadImage, callback, foil, dimension); @@ -368,32 +401,45 @@ public class CardPanelComponentImpl extends CardPanel { Graphics2D g2d = image.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - if (!key.hasImage) { - g2d.setColor(new Color(30, 200, 200, 120)); - } else { - g2d.setColor(new Color(0, 0, 0, 0)); - } + // card full size = select border + black border + real card + CardSizes sizes = new CardSizes(cardXOffset, cardYOffset, cardWidth, cardHeight); - int cornerSize = Math.max(4, Math.round(cardWidth * ROUNDED_CORNER_SIZE)); - g2d.fillRoundRect(cardXOffset, cardYOffset, cardWidth, cardHeight, cornerSize, cornerSize); + // corners for selection and for border + int cornerSizeSelection = Math.max(4, Math.round(sizes.rectSelection.width * ROUNDED_CORNER_SIZE)); + int cornerSizeBorder = Math.max(4, Math.round(sizes.rectBorder.width * ROUNDED_CORNER_SIZE)); + // DRAW ORDER from big to small: select -> select info -> border -> card + + // draw selection if (key.isSelected) { g2d.setColor(Color.green); - g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); + g2d.fillRoundRect(sizes.rectSelection.x + 1, sizes.rectSelection.y + 1, sizes.rectSelection.width - 2, sizes.rectSelection.height - 2, cornerSizeSelection, cornerSizeSelection); } else if (key.isChoosable) { g2d.setColor(new Color(250, 250, 0, 230)); - g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); + g2d.fillRoundRect(sizes.rectSelection.x + 1, sizes.rectSelection.y + 1, sizes.rectSelection.width - 2, sizes.rectSelection.height - 2, cornerSizeSelection, cornerSizeSelection); } else if (key.isPlayable) { g2d.setColor(new Color(153, 102, 204, 200)); - //g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); - g2d.fillRoundRect(cardXOffset, cardYOffset, cardWidth, cardHeight, cornerSize, cornerSize); + g2d.fillRoundRect(sizes.rectSelection.x, sizes.rectSelection.y, sizes.rectSelection.width, sizes.rectSelection.height, cornerSizeSelection, cornerSizeSelection); } + // draw empty card with border + if (!key.hasImage) { + // gray 1 px border + g2d.setColor(new Color(125, 125, 125, 255)); + g2d.fillRoundRect(sizes.rectBorder.x, sizes.rectBorder.y, sizes.rectBorder.width, sizes.rectBorder.height, cornerSizeBorder, cornerSizeBorder); + // color plate + g2d.setColor(new Color(30, 200, 200, 200)); + g2d.fillRoundRect(sizes.rectBorder.x + 1, sizes.rectBorder.y + 1, sizes.rectBorder.width - 2, sizes.rectBorder.height - 2, cornerSizeBorder, cornerSizeBorder); + } + + // draw attack border (inner part of selection) if (key.canAttack) { g2d.setColor(new Color(0, 0, 255, 230)); - g2d.fillRoundRect(cardXOffset + 1, cardYOffset + 1, cardWidth - 2, cardHeight - 2, cornerSize, cornerSize); + g2d.fillRoundRect(sizes.rectBorder.x + 1, sizes.rectBorder.y + 1, sizes.rectBorder.width - 2, sizes.rectBorder.height - 2, cornerSizeBorder, cornerSizeBorder); } + // draw real card by component (see imagePanel and other layout's items) + //TODO:uncomment /* if (gameCard.isAttacking()) { @@ -409,18 +455,54 @@ public class CardPanelComponentImpl extends CardPanel { protected void paintChildren(Graphics g) { super.paintChildren(g); - if (getShowCastingCost() && !isAnimationPanel() && getCardWidth() < 200 && getCardWidth() > 60) { + CardSizes realCard = new CardSizes(getCardXOffset(), getCardYOffset(), getCardWidth(), getCardHeight()); + + /* + // draw recs for debug + + // full card + g.setColor(new Color(255, 0, 0)); + g.drawRect(realCard.rectFull.x, realCard.rectFull.y, realCard.rectFull.width, realCard.rectFull.height); + + // real card - image + g.setColor(new Color(0, 0, 255)); + g.drawRect(imagePanel.getX(), imagePanel.getY(), imagePanel.getBounds().width, imagePanel.getBounds().height); + + // caption + g.setColor(new Color(0, 255, 255)); + g.drawRect(titleText.getX(), titleText.getY(), titleText.getBounds().width, titleText.getBounds().height); + + // life points + g.setColor(new Color(120, 0, 120)); + g.drawRect(ptText.getX(), ptText.getY(), ptText.getBounds().width, ptText.getBounds().height); + //*/ + + if (getShowCastingCost() && !isAnimationPanel() && canShowCardIcons(getCardWidth(), hasImage)) { + String manaCost = ManaSymbols.getStringManaCost(gameCard.getManaCost()); - int width = getWidth(manaCost); + int manaWidth = getManaWidth(manaCost); + + // right top corner with margin (sizes from any sample card, length from black border to mana icon) + int manaMarginRight = Math.round(22f / 672f * getCardWidth()); + int manaMarginTop = Math.round(24f / 936f * getCardHeight()); + + int manaX = getCardXOffset() + getCardWidth() - manaMarginRight - manaWidth; + int manaY = getCardYOffset() + manaMarginTop; + if (hasImage) { - ManaSymbols.draw(g, manaCost, getCardXOffset() + getCardWidth() - width - 5, getCardYOffset() + 5, getSymbolWidth()); + // top right corner if have image like real card + ManaSymbols.draw(g, manaCost, manaX, manaY, getSymbolWidth()); } else { - ManaSymbols.draw(g, manaCost, getCardXOffset() + 8, getCardHeight() - 9, getSymbolWidth()); + // old version - bottom left corner if haven't card + // ManaSymbols.draw(g, manaCost, getCardXOffset() + manaMarginRight, getCardYOffset() + getCardHeight() - manaMarginTop - getSymbolWidth(), getSymbolWidth()); + + // new version - like a normal image (it's best to view and construct decks) + ManaSymbols.draw(g, manaCost, manaX, manaY, getSymbolWidth()); } } } - private int getWidth(String manaCost) { + private int getManaWidth(String manaCost) { int width = 0; manaCost = manaCost.replace("\\", ""); StringTokenizer tok = new StringTokenizer(manaCost, " "); @@ -439,24 +521,28 @@ public class CardPanelComponentImpl extends CardPanel { int cardHeight = getCardHeight(); int cardXOffset = getCardXOffset(); int cardYOffset = getCardYOffset(); - int borderSize = Math.round(cardWidth * BLACK_BORDER_SIZE); - imagePanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize); - imagePanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2); + + CardSizes sizes = new CardSizes(cardXOffset, cardYOffset, cardWidth, cardHeight); + + // origin card without selection + Rectangle realCardSize = sizes.rectBorder; + imagePanel.setLocation(realCardSize.x, realCardSize.y); + imagePanel.setSize(realCardSize.width, realCardSize.height); if (hasSickness() && gameCard.isCreature() && isPermanent()) { - overlayPanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize); - overlayPanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2); + overlayPanel.setLocation(realCardSize.x, realCardSize.y); + overlayPanel.setSize(realCardSize.width, realCardSize.height); } else { overlayPanel.setVisible(false); } if (iconPanel != null) { - iconPanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize); - iconPanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2); + iconPanel.setLocation(realCardSize.x, realCardSize.y); + iconPanel.setSize(realCardSize.width, realCardSize.height); } if (counterPanel != null) { - counterPanel.setLocation(cardXOffset + borderSize, cardYOffset + borderSize); - counterPanel.setSize(cardWidth - borderSize * 2, cardHeight - borderSize * 2); + counterPanel.setLocation(realCardSize.x, realCardSize.y); + counterPanel.setSize(realCardSize.width, realCardSize.height); int size = cardWidth > WIDTH_LIMIT ? 40 : 20; minusCounterLabel.setLocation(counterPanel.getWidth() - size, counterPanel.getHeight() - size * 2); @@ -472,32 +558,52 @@ public class CardPanelComponentImpl extends CardPanel { otherCounterLabel.setSize(size, size); } - int fontHeight = Math.round(cardHeight * (27f / 680)); - boolean showText = (!isAnimationPanel() && fontHeight < 12); + + // TITLE + + //old version - text hide on small fonts, why? + //int fontHeight = Math.round(cardHeight * (26f / 672)); + //boolean showText = (!isAnimationPanel() && fontHeight < 12); + + boolean showText = !isAnimationPanel() && canShowCardIcons(cardWidth, hasImage); titleText.setVisible(showText); ptText.setVisible(showText); fullImageText.setVisible(fullImagePath != null); if (showText) { - int fontSize = cardHeight / 11; + int fontSize = cardHeight / 13; // startup font size (it same size on all zoom levels) titleText.setFont(getFont().deriveFont(Font.BOLD, fontSize)); - int titleX = Math.round(cardWidth * (20f / 480)); - int titleY = Math.round(cardHeight * (9f / 680)) + getTextOffset(); - titleText.setBounds(cardXOffset + titleX, cardYOffset + titleY, cardWidth - titleX, cardHeight - titleY); + // margins from card black border to text, not need? text show up good without margins + int titleMarginLeft = 0; //Math.round(28f / 672f * cardWidth); + int titleMarginRight = 0; + int titleMarginTop = 0 + Math.round(getCardCaptionTopOffset() / 100f * cardHeight);//Math.round(28f / 936f * cardHeight); + int titleMarginBottom = 0; + titleText.setBounds( + imagePanel.getX() + titleMarginLeft, + imagePanel.getY() + titleMarginTop, + imagePanel.getBounds().width - titleMarginLeft - titleMarginRight, + imagePanel.getBounds().height - titleMarginTop - titleMarginBottom + ); fullImageText.setFont(getFont().deriveFont(Font.PLAIN, 10)); - fullImageText.setBounds(cardXOffset, cardYOffset + titleY, cardWidth, cardHeight - titleY); + fullImageText.setBounds(titleText.getX(), titleText.getY(), titleText.getBounds().width, titleText.getBounds().height); + // life points location (font as title) ptText.setFont(getFont().deriveFont(Font.BOLD, fontSize)); Dimension ptSize = ptText.getPreferredSize(); ptText.setSize(ptSize.width, ptSize.height); - int ptX = Math.round(cardWidth * (420f / 480)) - ptSize.width / 2; - int ptY = Math.round(cardHeight * (675f / 680)) - ptSize.height; - int offsetX = Math.round((CARD_SIZE_FULL.width - cardWidth) / 10.0f); + // right bottom corner with margin (sizes from any sample card) + int ptMarginRight = Math.round(64f / 672f * cardWidth); + int ptMarginBottom = Math.round(62f / 936f * cardHeight); - ptText.setLocation(cardXOffset + ptX - TEXT_GLOW_SIZE / 2 - offsetX, cardYOffset + ptY - TEXT_GLOW_SIZE / 2); + int ptX = cardXOffset + cardWidth - ptMarginRight - ptSize.width; + int ptY = cardYOffset + cardHeight - ptMarginBottom - ptSize.height; + ptText.setLocation(ptX, ptY); + + // old version was with TEXT_GLOW_SIZE + //ptText.setLocation(cardXOffset + ptX - TEXT_GLOW_SIZE / 2 - offsetX, cardYOffset + ptY - TEXT_GLOW_SIZE / 2); } } @@ -581,7 +687,7 @@ public class CardPanelComponentImpl extends CardPanel { } else if (this.gameCard instanceof StackAbilityView) { return ImageCache.getMorphImage(); } else { - return ImageCache.loadImage(new TFile(DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename)); + return ImageCache.getCardbackImage(); } } 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 b418cd22df5..a3a5abd5b5d 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 @@ -218,13 +218,15 @@ 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; + // The faceart image for the card, loaded in from the disk (based on artid from mtgo) + private BufferedImage faceArtImage; + // Factory to generate card appropriate views private CardRendererFactory cardRendererFactory = new CardRendererFactory(); @@ -252,6 +254,8 @@ public class CardPanelRenderImpl extends CardPanel { // Use the art image and current rendered image from the card artImage = impl.artImage; cardRenderer.setArtImage(artImage); + faceArtImage = impl.faceArtImage; + cardRenderer.setFaceArtImage(faceArtImage); cardImage = impl.cardImage; } } @@ -263,8 +267,8 @@ public class CardPanelRenderImpl extends CardPanel { // Try to get card image from cache based on our card characteristics ImageKey key = new ImageKey(gameCard, artImage, - getCardWidth(), getCardHeight(), - isChoosable(), isSelected()); + getCardWidth(), getCardHeight(), + isChoosable(), isSelected()); cardImage = IMAGE_CACHE.computeIfAbsent(key, k -> renderCard()); // No cached copy exists? Render one and cache it @@ -277,7 +281,6 @@ public class CardPanelRenderImpl extends CardPanel { /** * Create an appropriate card renderer for the */ - /** * Render the card to a new BufferedImage at it's current dimensions * @@ -315,6 +318,7 @@ public class CardPanelRenderImpl extends CardPanel { artImage = null; cardImage = null; cardRenderer.setArtImage(null); + cardRenderer.setFaceArtImage(null); // Stop animation tappedAngle = isTapped() ? CardPanel.TAPPED_ANGLE : 0; @@ -332,19 +336,26 @@ public class CardPanelRenderImpl extends CardPanel { Util.threadPool.submit(() -> { try { final BufferedImage srcImage; + final BufferedImage faceArtSrcImage; if (gameCard.isFaceDown()) { // Nothing to do srcImage = null; + faceArtSrcImage = null; } else if (getCardWidth() > THUMBNAIL_SIZE_FULL.width) { srcImage = ImageCache.getImage(gameCard, getCardWidth(), getCardHeight()); + faceArtSrcImage = ImageCache.getFaceImage(gameCard, getCardWidth(), getCardHeight()); } else { srcImage = ImageCache.getThumbnail(gameCard); + faceArtSrcImage = ImageCache.getFaceImage(gameCard, getCardWidth(), getCardHeight()); } UI.invokeLater(() -> { if (stamp == updateArtImageStamp) { artImage = srcImage; cardRenderer.setArtImage(srcImage); + faceArtImage = faceArtSrcImage; + cardRenderer.setFaceArtImage(faceArtSrcImage); + if (srcImage != null) { // Invalidate and repaint cardImage = null; @@ -370,6 +381,7 @@ public class CardPanelRenderImpl extends CardPanel { cardImage = null; cardRenderer = cardRendererFactory.create(gameCard, isTransformed()); cardRenderer.setArtImage(artImage); + cardRenderer.setFaceArtImage(faceArtImage); // Repaint repaint(); @@ -398,7 +410,7 @@ public class CardPanelRenderImpl extends CardPanel { } else if (this.gameCard instanceof StackAbilityView) { return ImageCache.getMorphImage(); } else { - return ImageCache.loadImage(new TFile(DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename)); + return ImageCache.getCardbackImage(); } } 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 f28cb134e4b..35ec4fc2140 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 @@ -66,6 +66,9 @@ public abstract class CardRenderer { // The card image protected BufferedImage artImage; + // The face card image + protected BufferedImage faceArtImage; + /////////////////////////////////////////////////////////////////////////// // Common layout metrics between all cards // Polygons for counters @@ -289,8 +292,8 @@ public abstract class CardRenderer { try { BufferedImage subImg = artImage.getSubimage( - (int) (artRect.getX() * fullCardImgWidth), (int) (artRect.getY() * fullCardImgHeight), - (int) artWidth, (int) artHeight); + (int) (artRect.getX() * fullCardImgWidth), (int) (artRect.getY() * fullCardImgHeight), + (int) artWidth, (int) artHeight); g.drawImage(subImg, x, y, (int) targetWidth, (int) targetHeight, @@ -300,6 +303,44 @@ public abstract class CardRenderer { } } + protected void drawFaceArtIntoRect(Graphics2D g, int x, int y, int w, int h, Rectangle2D artRect, boolean shouldPreserveAspect) { + // Perform a process to make sure that the art is scaled uniformly to fill the frame, cutting + // off the minimum amount necessary to make it completely fill the frame without "squashing" it. + double fullCardImgWidth = faceArtImage.getWidth(); + double fullCardImgHeight = faceArtImage.getHeight(); + double artWidth = fullCardImgWidth; + double artHeight = fullCardImgHeight; + double targetWidth = w; + double targetHeight = h; + double targetAspect = targetWidth / targetHeight; + if (!shouldPreserveAspect) { + // No adjustment to art + } else if (targetAspect * artHeight < artWidth) { + // Trim off some width + artWidth = targetAspect * artHeight; + } else { + // Trim off some height + artHeight = artWidth / targetAspect; + } + try { + /*BufferedImage subImg + = faceArtImage.getSubimage( + (int) (artRect.getX() * fullCardImgWidth), (int) (artRect.getY() * fullCardImgHeight), + (int) artWidth, (int) artHeight);*/ + RenderingHints rh = new RenderingHints( + RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHints(rh); + g.drawImage(faceArtImage, + 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 + System.out.println(e); + } + } + // Draw +1/+1 and other counters protected void drawCounters(Graphics2D g) { int xPos = (int) (0.65 * cardWidth); @@ -442,4 +483,10 @@ public abstract class CardRenderer { public void setArtImage(Image image) { artImage = CardRendererUtils.toBufferedImage(image); } + + // Set the card art image (CardPanel will give it to us when it + // is loaded and ready) + public void setFaceArtImage(Image image) { + faceArtImage = CardRendererUtils.toBufferedImage(image); + } } 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 a3de8114955..e603ac6d1e2 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 @@ -279,6 +279,8 @@ public class ModernCardRenderer extends CardRenderer { // Just draw a brown rectangle drawCardBack(g); } else { + BufferedImage bufferedImage = new BufferedImage(300, 300, BufferedImage.TYPE_INT_RGB); + // Set texture to paint with g.setPaint(getBackgroundPaint(cardView.getColor(), cardView.getCardTypes(), cardView.getSubTypes())); @@ -348,8 +350,15 @@ public class ModernCardRenderer extends CardRenderer { @Override protected void drawArt(Graphics2D g) { if (artImage != null && !cardView.isFaceDown()) { + + boolean useFaceArt = false; + if (faceArtImage != null) { + useFaceArt = true; + } + // Invention rendering, art fills the entire frame if (useInventionFrame()) { + useFaceArt = false; drawArtIntoRect(g, borderWidth, borderWidth, cardWidth - 2 * borderWidth, cardHeight - 2 * borderWidth, @@ -360,6 +369,7 @@ public class ModernCardRenderer extends CardRenderer { Rectangle2D sourceRect = getArtRect(); if (cardView.getMageObjectType() == MageObjectType.SPELL) { + useFaceArt = false; ArtRect rect = cardView.getArtRect(); if (rect == ArtRect.SPLIT_FUSED) { // Special handling for fused, draw the art from both halves stacked on top of one and other @@ -380,10 +390,17 @@ public class ModernCardRenderer extends CardRenderer { } // Normal drawing of art from a source part of the card frame into the rect - drawArtIntoRect(g, - totalContentInset + 1, totalContentInset + boxHeight, - contentWidth - 2, typeLineY - totalContentInset - boxHeight, - sourceRect, shouldPreserveAspect); + if (useFaceArt) { + drawFaceArtIntoRect(g, + totalContentInset + 1, totalContentInset + boxHeight, + contentWidth - 2, typeLineY - totalContentInset - boxHeight, + sourceRect, shouldPreserveAspect); + } else { + drawArtIntoRect(g, + totalContentInset + 1, totalContentInset + boxHeight, + contentWidth - 2, typeLineY - totalContentInset - boxHeight, + sourceRect, shouldPreserveAspect); + } } } @@ -420,6 +437,7 @@ public class ModernCardRenderer extends CardRenderer { g.setPaint(new Color(255, 255, 255, 150)); } else { g.setPaint(textboxPaint); + } g.fillRect( totalContentInset + 1, typeLineY, @@ -476,6 +494,9 @@ public class ModernCardRenderer extends CardRenderer { // Draw the transform circle int nameOffset = drawTransformationCircle(g, borderPaint); + // Draw the transform circle + nameOffset = drawTransformationCircle(g, borderPaint); + // Draw the name line drawNameLine(g, cardView.getDisplayName(), manaCostString, totalContentInset + nameOffset, totalContentInset, diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java index 0ab60ad0862..374257a693c 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java @@ -22,7 +22,7 @@ import static org.mage.plugins.card.dl.DownloadJob.toFile; */ public class DirectLinksForDownload implements Iterable { - private static final String backsideUrl = "http://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"; + private static final String backsideUrl = "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"; private static final Map directLinks = new LinkedHashMap<>(); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java index 41a6fb93dc2..496768a6cf5 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java @@ -6,19 +6,44 @@ import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Iterator; + import mage.cards.ExpansionSet; import mage.cards.Sets; +import mage.constants.Rarity; import org.mage.plugins.card.dl.DownloadJob; + import static org.mage.plugins.card.dl.DownloadJob.fromURL; import static org.mage.plugins.card.dl.DownloadJob.toFile; +import org.apache.log4j.Logger; public class GathererSets implements Iterable { + private class CheckResult { + String code; + ExpansionSet set; + boolean haveCommon; + boolean haveUncommon; + boolean haveRare; + boolean haveMyth; + + private CheckResult(String ACode, ExpansionSet ASet, boolean AHaveCommon, boolean AHaveUncommon, boolean AHhaveRare, boolean AHaveMyth) { + code = ACode; + set = ASet; + haveCommon = AHaveCommon; + haveUncommon = AHaveUncommon; + haveRare = AHhaveRare; + haveMyth = AHaveMyth; + } + } + + private static final int DAYS_BEFORE_RELEASE_TO_DOWNLOAD = +14; // Try to load the symbolsBasic eralies 14 days before release date + private static final Logger logger = Logger.getLogger(GathererSets.class); + private static final String SETS_PATH = File.separator + "sets"; private static final File DEFAULT_OUT_DIR = new File("plugins" + File.separator + "images" + SETS_PATH); private static File outDir = DEFAULT_OUT_DIR; - private static final String[] symbols = {"10E", "9ED", "8ED", "7ED", "6ED", "5ED", "4ED", "3ED", "2ED", "LEB", "LEA", + private static final String[] symbolsBasic = {"10E", "9ED", "8ED", "7ED", "6ED", "5ED", "4ED", "3ED", "2ED", "LEB", "LEA", "HOP", "ARN", "ATQ", "LEG", "DRK", "FEM", "HML", "ICE", "ALL", "CSP", @@ -36,14 +61,19 @@ public class GathererSets implements Iterable { "LRW", "MOR", "SHM", "EVE", "MED", "ME2", "ME3", "ME4", - "POR", "PO2", "PTK", + "POR", "P02", "PTK", "ARC", "DD3EVG", - "W16", "W17"}; + "W16", "W17", + //"APAC" -- gatherer do not have that set, scrly have PALP + //"ARENA" -- is't many set with different codes, not one + "CLASH", "CP", "DD3GVL", "DPA", "EURO", "FNMP", "GPX", "GRC", "GUR", "H17", "JR", "MBP", "MGDC", "MLP", "MPRP", "MPS-AKH", "PTC", "S00", "S99", "SUS", "SWS", "UGIN", "UGL", "V10", "V17", "WMCQ", // need to fix + "H09", "PD2", "PD3", "UNH", "CM1", "E02", "V11", "M25", "UST", "IMA", "DD2", "EVG", "DDC", "DDE", "DDD", "DDT", "8EB", "9EB", "CHR" // ok + // current testing + }; - private static final String[] withMythics = {"M10", "M11", "M12", "M13", "M14", "M15", "ORI", - "ANB", + private static final String[] symbolsBasicWithMyth = {"M10", "M11", "M12", "M13", "M14", "M15", "ORI", "DDF", "DDG", "DDH", "DDI", "DDJ", "DDK", "DDL", "DDM", "DDN", - "DD3DVD", "DD3GLV", "DD3JVC", "DDO", "DDP", "DDQ", "DDR", "DDS", + "DD3DVD", "DD3JVC", "DDO", "DDP", "DDQ", "DDR", "DDS", "ALA", "CON", "ARB", "ZEN", "WWK", "ROE", "SOM", "MBS", "NPH", @@ -60,60 +90,64 @@ public class GathererSets implements Iterable { "SOI", "EMN", "KLD", "AER", "AKH", "HOU", + "XLN", "C17", + "RIX", "DOM", "M19", // not released "E01" }; - private static final String[] onlyMythics = { - "DRB", "V09", "V12", "V12", "V13", "V14", "V15", "V16", "EXP" + private static final String[] symbolsOnlyMyth = { + "DRB", "V09", "V12", "V13", "V14", "V15", "V16", "EXP" }; - private static final String[] onlyMythicsAsSpecial = { + private static final String[] symbolsOnlySpecial = { "MPS" }; - private static final HashMap symbolsReplacements = new HashMap<>(); + private static final HashMap codeReplacements = new HashMap<>(); static { - symbolsReplacements.put("2ED", "2U"); - symbolsReplacements.put("3ED", "3E"); - symbolsReplacements.put("4ED", "4E"); - symbolsReplacements.put("5ED", "5E"); - symbolsReplacements.put("6ED", "6E"); - symbolsReplacements.put("7ED", "7E"); - symbolsReplacements.put("ALL", "AL"); - symbolsReplacements.put("APC", "AP"); - symbolsReplacements.put("ARN", "AN"); - symbolsReplacements.put("ATQ", "AQ"); - symbolsReplacements.put("CMA", "CM1"); - symbolsReplacements.put("DD3DVD", "DD3_DVD"); - symbolsReplacements.put("DD3EVG", "DD3_EVG"); - symbolsReplacements.put("DD3GLV", "DD3_GLV"); - symbolsReplacements.put("DD3JVC", "DD3_JVC"); - symbolsReplacements.put("DRK", "DK"); - symbolsReplacements.put("EXO", "EX"); - symbolsReplacements.put("FEM", "FE"); - symbolsReplacements.put("HML", "HM"); - symbolsReplacements.put("ICE", "IA"); - symbolsReplacements.put("INV", "IN"); - symbolsReplacements.put("LEA", "1E"); - symbolsReplacements.put("LEB", "2E"); - symbolsReplacements.put("LEG", "LE"); - symbolsReplacements.put("MPS", "MPS_KLD"); - symbolsReplacements.put("MIR", "MI"); - symbolsReplacements.put("MMQ", "MM"); - symbolsReplacements.put("NEM", "NE"); - symbolsReplacements.put("ODY", "OD"); - symbolsReplacements.put("PCY", "PR"); - symbolsReplacements.put("PLS", "PS"); - symbolsReplacements.put("POR", "PO"); - symbolsReplacements.put("PO2", "P2"); - symbolsReplacements.put("PTK", "PK"); - symbolsReplacements.put("STH", "ST"); - symbolsReplacements.put("TMP", "TE"); - symbolsReplacements.put("UDS", "CG"); - symbolsReplacements.put("ULG", "GU"); - symbolsReplacements.put("USG", "UZ"); - symbolsReplacements.put("VIS", "VI"); - symbolsReplacements.put("WTH", "WL"); + codeReplacements.put("2ED", "2U"); + codeReplacements.put("3ED", "3E"); + codeReplacements.put("4ED", "4E"); + codeReplacements.put("5ED", "5E"); + codeReplacements.put("6ED", "6E"); + codeReplacements.put("7ED", "7E"); + codeReplacements.put("ALL", "AL"); + codeReplacements.put("APC", "AP"); + codeReplacements.put("ARN", "AN"); + codeReplacements.put("ATQ", "AQ"); + codeReplacements.put("CMA", "CM1"); + codeReplacements.put("DD3DVD", "DD3_DVD"); + codeReplacements.put("DD3EVG", "DD3_EVG"); + codeReplacements.put("DD3JVC", "DD3_JVC"); + codeReplacements.put("DRK", "DK"); + codeReplacements.put("EXO", "EX"); + codeReplacements.put("FEM", "FE"); + codeReplacements.put("HML", "HM"); + codeReplacements.put("ICE", "IA"); + codeReplacements.put("INV", "IN"); + codeReplacements.put("LEA", "1E"); + codeReplacements.put("LEB", "2E"); + codeReplacements.put("LEG", "LE"); + codeReplacements.put("MPS", "MPS_KLD"); + codeReplacements.put("MIR", "MI"); + codeReplacements.put("MMQ", "MM"); + codeReplacements.put("NEM", "NE"); + codeReplacements.put("ODY", "OD"); + codeReplacements.put("PCY", "PR"); + codeReplacements.put("PLS", "PS"); + codeReplacements.put("POR", "PO"); + codeReplacements.put("P02", "P2"); + codeReplacements.put("PTK", "PK"); + codeReplacements.put("STH", "ST"); + codeReplacements.put("TMP", "TE"); + codeReplacements.put("UDS", "CG"); + codeReplacements.put("ULG", "GU"); + codeReplacements.put("USG", "UZ"); + codeReplacements.put("VIS", "VI"); + codeReplacements.put("WTH", "WL"); + codeReplacements.put("8EB", "8ED"); // inner xmage set for 8th edition + codeReplacements.put("9EB", "8ED"); // inner xmage set for 9th edition + codeReplacements.put("CHR", "CH"); } public GathererSets(String path) { @@ -124,49 +158,155 @@ public class GathererSets implements Iterable { } } + // checks for wrong card settings and support (easy to control what all good) + private static final HashMap setsToDownload = new HashMap<>(); + private static final HashMap codesToIgnoreCheck = new HashMap<>(); + static { + // xMage have inner sets for 8th and 9th Edition for booster workaround (cards from core game do not include in boosters) + // see https://mtg.gamepedia.com/8th_Edition/Core_Game + // check must ignore that sets + codesToIgnoreCheck.put("8EB", "8th Edition Box"); + codesToIgnoreCheck.put("9EB", "9th Edition Box"); + } + + private void CheckSearchResult(String searchCode, ExpansionSet foundedExp, boolean canDownloadTask, + boolean haveCommon, boolean haveUncommon, boolean haveRare, boolean haveMyth){ + // duplicated in settings + CheckResult res = setsToDownload.get(searchCode); + + if (res != null) { + logger.error(String.format("Symbols: founded duplicated code: %s", searchCode)); + } else { + res = new CheckResult(searchCode, foundedExp, haveCommon, haveUncommon, haveRare, haveMyth); + setsToDownload.put(searchCode, res); + } + + // not found + if (foundedExp == null) { + logger.error(String.format("Symbols: can't find set by code: %s", searchCode)); + return; + } + + // checks for founded sets only + + // to early to download + if (!canDownloadTask){ + Calendar c = Calendar.getInstance(); + c.setTime(foundedExp.getReleaseDate()); + c.add(Calendar.DATE, -1 * DAYS_BEFORE_RELEASE_TO_DOWNLOAD); + logger.warn(String.format("Symbols: early to download: %s (%s), available after %s", + searchCode, foundedExp.getName(), c.getTime())); + } + } + + private void AnalyseSearchResult(){ + // analyze supported sets and show wrong settings + Date startedDate = new Date(); + + for (ExpansionSet set : Sets.getInstance().values()) { + + // ignore some inner sets + if (codesToIgnoreCheck.get(set.getCode()) != null){ + continue; + } + + CheckResult res = setsToDownload.get(set.getCode()); + + // 1. not configured at all + if (res == null) { + logger.warn(String.format("Symbols: set is not configured: %s (%s)", set.getCode(), set.getName())); + continue; // can't do other checks + } + + // 2. missing rarity icon: + // WARNING, need too much time (60+ secs), only for debug mode + if (logger.isDebugEnabled()) { + if ((set.getCardsByRarity(Rarity.COMMON).size() > 0) && !res.haveCommon) { + logger.error(String.format("Symbols: set have common cards, but don't download icon: %s (%s)", set.getCode(), set.getName())); + } + if ((set.getCardsByRarity(Rarity.UNCOMMON).size() > 0) && !res.haveUncommon) { + logger.error(String.format("Symbols: set have uncommon cards, but don't download icon: %s (%s)", set.getCode(), set.getName())); + } + if ((set.getCardsByRarity(Rarity.RARE).size() > 0) && !res.haveRare) { + logger.error(String.format("Symbols: set have rare cards, but don't download icon: %s (%s)", set.getCode(), set.getName())); + } + if ((set.getCardsByRarity(Rarity.MYTHIC).size() > 0) && !res.haveMyth) { + logger.error(String.format("Symbols: set have mythic cards, but don't download icon: %s (%s)", set.getCode(), set.getName())); + } + } + } + + Date endedDate = new Date(); + long secs = (endedDate.getTime() - startedDate.getTime()) / 1000; + logger.debug(String.format("Symbols: check completed after %d seconds", secs)); + } + @Override public Iterator iterator() { Calendar c = Calendar.getInstance(); c.setTime(new Date()); - c.add(Calendar.DATE, +14); // Try to load the symbols eralies 14 days before release date + c.add(Calendar.DATE, DAYS_BEFORE_RELEASE_TO_DOWNLOAD); Date compareDate = c.getTime(); ArrayList jobs = new ArrayList<>(); - for (String symbol : symbols) { + boolean canDownload; + + setsToDownload.clear(); + + for (String symbol : symbolsBasic) { ExpansionSet exp = Sets.findSet(symbol); + canDownload = false; if (exp != null && exp.getReleaseDate().before(compareDate)) { + canDownload = true; jobs.add(generateDownloadJob(symbol, "C", "C")); jobs.add(generateDownloadJob(symbol, "U", "U")); jobs.add(generateDownloadJob(symbol, "R", "R")); } + CheckSearchResult(symbol, exp, canDownload, true, true, true, false); } - for (String symbol : withMythics) { + + for (String symbol : symbolsBasicWithMyth) { ExpansionSet exp = Sets.findSet(symbol); + canDownload = false; if (exp != null && exp.getReleaseDate().before(compareDate)) { + canDownload = true; jobs.add(generateDownloadJob(symbol, "C", "C")); jobs.add(generateDownloadJob(symbol, "U", "U")); jobs.add(generateDownloadJob(symbol, "R", "R")); jobs.add(generateDownloadJob(symbol, "M", "M")); } + CheckSearchResult(symbol, exp, canDownload, true, true, true, true); } - for (String symbol : onlyMythics) { + + for (String symbol : symbolsOnlyMyth) { ExpansionSet exp = Sets.findSet(symbol); + canDownload = false; if (exp != null && exp.getReleaseDate().before(compareDate)) { + canDownload = true; jobs.add(generateDownloadJob(symbol, "M", "M")); } + CheckSearchResult(symbol, exp, canDownload, false, false, false, true); } - for (String symbol : onlyMythicsAsSpecial) { + + for (String symbol : symbolsOnlySpecial) { ExpansionSet exp = Sets.findSet(symbol); + canDownload = false; if (exp != null && exp.getReleaseDate().before(compareDate)) { + canDownload = true; jobs.add(generateDownloadJob(symbol, "M", "S")); } + CheckSearchResult(symbol, exp, canDownload, false, false, false, true); } + + // check wrong settings + AnalyseSearchResult(); + return jobs.iterator(); } private DownloadJob generateDownloadJob(String set, String rarity, String urlRarity) { File dst = new File(outDir, set + '-' + rarity + ".jpg"); - if (symbolsReplacements.containsKey(set)) { - set = symbolsReplacements.get(set); + if (codeReplacements.containsKey(set)) { + set = codeReplacements.get(set); } String url = "http://gatherer.wizards.com/Handlers/Image.ashx?type=symbol&set=" + set + "&size=small&rarity=" + urlRarity; return new DownloadJob(set + '-' + rarity, fromURL(url), toFile(dst)); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java index 759a9bdacf4..b95cc3fefd3 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java @@ -214,10 +214,18 @@ public enum ScryfallImageSource implements CardImageSource { @Override public String generateURL(CardDownloadData card) throws Exception { - return "https://api.scryfall.com/cards/" + formatSetName(card.getSet()) + "/" - + card.getCollectorId() - + (card.isSecondSide() ? "b" : "") - + "?format=image"; + + if (card.isTwoFacedCard()) { + // double faced cards do not supporte by API (need direct link for img) + // example: https://img.scryfall.com/cards/large/en/xln/173b.jpg + return "https://img.scryfall.com/cards/large/en/" + formatSetName(card.getSet()) + "/" + + card.getCollectorId() + (card.isSecondSide() ? "b" : "a") + ".jpg"; + }else { + // single face cards support by single API call (redirect to img link) + // example: https://api.scryfall.com/cards/xln/121?format=image + return "https://api.scryfall.com/cards/" + formatSetName(card.getSet()) + "/" + + card.getCollectorId() + "?format=image"; + } } @Override diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java index 5ea83188bf5..f5b83c97791 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java @@ -451,6 +451,7 @@ public enum WizardCardsImageSource implements CardImageSource { setsAliases.put("WMCQ", "World Magic Cup Qualifier"); setsAliases.put("WTH", "Weatherlight"); setsAliases.put("WWK", "Worldwake"); + setsAliases.put("XLN", "Ixalan"); setsAliases.put("ZEN", "Zendikar"); languageAliases = new HashMap<>(); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java index 97cbd4af822..9e90b03f2ae 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/ImageCache.java @@ -3,7 +3,9 @@ package org.mage.plugins.card.images; import com.google.common.base.Function; import com.google.common.collect.ComputationException; import com.google.common.collect.MapMaker; -import java.awt.Graphics2D; + +import java.awt.*; +import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -44,6 +46,7 @@ public final class ImageCache { private static final Logger LOGGER = Logger.getLogger(ImageCache.class); private static final Map IMAGE_CACHE; + private static final Map FACE_IMAGE_CACHE; /** * Common pattern for keys. Format: "##" @@ -81,12 +84,14 @@ public final class ImageCache { CardDownloadData info = new CardDownloadData(name, set, collectorId, usesVariousArt, type, tokenSetCode, tokenDescriptor); + boolean cardback = false; String path; if (collectorId.isEmpty() || "0".equals(collectorId)) { info.setToken(true); path = CardImageUtils.generateTokenImagePath(info); if (path == null) { - path = DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename; + cardback = true; + path = DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename; // TODO: replace empty token by other default card, not cardback } } else { path = CardImageUtils.generateImagePath(info); @@ -101,6 +106,7 @@ public final class ImageCache { } if (thumbnail && path.endsWith(".jpg")) { + // need thumbnail image String thumbnailPath = buildThumbnailPath(path); TFile thumbnailFile = null; try { @@ -118,19 +124,35 @@ public final class ImageCache { if (exists) { LOGGER.debug("loading thumbnail for " + key + ", path=" + thumbnailPath); BufferedImage thumbnailImage = loadImage(thumbnailFile); + if (thumbnailImage == null) { // thumbnail exists but broken for some reason LOGGER.warn("failed loading thumbnail for " + key + ", path=" + thumbnailPath + ", thumbnail file is probably broken, attempting to recreate it..."); thumbnailImage = makeThumbnailByFile(key, file, thumbnailPath); } + + if (cardback){ + // unknown tokens on opponent desk + thumbnailImage = getRoundCorner(thumbnailImage); + } + return thumbnailImage; } else { return makeThumbnailByFile(key, file, thumbnailPath); } } else { - BufferedImage image = loadImage(file); - image = getWizardsCard(image); - return image; + if (cardback){ + // need cardback image + BufferedImage image = loadImage(file); + image = getRoundCorner(image); + return image; + }else { + // need normal card image + BufferedImage image = loadImage(file); + image = getWizardsCard(image); + image = getRoundCorner(image); + return image; + } } } else { throw new RuntimeException( @@ -145,6 +167,56 @@ public final class ImageCache { } } + public BufferedImage makeThumbnailByFile(String key, TFile file, String thumbnailPath) { + BufferedImage image = loadImage(file); + image = getWizardsCard(image); + image = getRoundCorner(image); + if (image == null) { + return null; + } + LOGGER.debug("creating thumbnail for " + key); + return makeThumbnail(image, thumbnailPath); + } + }); + + FACE_IMAGE_CACHE = new MapMaker().softValues().makeComputingMap(new Function() { + @Override + public BufferedImage apply(String key) { + try { + + Matcher m = KEY_PATTERN.matcher(key); + + if (m.matches()) { + String name = m.group(1); + String set = m.group(2); + //Integer artid = Integer.parseInt(m.group(2)); + + String path; + path = CardImageUtils.generateFaceImagePath(name, set); + + if (path == null) { + return null; + } + TFile file = getTFile(path); + if (file == null) { + return null; + } + + BufferedImage image = loadImage(file); + return image; + } else { + throw new RuntimeException( + "Requested face image doesn't fit the requirement for key (##: " + key); + } + } catch (Exception ex) { + if (ex instanceof ComputationException) { + throw (ComputationException) ex; + } else { + throw new ComputationException(ex); + } + } + } + public BufferedImage makeThumbnailByFile(String key, TFile file, String thumbnailPath) { BufferedImage image = loadImage(file); image = getWizardsCard(image); @@ -189,7 +261,7 @@ public final class ImageCache { info.setToken(true); path = CardImageUtils.generateFullTokenImagePath(info); if (path == null) { - path = DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename; + path = DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename; // TODO: replace empty token by other default card, not cardback } } else { path = CardImageUtils.generateImagePath(info); @@ -207,6 +279,12 @@ public final class ImageCache { private ImageCache() { } + public static BufferedImage getCardbackImage() { + BufferedImage image = ImageCache.loadImage(new TFile(DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename)); + image = getRoundCorner(image); + return image; + } + public static BufferedImage getMorphImage() { CardDownloadData info = new CardDownloadData("Morph", "KTK", "0", false, 0, "KTK", ""); info.setToken(true); @@ -215,7 +293,10 @@ public final class ImageCache { return null; } TFile file = getTFile(path); - return loadImage(file); + + BufferedImage image = loadImage(file); + image = getRoundCorner(image); + return image; } public static BufferedImage getManifestImage() { @@ -226,7 +307,10 @@ public final class ImageCache { return null; } TFile file = getTFile(path); - return loadImage(file); + + BufferedImage image = loadImage(file); + image = getRoundCorner(image); + return image; } private static String buildThumbnailPath(String path) { @@ -239,6 +323,32 @@ public final class ImageCache { return thumbnailPath; } + public static BufferedImage getRoundCorner(BufferedImage image){ + if (image != null) { + BufferedImage cornerImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + + // corner + float ROUNDED_CORNER_SIZE = 0.15f; // see CardPanelComponentImpl + int cornerSizeBorder = Math.max(4, Math.round(image.getWidth() * ROUNDED_CORNER_SIZE)); + + // corner mask + Graphics2D gg = cornerImage.createGraphics(); + gg.setComposite(AlphaComposite.Src); + gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + gg.setColor(Color.white); + gg.fill(new RoundRectangle2D.Float(0, 0, cornerImage.getWidth(), cornerImage.getHeight(), cornerSizeBorder, cornerSizeBorder)); + + // image draw to buffer + gg.setComposite(AlphaComposite.SrcAtop); + gg.drawImage(image, 0, 0, null); + //gg.dispose(); + + return cornerImage; + } else { + return image; + } + } + public static BufferedImage getWizardsCard(BufferedImage image) { if (image != null && image.getWidth() == 265 && image.getHeight() == 370) { BufferedImage crop = new BufferedImage(256, 360, BufferedImage.TYPE_INT_RGB); @@ -263,6 +373,10 @@ public final class ImageCache { return getImage(getKey(card, card.getName(), "")); } + public static BufferedImage getImageFaceOriginal(CardView card) { + return getFaceImage(getFaceKey(card, card.getName(), card.getExpansionSetCode())); + } + public static BufferedImage getImageOriginalAlternateName(CardView card) { return getImage(getKey(card, card.getAlternateName(), "")); } @@ -288,6 +402,27 @@ public final class ImageCache { } } + /** + * Returns the Image corresponding to the key + */ + private static BufferedImage getFaceImage(String key) { + try { + return FACE_IMAGE_CACHE.get(key); + } catch (NullPointerException ex) { + // unfortunately NullOutputException, thrown when apply() returns + // null, is not public + // NullOutputException is a subclass of NullPointerException + // legitimate, happens when a card has no image + return null; + } catch (ComputationException ex) { + if (ex.getCause() instanceof NullPointerException) { + return null; + } + LOGGER.error(ex, ex); + return null; + } + } + /** * Returns the Image corresponding to the key only if it already exists in * the cache. @@ -296,6 +431,14 @@ public final class ImageCache { return IMAGE_CACHE.containsKey(key) ? IMAGE_CACHE.get(key) : null; } + /** + * Returns the Image corresponding to the key only if it already exists in + * the cache. + */ + private static BufferedImage tryGetFaceImage(String key) { + return FACE_IMAGE_CACHE.containsKey(key) ? FACE_IMAGE_CACHE.get(key) : null; + } + /** * Returns the map key for a card, without any suffixes for the image size. */ @@ -307,6 +450,13 @@ public final class ImageCache { + (card.getTokenDescriptor() != null ? '#' + card.getTokenDescriptor() : "#"); } + /** + * Returns the map key for a card, without any suffixes for the image size. + */ + private static String getFaceKey(CardView card, String name, String set) { + return name + '#' + set + "####"; + } + // /** // * Returns the map key for the flip image of a card, without any suffixes for the image size. // */ @@ -314,6 +464,7 @@ public final class ImageCache { // return alternateName + "#" + card.getExpansionSetCode() + "#" +card.getType()+ "#" + card.getCardNumber() + "#" // + (card.getTokenSetCode() == null ? "":card.getTokenSetCode()); // } + /** * Load image from file * @@ -409,6 +560,25 @@ public final class ImageCache { return TransformedImageCache.getResizedImage(original, (int) (original.getWidth() * scale), (int) (original.getHeight() * scale)); } + /** + * Returns the image appropriate to display the card in the picture panel + * + * @param card + * @param width + * @param height + * @return + */ + public static BufferedImage getFaceImage(CardView card, int width, int height) { + String key = getFaceKey(card, card.getName(), card.getExpansionSetCode()); + BufferedImage original = getFaceImage(key); + if (original == null) { + LOGGER.debug(key + " (faceimage) not found"); + return null; + } + + return original; + } + /** * Returns the image appropriate to display for a card in a picture panel, * but only it was ALREADY LOADED. That is, the call is immediate and will diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java b/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java index 7cbe1ec9a0c..e8611b63e08 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java @@ -48,7 +48,7 @@ public final class CardImageUtils { log.warn("Token image file not found: " + card.getSet() + " - " + card.getTokenSetCode() + " - " + card.getName()); return null; } - + /** * * @param card @@ -204,6 +204,14 @@ public final class CardImageUtils { return imageDir + TFile.separator + imageName; } + public static String generateFaceImagePath(String cardname, String set) { + String useDefault = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_USE_DEFAULT, "true"); + String imagesPath = Objects.equals(useDefault, "true") ? Constants.IO.imageBaseDir : PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_PATH, null); + String imageDir = imagesPath; + String imageName = set + TFile.separator + cardname + ".jpg"; + return imageDir + TFile.separator + "FACE" + TFile.separator + imageName; + } + public static String generateTokenDescriptorImagePath(CardDownloadData card) { // String useDefault = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_USE_DEFAULT, "true"); // String imagesPath = Objects.equals(useDefault, "true") ? null : PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_PATH, null); diff --git a/Mage.Common/src/main/java/mage/cards/MageCard.java b/Mage.Common/src/main/java/mage/cards/MageCard.java index 61908d4daef..baea1105f26 100644 --- a/Mage.Common/src/main/java/mage/cards/MageCard.java +++ b/Mage.Common/src/main/java/mage/cards/MageCard.java @@ -25,8 +25,8 @@ public abstract class MageCard extends JPanel { public abstract CardView getOriginal(); - // sets the vertical text offset for the card name on the image - public abstract void setTextOffset(int yOffset); + // sets the vertical text offset for the card name on the image, use to move caption to card center + public abstract void setCardCaptionTopOffset(int yOffsetPercent); public abstract void setCardBounds(int x, int y, int width, int height); diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/CanadianHighlander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/CanadianHighlander.java index 75babb9a1b8..9f742d4fbcd 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/CanadianHighlander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/CanadianHighlander.java @@ -117,9 +117,9 @@ public class CanadianHighlander extends Constructed { || cn.equals("Mana Crypt") || cn.equals("Mystical Tutor") || cn.equals("Strip Mine") - || cn.equals("Summoner’s Pact") + || cn.equals("Summoner's Pact") || cn.equals("Survival of the Fittest") - || cn.equals("Umezawa’s Jitte")) { + || cn.equals("Umezawa's Jitte")) { totalPoints += 2; invalid.put(entry.getKey(), " 2 points " + cn); } diff --git a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties deleted file mode 100644 index 8603daf3264..00000000000 --- a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/target/maven-archiver/pom.properties +++ /dev/null @@ -1,5 +0,0 @@ -#Generated by Maven -#Sun Oct 22 00:34:49 EDT 2017 -version=1.4.26 -groupId=org.mage -artifactId=mage-game-pennydreadfulcommanderfreeforall diff --git a/Mage.Sets/src/mage/cards/f/FactOrFiction.java b/Mage.Sets/src/mage/cards/f/FactOrFiction.java index e90d554dd0a..0937996e9b8 100644 --- a/Mage.Sets/src/mage/cards/f/FactOrFiction.java +++ b/Mage.Sets/src/mage/cards/f/FactOrFiction.java @@ -41,7 +41,9 @@ import mage.constants.Zone; import mage.filter.FilterCard; import mage.game.Game; import mage.players.Player; +import mage.target.Target; import mage.target.TargetCard; +import mage.target.common.TargetOpponent; /** * @author North @@ -96,6 +98,13 @@ class FactOrFictionEffect extends OneShotEffect { Set opponents = game.getOpponents(source.getControllerId()); if (!opponents.isEmpty()) { Player opponent = game.getPlayer(opponents.iterator().next()); + if (opponents.size() > 1) { + Target targetOpponent = new TargetOpponent(true); + if (controller.chooseTarget(Outcome.Neutral, targetOpponent, source, game)) { + opponent = game.getPlayer(targetOpponent.getFirstTarget()); + game.informPlayers(controller.getLogName() + " chose " + opponent.getLogName() + " to separate the revealed cards"); + } + } TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, new FilterCard("cards to put in the first pile")); List pile1 = new ArrayList<>(); if (opponent.choose(Outcome.Neutral, cards, target, game)) { diff --git a/Mage.Sets/src/mage/cards/h/Hubris.java b/Mage.Sets/src/mage/cards/h/Hubris.java index cb4cb55983e..c8ce8463789 100644 --- a/Mage.Sets/src/mage/cards/h/Hubris.java +++ b/Mage.Sets/src/mage/cards/h/Hubris.java @@ -99,7 +99,13 @@ class HubrisReturnEffect extends OneShotEffect { for (UUID targetId : targetPointer.getTargets(game, source)) { Permanent creature = game.getPermanent(targetId); if (creature != null) { - Cards cardsToHand = new CardsImpl(creature.getAttachments()); + Cards cardsToHand = new CardsImpl(); + for (UUID cardId : creature.getAttachments()) { + Permanent card = game.getPermanent(cardId); + if (card != null && card.hasSubtype(SubType.AURA, game)) { + cardsToHand.add(card); + } + } cardsToHand.add(creature); controller.moveCards(cardsToHand, Zone.HAND, source, game); } diff --git a/Mage.Sets/src/mage/cards/k/KrovikanPlague.java b/Mage.Sets/src/mage/cards/k/KrovikanPlague.java index 7597a464d76..190b3415360 100644 --- a/Mage.Sets/src/mage/cards/k/KrovikanPlague.java +++ b/Mage.Sets/src/mage/cards/k/KrovikanPlague.java @@ -48,8 +48,10 @@ import mage.constants.Outcome; import mage.constants.Duration; import mage.constants.Zone; import mage.counters.BoostCounter; +import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.SubtypePredicate; import mage.filter.predicate.permanent.TappedPredicate; import mage.target.TargetPermanent; import mage.target.common.TargetControlledCreaturePermanent; @@ -60,6 +62,12 @@ import mage.target.common.TargetCreatureOrPlayer; * @author L_J */ public class KrovikanPlague extends CardImpl { + + private static final FilterControlledCreaturePermanent filterNonWall = new FilterControlledCreaturePermanent("non-Wall creature you control"); + + static { + filterNonWall.add(Predicates.not(new SubtypePredicate(SubType.WALL))); + } private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("enchanted creature is untapped"); @@ -71,8 +79,8 @@ public class KrovikanPlague extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{B}"); this.subtype.add(SubType.AURA); - // Enchant creature you control - TargetPermanent auraTarget = new TargetControlledCreaturePermanent(); + // Enchant non-Wall creature you control + TargetPermanent auraTarget = new TargetControlledCreaturePermanent(filterNonWall); this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); Ability ability = new EnchantAbility(auraTarget.getTargetName()); diff --git a/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java b/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java index c0128b767ba..1a69e274c63 100644 --- a/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java +++ b/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java @@ -32,6 +32,7 @@ import mage.abilities.Ability; import mage.abilities.SpellAbility; import mage.abilities.costs.common.PayLifeCost; import mage.abilities.effects.common.DestroyMultiTargetEffect; +import mage.abilities.effects.common.InfoEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -44,17 +45,17 @@ import mage.target.common.TargetCreaturePermanent; * @author escplan9 - Derek Monturo */ public class PhyrexianPurge extends CardImpl { - + public PhyrexianPurge(UUID ownerId, CardSetInfo setInfo) { - - super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}{R}"); - + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}{R}"); + // Destroy any number of target creatures. // Phyrexian Purge costs 3 life more to cast for each target. this.getSpellAbility().addTarget(new TargetCreaturePermanent(0, Integer.MAX_VALUE)); this.getSpellAbility().addEffect(new DestroyMultiTargetEffect()); + this.getSpellAbility().addEffect(new InfoEffect("

{this} costs 3 life more to cast for each target")); } - + @Override public void adjustCosts(Ability ability, Game game) { int numTargets = ability.getTargets().get(0).getTargets().size(); @@ -62,17 +63,17 @@ public class PhyrexianPurge extends CardImpl { ability.getCosts().add(new PayLifeCost(numTargets * 3)); } } - + @Override public void adjustTargets(Ability ability, Game game) { - if (ability instanceof SpellAbility) { - ability.getTargets().clear(); - Player you = game.getPlayer(ownerId); - int maxTargets = you.getLife() / 3; - ability.addTarget(new TargetCreaturePermanent(0, maxTargets)); - } + if (ability instanceof SpellAbility) { + ability.getTargets().clear(); + Player you = game.getPlayer(ownerId); + int maxTargets = you.getLife() / 3; + ability.addTarget(new TargetCreaturePermanent(0, maxTargets)); + } } - + public PhyrexianPurge(final PhyrexianPurge card) { super(card); } @@ -81,4 +82,4 @@ public class PhyrexianPurge extends CardImpl { public PhyrexianPurge copy() { return new PhyrexianPurge(this); } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/r/RootSliver.java b/Mage.Sets/src/mage/cards/r/RootSliver.java index 92a181ed9e5..facf6a51ffe 100644 --- a/Mage.Sets/src/mage/cards/r/RootSliver.java +++ b/Mage.Sets/src/mage/cards/r/RootSliver.java @@ -29,34 +29,39 @@ package mage.cards.r; import java.util.UUID; import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.common.CantBeCounteredControlledEffect; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; import mage.abilities.effects.common.CantBeCounteredSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.Zone; import mage.filter.FilterSpell; -import mage.filter.FilterStackObject; import mage.filter.predicate.mageobject.SubtypePredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; /** * * @author cbt33, BetaSteward (Autumn's Veil, Combust) */ public class RootSliver extends CardImpl { - + private static final FilterSpell filter = new FilterSpell("Sliver spells"); - + static { filter.add(new SubtypePredicate(SubType.SLIVER)); } - public RootSliver(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}"); this.subtype.add(SubType.SLIVER); this.power = new MageInt(2); @@ -65,7 +70,7 @@ public class RootSliver extends CardImpl { // Root Sliver can't be countered. this.addAbility(new SimpleStaticAbility(Zone.STACK, new CantBeCounteredSourceEffect())); // Sliver spells can't be countered by spells or abilities. - this.addAbility(new SimpleStaticAbility(Zone.STACK, new CantBeCounteredControlledEffect(filter, new FilterStackObject(), Duration.WhileOnBattlefield))); + this.addAbility(new SimpleStaticAbility(Zone.STACK, new RootSliverEffect())); } @@ -79,3 +84,42 @@ public class RootSliver extends CardImpl { } } +class RootSliverEffect extends ContinuousRuleModifyingEffectImpl { + + public RootSliverEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "Sliver spells can't be countered by spells or abilities."; + } + + public RootSliverEffect(final RootSliverEffect effect) { + super(effect); + } + + @Override + public RootSliverEffect copy() { + return new RootSliverEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.COUNTER; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Spell spell = game.getStack().getSpell(event.getTargetId()); + if (spell != null && spell.hasSubtype(SubType.SLIVER, game)) { + MageObject sourceObject = game.getObject(event.getSourceId()); + if (sourceObject != null && sourceObject instanceof StackObject) { + return true; + } + } + return false; + } + +} diff --git a/Mage.Sets/src/mage/cards/w/WanderwineProphets.java b/Mage.Sets/src/mage/cards/w/WanderwineProphets.java index ec1dc319225..0eaf95a4161 100644 --- a/Mage.Sets/src/mage/cards/w/WanderwineProphets.java +++ b/Mage.Sets/src/mage/cards/w/WanderwineProphets.java @@ -32,6 +32,7 @@ import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.turn.AddExtraTurnControllerEffect; import mage.abilities.keyword.ChampionAbility; import mage.cards.CardImpl; @@ -48,7 +49,7 @@ import mage.target.common.TargetControlledPermanent; */ public class WanderwineProphets extends CardImpl { - private static final FilterControlledPermanent filter = new FilterControlledPermanent("Merfolk"); + private static final FilterControlledPermanent filter = new FilterControlledPermanent("a Merfolk"); static { filter.add(new SubtypePredicate(SubType.MERFOLK)); @@ -64,8 +65,10 @@ public class WanderwineProphets extends CardImpl { // Champion a Merfolk this.addAbility(new ChampionAbility(this, SubType.MERFOLK, false)); // Whenever Wanderwine Prophets deals combat damage to a player, you may sacrifice a Merfolk. If you do, take an extra turn after this one. - Ability ability = new DealsCombatDamageToAPlayerTriggeredAbility(new AddExtraTurnControllerEffect(), true); - ability.addCost(new SacrificeTargetCost(new TargetControlledPermanent(1, 1, filter, true))); + Ability ability = new DealsCombatDamageToAPlayerTriggeredAbility(new DoIfCostPaid( + new AddExtraTurnControllerEffect(), + new SacrificeTargetCost(new TargetControlledPermanent(1, 1, filter, true)) + ), true); this.addAbility(ability); } diff --git a/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfNextEndStepDelayedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfNextEndStepDelayedTriggeredAbility.java index 3b533fa098d..7357c3bd418 100644 --- a/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfNextEndStepDelayedTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfNextEndStepDelayedTriggeredAbility.java @@ -59,7 +59,7 @@ public class AtTheBeginOfNextEndStepDelayedTriggeredAbility extends DelayedTrigg } public AtTheBeginOfNextEndStepDelayedTriggeredAbility(Zone zone, Effect effect, TargetController targetController, Condition condition) { - super(effect, Duration.EndOfTurn); + super(effect, Duration.Custom); this.zone = zone; this.targetController = targetController; this.condition = condition; diff --git a/Utils/cut.pl b/Utils/cut.pl index 674a7982d15..3b03a0a5b54 100644 --- a/Utils/cut.pl +++ b/Utils/cut.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl ## -# File : get_all.pl +# File : cut.pl # Author : spjspj ## @@ -22,6 +22,7 @@ use POSIX qw(strftime); print (" cut.pl full_text.txt keys 0 filegrep\n"); print (" cut.pl full_text.txt 0 0 make_code_bat\n"); print (" dir /a /b /s *.java | cut.pl stdin 0 0 make_code_bat > bob.bat\n"); + print (" dir /a /b /s *.xml | cut.pl stdin 0 0 make_code_bat > bob_xml.bat\n"); print (" cut.pl d:\\perl_programs output.*txt 7 age_dir | cut.pl list . 0 grep\n"); print (" cut.pl bob.txt 0 0 uniquelines \n"); print (" cut.pl file 0 0 strip_http\n"); @@ -331,16 +332,27 @@ use POSIX qw(strftime); { my $i; { - my $url = $term; - print ("Download :$url:\n"); - my $content = get $url; - print ("Saw " . length ($content) . " bytes!\n"); - print ("Save in $helper\n"); - open OUTPUT, "> " . $helper or die "No dice!"; - binmode (OUTPUT); - print OUTPUT $content; - close OUTPUT; - print $url, " >>> ", $helper, "\n"; + if (!(-f "$helper")) + { + my $url = $term; + print ("Download :$url:\n"); + my $content = get $url; + print ("Saw " . length ($content) . " bytes!\n"); + print ("Save in $helper\n"); + open OUTPUT, "> " . $helper or die "No dice!"; + binmode (OUTPUT); + print OUTPUT $content; + close OUTPUT; + print $url, " >>> ", $helper, "\n"; + } + else + { + print ("Found $helper existed already..\n"); + if (-s "$helper" == 0) + { + `del "$helper"`; + } + } } } if ($operation eq "grep") @@ -898,5 +910,4 @@ use POSIX qw(strftime); } } } - } diff --git a/Utils/get_modo_artids.pl b/Utils/get_modo_artids.pl new file mode 100644 index 00000000000..b55a732b3ca --- /dev/null +++ b/Utils/get_modo_artids.pl @@ -0,0 +1,281 @@ +#!/usr/bin/perl +## +# File : get_modo_artids.pl +# Date : 12/Nov/2017 +# Author : spjspj +# Purpose : Get the artid for each face art of each card from modo (mtgo) +# this then goes into some form of image such as: +# http://mtgoclientdepot.onlinegaming.wizards.com/Graphics/Cards/Pics/161922_typ_reg_sty_050.jpg +# "" /00100_typ_reg_sty_001.jpg +# "" /01440_typ_reg_sty_010.jpg +# "" /102392_typ_reg_sty_010.jpg +# "" /400603_typ_reg_sty_050.jpg +# This program expects that you've copied the relevant .xml files from an installation of mtgo +# into the current working directory for this perl script: +# For example: CARDNAME_STRING.xml, client_BNG.xml, +# Also, it expects to be running on windows +## + +use strict; +use LWP::Simple; +use POSIX qw(strftime); + + +# From xmage code - fix from MODO to XMAGE type set codes +my %fix_set_codes; +$fix_set_codes {"2U"} = "2ED"; +$fix_set_codes {"3E"} = "3ED"; +$fix_set_codes {"4E"} = "4ED"; +$fix_set_codes {"5E"} = "5ED"; +$fix_set_codes {"6E"} = "6ED"; +$fix_set_codes {"7E"} = "7ED"; +$fix_set_codes {"AL"} = "ALL"; +$fix_set_codes {"AP"} = "APC"; +$fix_set_codes {"AN"} = "ARN"; +$fix_set_codes {"AQ"} = "ATQ"; +$fix_set_codes {"CM1"} = "CMA"; +$fix_set_codes {"DD3_DVD"} = "DD3DVD"; +$fix_set_codes {"DD3_EVG"} = "DD3EVG"; +$fix_set_codes {"DD3_GLV"} = "DD3GLV"; +$fix_set_codes {"DD3_JVC"} = "DD3JVC"; +$fix_set_codes {"DK"} = "DRK"; +$fix_set_codes {"EX"} = "EXO"; +$fix_set_codes {"FE"} = "FEM"; +$fix_set_codes {"HM"} = "HML"; +$fix_set_codes {"IA"} = "ICE"; +$fix_set_codes {"IN"} = "INV"; +$fix_set_codes {"1E"} = "LEA"; +$fix_set_codes {"2E"} = "LEB"; +$fix_set_codes {"LE"} = "LEG"; +$fix_set_codes {"MI"} = "MIR"; +$fix_set_codes {"MM"} = "MMQ"; +$fix_set_codes {"MPS_KLD"} = "MPS"; +$fix_set_codes {"NE"} = "NEM"; +$fix_set_codes {"NE"} = "NMS"; +$fix_set_codes {"OD"} = "ODY"; +$fix_set_codes {"PR"} = "PCY"; +$fix_set_codes {"PS"} = "PLS"; +$fix_set_codes {"P2"} = "PO2"; +$fix_set_codes {"PO"} = "POR"; +$fix_set_codes {"PK"} = "PTK"; +$fix_set_codes {"ST"} = "STH"; +$fix_set_codes {"TE"} = "TMP"; +$fix_set_codes {"CG"} = "UDS"; +$fix_set_codes {"UD"} = "UDS"; +$fix_set_codes {"GU"} = "ULG"; +$fix_set_codes {"UZ"} = "USG"; +$fix_set_codes {"VI"} = "VIS"; +$fix_set_codes {"WL"} = "WTH"; + +# Main +{ + my $card = $ARGV [0]; + + # Example: Abandoned Sarcophagus + my $vals = `find /I "CARDNAME" *CARDNAME*STR*`; + my %ids; + my %set_names; + my $count = 0; + while ($vals =~ s/^.*CARDNAME_STRING_ITEM id='(.*?)'>(.*?)<\/CARDNAME_STRING.*\n//im) + { + $ids {$1} = $2; + $count++; + } + print ("Finished reading $count names\n"); + + + #$vals = `find /I "<" *lient*`; + $vals = `findstr /I "CARDNAME_STRING DIGITALOBJECT ARTID CLONE FRAMESTYLE " *lient* | find /I /V "_DO.xml"`; + + my $current_artid = ""; + my $current_clone_id = ""; + my $current_doc_id = ""; + my $current_line = ""; + my $current_name = ""; + my $current_set = ""; + my $num_set = 1; + my %docid_to_clone; + + while ($vals =~ s/^(.*)\n//im) + { + my $line = $1; + $line =~ m/^client_(.*)?\.xml/; + $current_set = $1; + +# *** IN +# *** IN +# +# *** IN +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# + + if ($line =~ m/