Face down images and cards rework (#11873)

Face down changes:
* GUI: added visible face down type and real card name for controller/owner (opponent can see it after game ends);
* GUI: added day/night button to view real card for controller/owner (opponent can see it after game ends);
* game: fixed that faced-down card can render symbols, abilities and other hidden data from a real card;
* images: added image support for normal faced-down cards;
* images: added image support for morph and megamorph faced-down cards;
* images: added image support for foretell faced-down cards;

Other changes:
* images: fixed missing tokens from DDD set;
* images: no more client restart to apply newly downloaded images or render settings;
* images: improved backface image quality (use main menu -> symbols to download it);
This commit is contained in:
Oleg Agafonov 2024-02-29 01:14:54 +04:00 committed by GitHub
parent 4901de12c1
commit e38a79f231
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
104 changed files with 2178 additions and 1495 deletions

View file

@ -1517,7 +1517,11 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
public void setConnectButtonText(String status) { public void setConnectButtonText(String status) {
this.btnConnect.setText(status); this.btnConnect.setText(status);
changeGUISize(); // Needed to layout the tooltbar after text length change
// Needed to layout the tooltbar after text length change
// TODO: need research, is it actual?
GUISizeHelper.refreshGUIAndCards();
this.btnConnect.repaint(); this.btnConnect.repaint();
this.btnConnect.revalidate(); this.btnConnect.revalidate();
} }
@ -1741,8 +1745,13 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
} }
} }
public void changeGUISize() { /**
ImageCaches.flush(); * Refresh whole GUI including cards and card images.
* Use it after new images downloaded, new fonts or theme settings selected.
*/
public void refreshGUIAndCards() {
ImageCaches.clearAll();
setGUISize(); setGUISize();
setGUISizeTooltipContainer(); setGUISizeTooltipContainer();

View file

@ -24,12 +24,12 @@ public class MageRoundPane extends JPanel {
private int Y_OFFSET = 30; private int Y_OFFSET = 30;
private final Color defaultBackgroundColor = new Color(141, 130, 112, 200); // color of the frame of the popup window private final Color defaultBackgroundColor = new Color(141, 130, 112, 200); // color of the frame of the popup window
private Color backgroundColor = defaultBackgroundColor; private Color backgroundColor = defaultBackgroundColor;
private static final SoftValuesLoadingCache<ShadowKey, BufferedImage> SHADOW_IMAGE_CACHE; private static final SoftValuesLoadingCache<ShadowKey, BufferedImage> ROUND_PANEL_SHADOW_IMAGES_CACHE;
private static final SoftValuesLoadingCache<Key, BufferedImage> IMAGE_CACHE; private static final SoftValuesLoadingCache<Key, BufferedImage> ROUND_PANEL_IMAGES_CACHE;
static { static {
SHADOW_IMAGE_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(MageRoundPane::createShadowImage)); ROUND_PANEL_IMAGES_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(MageRoundPane::createImage));
IMAGE_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(MageRoundPane::createImage)); ROUND_PANEL_SHADOW_IMAGES_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(MageRoundPane::createShadowImage));
} }
private static final class ShadowKey { private static final class ShadowKey {
@ -132,7 +132,7 @@ public class MageRoundPane extends JPanel {
@Override @Override
protected void paintComponent(Graphics g) { protected void paintComponent(Graphics g) {
g.drawImage(IMAGE_CACHE.getOrThrow(new Key(getWidth(), getHeight(), X_OFFSET, Y_OFFSET, backgroundColor)), 0, 0, null); g.drawImage(ROUND_PANEL_IMAGES_CACHE.getOrThrow(new Key(getWidth(), getHeight(), X_OFFSET, Y_OFFSET, backgroundColor)), 0, 0, null);
} }
private static BufferedImage createImage(Key key) { private static BufferedImage createImage(Key key) {
@ -146,7 +146,7 @@ public class MageRoundPane extends JPanel {
Graphics2D g2 = image.createGraphics(); Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
BufferedImage shadow = SHADOW_IMAGE_CACHE.getOrThrow(new ShadowKey(w, h)); BufferedImage shadow = ROUND_PANEL_SHADOW_IMAGES_CACHE.getOrThrow(new ShadowKey(w, h));
{ {
int xOffset = (shadow.getWidth() - w) / 2; int xOffset = (shadow.getWidth() - w) / 2;

View file

@ -19,6 +19,7 @@ import mage.client.util.audio.AudioManager;
import mage.client.util.sets.ConstructedFormats; import mage.client.util.sets.ConstructedFormats;
import mage.components.ImagePanel; import mage.components.ImagePanel;
import mage.components.ImagePanelStyle; import mage.components.ImagePanelStyle;
import mage.constants.MageObjectType;
import mage.game.command.Dungeon; import mage.game.command.Dungeon;
import mage.game.command.Emblem; import mage.game.command.Emblem;
import mage.game.command.Plane; import mage.game.command.Plane;
@ -396,7 +397,7 @@ public class MageBook extends JComponent {
draftRating.setBounds(rectangle.x, rectangle.y + cardImg.getCardLocation().getCardHeight() + dy, cardDimensions.getFrameWidth(), 20); draftRating.setBounds(rectangle.x, rectangle.y + cardImg.getCardLocation().getCardHeight() + dy, cardDimensions.getFrameWidth(), 20);
draftRating.setHorizontalAlignment(SwingConstants.CENTER); draftRating.setHorizontalAlignment(SwingConstants.CENTER);
draftRating.setFont(jLayeredPane.getFont().deriveFont(jLayeredPane.getFont().getStyle() | Font.BOLD)); draftRating.setFont(jLayeredPane.getFont().deriveFont(jLayeredPane.getFont().getStyle() | Font.BOLD));
if (card.isOriginalACard()) { if (card.getMageObjectType().equals(MageObjectType.CARD)) {
// card // card
draftRating.setText("draft rating: " + RateCard.rateCard(card, null)); draftRating.setText("draft rating: " + RateCard.rateCard(card, null));
} else { } else {

View file

@ -87,8 +87,8 @@ public class MageCardComparator implements CardViewComparator {
bCom = RateCard.rateCard(b, null); bCom = RateCard.rateCard(b, null);
break; break;
case 10: case 10:
aCom = a.getColorIdentityStr(); aCom = a.getOriginalColorIdentity();
bCom = b.getColorIdentityStr(); bCom = b.getOriginalColorIdentity();
break; break;
default: default:
break; break;

View file

@ -255,7 +255,7 @@ public class TableModel extends AbstractTableModel implements ICardGrid {
case 9: case 9:
return RateCard.rateCard(c, null); return RateCard.rateCard(c, null);
case 10: case 10:
return ManaSymbols.getClearManaCost(c.getColorIdentityStr()); return ManaSymbols.getClearManaCost(c.getOriginalColorIdentity());
default: default:
return "error"; return "error";
} }

View file

@ -2928,8 +2928,8 @@ public class PreferencesDialog extends javax.swing.JDialog {
save(prefs, dialog.sliderCardSizeMinBattlefield, KEY_GUI_CARD_BATTLEFIELD_MIN_SIZE, "true", "false", UPDATE_CACHE_POLICY); save(prefs, dialog.sliderCardSizeMinBattlefield, KEY_GUI_CARD_BATTLEFIELD_MIN_SIZE, "true", "false", UPDATE_CACHE_POLICY);
save(prefs, dialog.sliderCardSizeMaxBattlefield, KEY_GUI_CARD_BATTLEFIELD_MAX_SIZE, "true", "false", UPDATE_CACHE_POLICY); save(prefs, dialog.sliderCardSizeMaxBattlefield, KEY_GUI_CARD_BATTLEFIELD_MAX_SIZE, "true", "false", UPDATE_CACHE_POLICY);
// do as worker job // refresh full GUI with new settings
GUISizeHelper.changeGUISize(); GUISizeHelper.refreshGUIAndCards();
} }
private void exitButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exitButtonActionPerformed private void exitButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exitButtonActionPerformed

View file

@ -2,6 +2,7 @@ package mage.client.dialog;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
import mage.abilities.icon.*; import mage.abilities.icon.*;
import mage.abilities.keyword.TransformAbility; import mage.abilities.keyword.TransformAbility;
import mage.cards.*; import mage.cards.*;
@ -184,6 +185,18 @@ public class TestCardRenderDialog extends MageDialog {
if (perm.isTransformable()) { if (perm.isTransformable()) {
perm.setTransformed(true); perm.setTransformed(true);
} }
// workaround to apply face down image and other settings
if (perm.isFaceDown(game)) {
BecomesFaceDownCreatureEffect.makeFaceDownObject(
game,
null,
perm,
BecomesFaceDownCreatureEffect.findFaceDownType(game, perm),
null
);
}
PermanentView cardView = new PermanentView(perm, permCard, controllerId, game); PermanentView cardView = new PermanentView(perm, permCard, controllerId, game);
cardView.setInViewerOnly(false); // must false for face down cardView.setInViewerOnly(false); // must false for face down
return cardView; return cardView;
@ -386,9 +399,9 @@ public class TestCardRenderDialog extends MageDialog {
List<CardView> cardViews = new ArrayList<>(); List<CardView> cardViews = new ArrayList<>();
/* test morphed //* test face down
cardViews.add(createPermanentCard(game, playerYou.getId(), "RNA", "263", 0, 0, 0, false, null)); // mountain cardViews.add(createPermanentCard(game, playerYou.getId(), "RNA", "263", 0, 0, 0, false, false, null)); // mountain
cardViews.add(createPermanentCard(game, playerYou.getId(), "RNA", "185", 0, 0, 0, true, null)); // Judith, the Scourge Diva cardViews.add(createPermanentCard(game, playerYou.getId(), "RNA", "185", 0, 0, 0, true, false, null)); // Judith, the Scourge Diva
cardViews.add(createHandCard(game, playerYou.getId(), "DIS", "153")); // Odds // Ends (split card) cardViews.add(createHandCard(game, playerYou.getId(), "DIS", "153")); // Odds // Ends (split card)
cardViews.add(createHandCard(game, playerYou.getId(), "ELD", "38")); // Animating Faerie (adventure card) cardViews.add(createHandCard(game, playerYou.getId(), "ELD", "38")); // Animating Faerie (adventure card)
cardViews.add(createFaceDownCard(game, playerOpponent.getId(), "ELD", "38", false, false, false)); // face down cardViews.add(createFaceDownCard(game, playerOpponent.getId(), "ELD", "38", false, false, false)); // face down
@ -414,7 +427,7 @@ public class TestCardRenderDialog extends MageDialog {
cardViews.add(createHandCard(game, playerYou.getId(), "AKH", "210")); // Dusk // Dawn cardViews.add(createHandCard(game, playerYou.getId(), "AKH", "210")); // Dusk // Dawn
//*/ //*/
//* test adventure cards in hands /* test adventure cards in hands
cardViews.add(createHandCard(game, playerYou.getId(), "ELD", "14")); // Giant Killer cardViews.add(createHandCard(game, playerYou.getId(), "ELD", "14")); // Giant Killer
cardViews.add(createHandCard(game, playerYou.getId(), "WOE", "222")); // Cruel Somnophage cardViews.add(createHandCard(game, playerYou.getId(), "WOE", "222")); // Cruel Somnophage
cardViews.add(createHandCard(game, playerYou.getId(), "WOE", "227")); // Gingerbread Hunter cardViews.add(createHandCard(game, playerYou.getId(), "WOE", "227")); // Gingerbread Hunter
@ -430,7 +443,7 @@ public class TestCardRenderDialog extends MageDialog {
cardViews.add(createHandCard(game, playerYou.getId(), "MKM", "155")); // Case of the Locked Hothouse cardViews.add(createHandCard(game, playerYou.getId(), "MKM", "155")); // Case of the Locked Hothouse
//*/ //*/
//* test case, class and saga cards in hands /* test case, class and saga cards in hands
cardViews.add(createHandCard(game, playerYou.getId(), "MKM", "113")); // Case of the Burning Masks cardViews.add(createHandCard(game, playerYou.getId(), "MKM", "113")); // Case of the Burning Masks
cardViews.add(createHandCard(game, playerYou.getId(), "MKM", "155")); // Case of the Locked Hothouse cardViews.add(createHandCard(game, playerYou.getId(), "MKM", "155")); // Case of the Locked Hothouse
cardViews.add(createHandCard(game, playerYou.getId(), "AFR", "6")); // Cleric Class cardViews.add(createHandCard(game, playerYou.getId(), "AFR", "6")); // Cleric Class
@ -504,7 +517,7 @@ public class TestCardRenderDialog extends MageDialog {
PermanentView oldPermanent = (PermanentView) main.getGameCard(); PermanentView oldPermanent = (PermanentView) main.getGameCard();
PermanentView newPermament = new PermanentView( PermanentView newPermament = new PermanentView(
oldPermanent, oldPermanent,
game.getCard(oldPermanent.getOriginalId()), game.getCard(oldPermanent.getId()),
UUID.randomUUID(), UUID.randomUUID(),
game game
); );

View file

@ -720,7 +720,7 @@ public class MageActionCallback implements ActionCallback {
switch (enlargeMode) { switch (enlargeMode) {
case COPY: case COPY:
if (cardView instanceof PermanentView) { if (cardView instanceof PermanentView) {
image = ImageCache.getImageOriginal(((PermanentView) cardView).getOriginal()).getImage(); image = ImageCache.getCardImageOriginal(((PermanentView) cardView).getOriginal()).getImage();
} }
break; break;
case ALTERNATE: case ALTERNATE:
@ -729,10 +729,14 @@ public class MageActionCallback implements ActionCallback {
&& !cardView.isFlipCard() && !cardView.isFlipCard()
&& !cardView.canTransform() && !cardView.canTransform()
&& ((PermanentView) cardView).isCopy()) { && ((PermanentView) cardView).isCopy()) {
image = ImageCache.getImageOriginal(((PermanentView) cardView).getOriginal()).getImage(); image = ImageCache.getCardImageOriginal(((PermanentView) cardView).getOriginal()).getImage();
} else { } else {
image = ImageCache.getImageOriginalAlternateName(cardView).getImage(); image = ImageCache.getCardImageAlternate(cardView).getImage();
displayCard = displayCard.getSecondCardFace(); displayCard = displayCard.getSecondCardFace();
if (displayCard == null) {
// opponent's face down cards are hidden, so no alternative
displayCard = cardPanel.getOriginal();
}
} }
} }
break; break;
@ -745,7 +749,6 @@ public class MageActionCallback implements ActionCallback {
} else { } else {
logger.warn("No Card preview Pane in Mage Frame defined. Card: " + cardView.getName()); logger.warn("No Card preview Pane in Mage Frame defined. Card: " + cardView.getName());
} }
} catch (Exception e) { } catch (Exception e) {
logger.warn("Problem dring display of enlarged card", e); logger.warn("Problem dring display of enlarged card", e);
} }
@ -786,6 +789,7 @@ public class MageActionCallback implements ActionCallback {
private void displayCardInfo(CardView card, Image image, BigCard bigCard) { private void displayCardInfo(CardView card, Image image, BigCard bigCard) {
if (image instanceof BufferedImage) { if (image instanceof BufferedImage) {
// IMAGE MODE
// XXX: scaled to fit width // XXX: scaled to fit width
bigCard.setCard(card.getId(), enlargeMode, image, card.getRules(), card.isToRotate()); bigCard.setCard(card.getId(), enlargeMode, image, card.getRules(), card.isToRotate());
// if it's an ability, show only the ability text as overlay // if it's an ability, show only the ability text as overlay
@ -795,6 +799,7 @@ public class MageActionCallback implements ActionCallback {
bigCard.hideTextComponent(); bigCard.hideTextComponent();
} }
} else { } else {
// TEXT MODE
JXPanel panel = GuiDisplayUtil.getDescription(card, bigCard.getWidth(), bigCard.getHeight()); JXPanel panel = GuiDisplayUtil.getDescription(card, bigCard.getWidth(), bigCard.getHeight());
panel.setVisible(true); panel.setVisible(true);
bigCard.hideTextComponent(); bigCard.hideTextComponent();

View file

@ -2,8 +2,9 @@ package mage.client.themes;
import mage.abilities.hint.HintUtils; import mage.abilities.hint.HintUtils;
import mage.abilities.icon.CardIconColor; import mage.abilities.icon.CardIconColor;
import mage.client.util.GUISizeHelper;
import mage.client.util.ImageCaches;
import org.mage.card.arcane.SvgUtils; import org.mage.card.arcane.SvgUtils;
import org.mage.plugins.card.images.ImageCache;
import java.awt.*; import java.awt.*;
@ -350,6 +351,6 @@ public enum ThemeType {
} }
// reload card icons and other rendering things from cache - it can depend on current theme // reload card icons and other rendering things from cache - it can depend on current theme
ImageCache.clearCache(); GUISizeHelper.refreshGUIAndCards();
} }
} }

View file

@ -9,7 +9,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public final class CardsViewUtil { public final class CardsViewUtil {

View file

@ -84,9 +84,11 @@ public final class GUISizeHelper {
return new Font("Arial", Font.PLAIN, 14); return new Font("Arial", Font.PLAIN, 14);
} }
public static void changeGUISize() { public static void refreshGUIAndCards() {
calculateGUISizes(); calculateGUISizes();
MageFrame.getInstance().changeGUISize(); if (MageFrame.getInstance() != null) {
MageFrame.getInstance().refreshGUIAndCards();
}
} }
public static void calculateGUISizes() { public static void calculateGUISizes() {

View file

@ -1,11 +1,11 @@
package mage.client.util; package mage.client.util;
import java.util.ArrayList;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import java.util.ArrayList;
/** /**
* GUI: collect info about all used image caches, so it can be cleared from a single place
* *
* @author draxdyn * @author draxdyn
*/ */
@ -22,7 +22,11 @@ public final class ImageCaches {
return map; return map;
} }
public static void flush() { /**
* Global method to clear all images cache.
* Warning, GUI must be refreshed too for card updates, so use GUISizeHelper.refreshGUIAndCards instead
*/
public static void clearAll() {
for (Cache<?, ?> map : IMAGE_CACHES) { for (Cache<?, ?> map : IMAGE_CACHES) {
map.invalidateAll(); map.invalidateAll();
} }

View file

@ -55,11 +55,10 @@ public final class TransformedImageCache {
} }
} }
private static final SoftValuesLoadingCache<Key, SoftValuesLoadingCache<BufferedImage, BufferedImage>> IMAGE_CACHE; private static final SoftValuesLoadingCache<Key, SoftValuesLoadingCache<BufferedImage, BufferedImage>> TRANSFORMED_IMAGES_CACHE;
static { static {
// TODO: can we use a single map? TRANSFORMED_IMAGES_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(TransformedImageCache::createTransformedImageCache));
IMAGE_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(TransformedImageCache::createTransformedImageCache));
} }
private static SoftValuesLoadingCache<BufferedImage, BufferedImage> createTransformedImageCache(Key key) { private static SoftValuesLoadingCache<BufferedImage, BufferedImage> createTransformedImageCache(Key key) {
@ -139,6 +138,6 @@ public final class TransformedImageCache {
if (resHeight < 3) { if (resHeight < 3) {
resHeight = 3; resHeight = 3;
} }
return IMAGE_CACHE.getOrThrow(new Key(resWidth, resHeight, angle)).getOrThrow(image); return TRANSFORMED_IMAGES_CACHE.getOrThrow(new Key(resWidth, resHeight, angle)).getOrThrow(image);
} }
} }

View file

@ -377,7 +377,7 @@ public abstract class CardPanel extends MagePermanent implements ComponentListen
protected void paintComponent(Graphics g) { protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) (g.create()); Graphics2D g2d = (Graphics2D) (g.create());
// Deferr to subclasses // Defer to subclasses
paintCard(g2d); paintCard(g2d);
// Done, dispose of the context // Done, dispose of the context
@ -854,7 +854,7 @@ public abstract class CardPanel extends MagePermanent implements ComponentListen
// VIEW mode (user can change card side at any time by n/d button) // VIEW mode (user can change card side at any time by n/d button)
this.guiTransformed = !this.guiTransformed; this.guiTransformed = !this.guiTransformed;
if (dayNightButton != null) { // if transformbable card is copied, button can be null if (dayNightButton != null) { // if transformable card is copied, button can be null
BufferedImage image = this.isTransformed() ? ImageManagerImpl.instance.getNightImage() : ImageManagerImpl.instance.getDayImage(); BufferedImage image = this.isTransformed() ? ImageManagerImpl.instance.getNightImage() : ImageManagerImpl.instance.getDayImage();
dayNightButton.setIcon(new ImageIcon(image)); dayNightButton.setIcon(new ImageIcon(image));
} }

View file

@ -14,8 +14,6 @@ import mage.constants.SubType;
import mage.util.DebugUtil; import mage.util.DebugUtil;
import mage.view.CardView; import mage.view.CardView;
import mage.view.CounterView; import mage.view.CounterView;
import mage.view.PermanentView;
import mage.view.StackAbilityView;
import org.jdesktop.swingx.graphics.GraphicsUtilities; import org.jdesktop.swingx.graphics.GraphicsUtilities;
import org.mage.plugins.card.images.ImageCache; import org.mage.plugins.card.images.ImageCache;
import org.mage.plugins.card.images.ImageCacheData; import org.mage.plugins.card.images.ImageCacheData;
@ -37,7 +35,7 @@ public class CardPanelRenderModeImage extends CardPanel {
private static final long serialVersionUID = -3272134219262184411L; private static final long serialVersionUID = -3272134219262184411L;
private static final SoftValuesLoadingCache<Key, BufferedImage> IMAGE_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(CardPanelRenderModeImage::createImage)); private static final SoftValuesLoadingCache<Key, BufferedImage> IMAGE_MODE_RENDERED_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(CardPanelRenderModeImage::createImage));
private static final int WIDTH_LIMIT = 90; // card width limit to create smaller counter private static final int WIDTH_LIMIT = 90; // card width limit to create smaller counter
@ -472,11 +470,7 @@ public class CardPanelRenderModeImage extends CardPanel {
@Override @Override
public Image getImage() { public Image getImage() {
if (this.hasImage) { if (this.hasImage) {
if (getGameCard().isFaceDown()) { return ImageCache.getCardImageOriginal(getGameCard()).getImage();
return getFaceDownImage().getImage();
} else {
return ImageCache.getImageOriginal(getGameCard()).getImage();
}
} }
return null; return null;
} }
@ -492,7 +486,7 @@ public class CardPanelRenderModeImage extends CardPanel {
// draw background (selected/chooseable/playable) // draw background (selected/chooseable/playable)
MageCardLocation cardLocation = getCardLocation(); MageCardLocation cardLocation = getCardLocation();
g2d.drawImage( g2d.drawImage(
IMAGE_CACHE.getOrThrow( IMAGE_MODE_RENDERED_CACHE.getOrThrow(
new Key(getInsets(), new Key(getInsets(),
cardLocation.getCardWidth(), cardLocation.getCardHeight(), cardLocation.getCardWidth(), cardLocation.getCardHeight(),
cardLocation.getCardWidth(), cardLocation.getCardHeight(), cardLocation.getCardWidth(), cardLocation.getCardHeight(),
@ -640,14 +634,9 @@ public class CardPanelRenderModeImage extends CardPanel {
Util.threadPool.submit(() -> { Util.threadPool.submit(() -> {
try { try {
final ImageCacheData data; ImageCacheData data = ImageCache.getCardImage(getGameCard(), getCardWidth(), getCardHeight());
if (getGameCard().isFaceDown()) {
data = getFaceDownImage();
} else {
data = ImageCache.getImage(getGameCard(), getCardWidth(), getCardHeight());
}
// show path on miss image // save missing image
if (data.getImage() == null) { if (data.getImage() == null) {
setFullPath(data.getPath()); setFullPath(data.getPath());
} }
@ -665,21 +654,6 @@ public class CardPanelRenderModeImage extends CardPanel {
}); });
} }
private ImageCacheData getFaceDownImage() {
// TODO: add download default images
if (isPermanent() && getGameCard() instanceof PermanentView) {
if (((PermanentView) getGameCard()).isMorphed()) {
return ImageCache.getMorphImage();
} else {
return ImageCache.getManifestImage();
}
} else if (this.getGameCard() instanceof StackAbilityView) {
return ImageCache.getMorphImage();
} else {
return ImageCache.getCardbackImage();
}
}
private int getManaWidth(String manaCost, int symbolMarginX) { private int getManaWidth(String manaCost, int symbolMarginX) {
int width = 0; int width = 0;
manaCost = manaCost.replace("\\", ""); manaCost = manaCost.replace("\\", "");

View file

@ -3,46 +3,40 @@ package org.mage.card.arcane;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import mage.cards.action.ActionCallback; import mage.cards.action.ActionCallback;
import mage.client.util.ImageCaches;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import mage.view.CardView; import mage.view.CardView;
import mage.view.CounterView; import mage.view.CounterView;
import mage.view.PermanentView; import mage.view.PermanentView;
import mage.view.StackAbilityView;
import org.jdesktop.swingx.graphics.GraphicsUtilities; import org.jdesktop.swingx.graphics.GraphicsUtilities;
import org.mage.plugins.card.images.ImageCache; import org.mage.plugins.card.images.ImageCache;
import org.mage.plugins.card.images.ImageCacheData;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* Render mode: MTGO * Render mode: MTGO
*
*/ */
public class CardPanelRenderModeMTGO extends CardPanel { public class CardPanelRenderModeMTGO extends CardPanel {
//
// https://www.mtg.onl/evolution-of-magic-token-card-frame-design/ // https://www.mtg.onl/evolution-of-magic-token-card-frame-design/
// Map of generated images private static final Cache<ImageKey, BufferedImage> MTGO_MODE_RENDERED_CACHE = ImageCaches.register(
private static final Cache<ImageKey, BufferedImage> IMAGE_CACHE = CacheBuilder CacheBuilder
.newBuilder() .newBuilder()
.maximumSize(3000) .maximumSize(3000)
.expireAfterAccess(60, TimeUnit.MINUTES) .expireAfterAccess(60, TimeUnit.MINUTES)
.softValues() .softValues()
.build(); .build()
);
// The art image for the card, loaded in from the disk // The art image for the card, loaded in from the disk
private BufferedImage artImage; 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 // Factory to generate card appropriate views
private final CardRendererFactory cardRendererFactory = new CardRendererFactory(); private final CardRendererFactory cardRendererFactory = new CardRendererFactory();
@ -161,11 +155,7 @@ public class CardPanelRenderModeMTGO extends CardPanel {
if (artImage == null) { if (artImage == null) {
return null; return null;
} }
if (getGameCard().isFaceDown()) { return ImageCache.getCardImageOriginal(getGameCard()).getImage();
return getFaceDownImage().getImage();
} else {
return ImageCache.getImageOriginal(getGameCard()).getImage();
}
} }
@Override @Override
@ -173,16 +163,21 @@ public class CardPanelRenderModeMTGO extends CardPanel {
// Render the card if we don't have an image ready to use // Render the card if we don't have an image ready to use
if (cardImage == null) { if (cardImage == null) {
// Try to get card image from cache based on our card characteristics // Try to get card image from cache based on our card characteristics
ImageKey key = new ImageKey(getGameCard(), artImage, ImageKey key = new ImageKey(
getCardWidth(), getCardHeight(), getGameCard(),
isChoosable(), isSelected(), isTransformed()); artImage,
getCardWidth(),
getCardHeight(),
isChoosable(),
isSelected(),
isTransformed()
);
try { try {
cardImage = IMAGE_CACHE.get(key, this::renderCard); cardImage = MTGO_MODE_RENDERED_CACHE.get(key, this::renderCard);
} catch (ExecutionException e) { } catch (Exception e) {
// TODO: research and replace with logs, message and backface image
throw new RuntimeException(e); throw new RuntimeException(e);
} }
// No cached copy exists? Render one and cache it
} }
// And draw the image we now have // And draw the image we now have
@ -237,8 +232,6 @@ public class CardPanelRenderModeMTGO extends CardPanel {
// Use the art image and current rendered image from the card // Use the art image and current rendered image from the card
artImage = impl.artImage; artImage = impl.artImage;
cardRenderer.setArtImage(artImage); cardRenderer.setArtImage(artImage);
faceArtImage = impl.faceArtImage;
cardRenderer.setFaceArtImage(faceArtImage);
cardImage = impl.cardImage; cardImage = impl.cardImage;
} }
} }
@ -252,7 +245,6 @@ public class CardPanelRenderModeMTGO extends CardPanel {
cardImage = null; cardImage = null;
cardRenderer = cardRendererFactory.create(getGameCard()); cardRenderer = cardRendererFactory.create(getGameCard());
cardRenderer.setArtImage(artImage); cardRenderer.setArtImage(artImage);
cardRenderer.setFaceArtImage(faceArtImage);
// Repaint // Repaint
repaint(); repaint();
@ -264,7 +256,6 @@ public class CardPanelRenderModeMTGO extends CardPanel {
artImage = null; artImage = null;
cardImage = null; cardImage = null;
cardRenderer.setArtImage(null); cardRenderer.setArtImage(null);
cardRenderer.setFaceArtImage(null);
// Stop animation // Stop animation
setTappedAngle(isTapped() ? CardPanel.TAPPED_ANGLE : 0); setTappedAngle(isTapped() ? CardPanel.TAPPED_ANGLE : 0);
@ -276,29 +267,18 @@ public class CardPanelRenderModeMTGO extends CardPanel {
// See if the image is already loaded // See if the image is already loaded
//artImage = ImageCache.tryGetImage(gameCard, getCardWidth(), getCardHeight()); //artImage = ImageCache.tryGetImage(gameCard, getCardWidth(), getCardHeight());
//this.cardRenderer.setArtImage(artImage); //this.cardRenderer.setArtImage(artImage);
// Submit a task to draw with the card art when it arrives // Submit a task to draw with the card art when it arrives
if (artImage == null) { if (artImage == null) {
final int stamp = ++updateArtImageStamp; final int stamp = ++updateArtImageStamp;
Util.threadPool.submit(() -> { Util.threadPool.submit(() -> {
try { try {
final BufferedImage srcImage; final BufferedImage srcImage;
final BufferedImage faceArtSrcImage; srcImage = ImageCache.getCardImage(getGameCard(), getCardWidth(), getCardHeight()).getImage();
if (getGameCard().isFaceDown()) {
// Nothing to do
srcImage = null;
faceArtSrcImage = null;
} else {
srcImage = ImageCache.getImage(getGameCard(), getCardWidth(), getCardHeight()).getImage();
faceArtSrcImage = ImageCache.getFaceImage(getGameCard(), getCardWidth(), getCardHeight()).getImage();
}
UI.invokeLater(() -> { UI.invokeLater(() -> {
if (stamp == updateArtImageStamp) { if (stamp == updateArtImageStamp) {
artImage = srcImage; artImage = srcImage;
cardRenderer.setArtImage(srcImage); cardRenderer.setArtImage(srcImage);
faceArtImage = faceArtSrcImage;
cardRenderer.setFaceArtImage(faceArtSrcImage);
if (srcImage != null) { if (srcImage != null) {
// Invalidate and repaint // Invalidate and repaint
cardImage = null; cardImage = null;
@ -317,21 +297,6 @@ public class CardPanelRenderModeMTGO extends CardPanel {
return new CardPanelAttributes(getCardWidth(), getCardHeight(), isChoosable(), isSelected(), isTransformed()); return new CardPanelAttributes(getCardWidth(), getCardHeight(), isChoosable(), isSelected(), isTransformed());
} }
private ImageCacheData getFaceDownImage() {
// TODO: add download default images
if (isPermanent() && getGameCard() instanceof PermanentView) {
if (((PermanentView) getGameCard()).isMorphed()) {
return ImageCache.getMorphImage();
} else {
return ImageCache.getManifestImage();
}
} else if (this.getGameCard() instanceof StackAbilityView) {
return ImageCache.getMorphImage();
} else {
return ImageCache.getCardbackImage();
}
}
/** /**
* Render the card to a new BufferedImage at it's current dimensions * Render the card to a new BufferedImage at it's current dimensions
* *

View file

@ -55,10 +55,7 @@ public abstract class CardRenderer {
protected final CardView cardView; protected final CardView cardView;
// The card image // The card image
protected BufferedImage artImage; protected BufferedImage artImage; // TODO: make sure it changed/reset on face down/up change
// The face card image
protected BufferedImage faceArtImage;
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Common layout metrics between all cards // Common layout metrics between all cards
@ -206,7 +203,7 @@ public abstract class CardRenderer {
} }
// The Draw Method // The Draw Method
// The draw method takes the information caculated by the constructor // The draw method takes the information calculated by the constructor
// and uses it to draw to a concrete size of card and graphics. // and uses it to draw to a concrete size of card and graphics.
public void draw(Graphics2D g, CardPanelAttributes attribs, BufferedImage image) { public void draw(Graphics2D g, CardPanelAttributes attribs, BufferedImage image) {
@ -313,51 +310,6 @@ public abstract class CardRenderer {
} }
private boolean lessOpaqueRulesTextBox = false; private boolean lessOpaqueRulesTextBox = false;
protected void drawFaceArtIntoRect(Graphics2D g, int x, int y, int w, int h, int alternate_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);
if (fullCardImgWidth > fullCardImgHeight) {
g.drawImage(faceArtImage,
x, y,
(int) targetWidth, (int) targetHeight,
null);
} else {
g.drawImage(faceArtImage,
x, y,
(int) targetWidth, alternate_h, // alernate_h is roughly (targetWidth / 0.74)
null);
lessOpaqueRulesTextBox = true;
}
} 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 // Draw +1/+1 and other counters
protected void drawCounters(Graphics2D g) { protected void drawCounters(Graphics2D g) {
@ -532,10 +484,4 @@ public abstract class CardRenderer {
public void setArtImage(Image image) { public void setArtImage(Image image) {
artImage = CardRendererUtils.toBufferedImage(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);
}
} }

View file

@ -28,7 +28,7 @@ public class GlowText extends JLabel {
private Color glowColor; private Color glowColor;
private boolean wrap; private boolean wrap;
private int lineCount = 0; private int lineCount = 0;
private static final SoftValuesLoadingCache<Key, BufferedImage> IMAGE_CACHE; private static final SoftValuesLoadingCache<Key, BufferedImage> GLOW_TEXT_IMAGES_CACHE;
private static final class Key { private static final class Key {
@ -122,7 +122,7 @@ public class GlowText extends JLabel {
} }
static { static {
IMAGE_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(GlowText::createGlowImage)); GLOW_TEXT_IMAGES_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(GlowText::createGlowImage));
} }
public void setGlow(Color glowColor, int size, float intensity) { public void setGlow(Color glowColor, int size, float intensity) {
@ -153,7 +153,7 @@ public class GlowText extends JLabel {
} }
public BufferedImage getGlowImage() { public BufferedImage getGlowImage() {
return IMAGE_CACHE.getOrThrow(new Key(getWidth(), getHeight(), getText(), getFont(), getForeground(), glowSize, glowIntensity, glowColor, wrap)); return GLOW_TEXT_IMAGES_CACHE.getOrThrow(new Key(getWidth(), getHeight(), getText(), getFont(), getForeground(), glowSize, glowIntensity, glowColor, wrap));
} }
private static BufferedImage createGlowImage(Key key) { private static BufferedImage createGlowImage(Key key) {

View file

@ -8,6 +8,7 @@ import mage.client.dialog.PreferencesDialog;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.MageObjectType; import mage.constants.MageObjectType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.util.CardUtil;
import mage.util.SubTypes; import mage.util.SubTypes;
import mage.view.CardView; import mage.view.CardView;
import mage.view.PermanentView; import mage.view.PermanentView;
@ -26,27 +27,6 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
/*
private void cardRendererBasedRender(Graphics2D g) {
// Prepare for draw
g.translate(cardXOffset, cardYOffset);
int cardWidth = this.cardWidth - cardXOffset;
int cardHeight = this.cardHeight - cardYOffset;
// AA on
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Renderer
CardRenderer render = new ModernCardRenderer(gameCard, transformed);
Image img = imagePanel.getSrcImage();
if (img != null) {
render.setArtImage(img);
}
render.draw(g, cardWidth, cardHeight);
}
*/
/** /**
* @author stravant@gmail.com, JayDi85 * @author stravant@gmail.com, JayDi85
* <p> * <p>
@ -285,7 +265,8 @@ public class ModernCardRenderer extends CardRenderer {
protected void drawBackground(Graphics2D g) { protected void drawBackground(Graphics2D g) {
// Draw background, in 3 parts // Draw background, in 3 parts
if (cardView.isFaceDown()) { if (false && cardView.isFaceDown()) {
// TODO: delete un-used code?!
// Just draw a brown rectangle // Just draw a brown rectangle
drawCardBack(g); drawCardBack(g);
} else { } else {
@ -392,16 +373,9 @@ public class ModernCardRenderer extends CardRenderer {
@Override @Override
protected void drawArt(Graphics2D g) { protected void drawArt(Graphics2D g) {
if ((artImage != null || faceArtImage != null) && !cardView.isFaceDown()) { if (artImage != null) {
boolean useFaceArt = false;
if (faceArtImage != null && !isZendikarFullArtLand()) {
useFaceArt = true;
}
// Invention rendering, art fills the entire frame // Invention rendering, art fills the entire frame
if (useInventionFrame()) { if (useInventionFrame()) {
useFaceArt = false;
drawArtIntoRect(g, drawArtIntoRect(g,
borderWidth, borderWidth, borderWidth, borderWidth,
cardWidth - 2 * borderWidth, cardHeight - 2 * borderWidth, cardWidth - 2 * borderWidth, cardHeight - 2 * borderWidth,
@ -412,7 +386,6 @@ public class ModernCardRenderer extends CardRenderer {
Rectangle2D sourceRect = getArtRect(); Rectangle2D sourceRect = getArtRect();
if (cardView.getMageObjectType() == MageObjectType.SPELL) { if (cardView.getMageObjectType() == MageObjectType.SPELL) {
useFaceArt = false;
ArtRect rect = cardView.getArtRect(); ArtRect rect = cardView.getArtRect();
if (rect != ArtRect.NORMAL) { if (rect != ArtRect.NORMAL) {
sourceRect = rect.rect; sourceRect = rect.rect;
@ -421,14 +394,7 @@ public class ModernCardRenderer extends CardRenderer {
} }
// Normal drawing of art from a source part of the card frame into the rect // Normal drawing of art from a source part of the card frame into the rect
if (useFaceArt) { if (cardView.getArtRect() == ArtRect.FULL_LENGTH_RIGHT) {
int alternate_height = cardHeight - boxHeight * 2 - totalContentInset;
drawFaceArtIntoRect(g,
totalContentInset + 1, totalContentInset + boxHeight,
contentWidth - 2, typeLineY - totalContentInset - boxHeight,
alternate_height,
sourceRect, shouldPreserveAspect);
} else if (cardView.getArtRect() == ArtRect.FULL_LENGTH_RIGHT) {
drawArtIntoRect(g, drawArtIntoRect(g,
contentWidth / 2 + totalContentInset + 1, totalContentInset + boxHeight, contentWidth / 2 + totalContentInset + 1, totalContentInset + boxHeight,
contentWidth / 2 - 1, typeLineY - totalContentInset - boxHeight, contentWidth / 2 - 1, typeLineY - totalContentInset - boxHeight,
@ -713,27 +679,20 @@ public class ModernCardRenderer extends CardRenderer {
public void drawZendikarCurvedFace(Graphics2D g2, BufferedImage image, int x, int y, int x2, int y2, public void drawZendikarCurvedFace(Graphics2D g2, BufferedImage image, int x, int y, int x2, int y2,
Color boxColor, Paint paint) { Color boxColor, Paint paint) {
BufferedImage artToUse = faceArtImage;
boolean hadToUseFullArt = false;
if (faceArtImage == null) {
if (artImage == null) { if (artImage == null) {
return; return;
} }
hadToUseFullArt = true;
artToUse = artImage; BufferedImage artToUse = artImage;
}
int srcW = artToUse.getWidth(); int srcW = artToUse.getWidth();
int srcH = artToUse.getHeight(); int srcH = artToUse.getHeight();
if (hadToUseFullArt) {
// Get a box based on the standard scan from gatherer. // Get a box based on the standard scan from gatherer.
// Width = 185/223 pixels (centered) // Width = 185/223 pixels (centered)
// Height = 220/310, 38 pixels from top // Height = 220/310, 38 pixels from top
int subx = 19 * srcW / 223; int subx = 19 * srcW / 223;
int suby = 38 * srcH / 310; int suby = 38 * srcH / 310;
artToUse = artImage.getSubimage(subx, suby, 185 * srcW / 223, 220 * srcH / 310); artToUse = artImage.getSubimage(subx, suby, 185 * srcW / 223, 220 * srcH / 310);
}
Path2D.Double curve = new Path2D.Double(); Path2D.Double curve = new Path2D.Double();
@ -762,26 +721,19 @@ public class ModernCardRenderer extends CardRenderer {
public void drawBFZCurvedFace(Graphics2D g2, BufferedImage image, int x, int y, int x2, int y2, public void drawBFZCurvedFace(Graphics2D g2, BufferedImage image, int x, int y, int x2, int y2,
int topxdelta, int endydelta, int topxdelta, int endydelta,
Color boxColor, Paint paint) { Color boxColor, Paint paint) {
BufferedImage artToUse = faceArtImage;
boolean hadToUseFullArt = false;
if (faceArtImage == null) {
if (artImage == null) { if (artImage == null) {
return; return;
} }
hadToUseFullArt = true; BufferedImage artToUse = artImage;
artToUse = artImage;
}
int srcW = artToUse.getWidth(); int srcW = artToUse.getWidth();
int srcH = artToUse.getHeight(); int srcH = artToUse.getHeight();
if (hadToUseFullArt) {
// Get a box based on the standard scan from gatherer. // Get a box based on the standard scan from gatherer.
// Width = 185/223 pixels (centered) // Width = 185/223 pixels (centered)
// Height = 220/310, 38 pixels from top // Height = 220/310, 38 pixels from top
int subx = 19 * srcW / 223; int subx = 19 * srcW / 223;
int suby = 38 * srcH / 310; int suby = 38 * srcH / 310;
artToUse = artImage.getSubimage(subx, suby, 185 * srcW / 223, 220 * srcH / 310); artToUse = artImage.getSubimage(subx, suby, 185 * srcW / 223, 220 * srcH / 310);
}
Path2D.Double curve = new Path2D.Double(); Path2D.Double curve = new Path2D.Double();
curve.moveTo(x + topxdelta, y); curve.moveTo(x + topxdelta, y);
@ -907,23 +859,13 @@ public class ModernCardRenderer extends CardRenderer {
int availableWidth = w - manaCostWidth + 2; int availableWidth = w - manaCostWidth + 2;
// Draw the name // Draw the name
String nameStr; if (!baseName.isEmpty()) {
if (cardView.isFaceDown()) { AttributedString str = new AttributedString(baseName);
if (cardView instanceof PermanentView && ((PermanentView) cardView).isManifested()) {
nameStr = "Manifest: " + cardView.getName();
} else {
nameStr = "Morph: " + cardView.getName();
}
} else {
nameStr = baseName;
}
if (!nameStr.isEmpty()) {
AttributedString str = new AttributedString(nameStr);
str.addAttribute(TextAttribute.FONT, boxTextFont); str.addAttribute(TextAttribute.FONT, boxTextFont);
TextMeasurer measure = new TextMeasurer(str.getIterator(), g.getFontRenderContext()); TextMeasurer measure = new TextMeasurer(str.getIterator(), g.getFontRenderContext());
int breakIndex = measure.getLineBreakIndex(0, availableWidth); int breakIndex = measure.getLineBreakIndex(0, availableWidth);
if (breakIndex < nameStr.length()) { if (breakIndex < baseName.length()) {
str = new AttributedString(nameStr); str = new AttributedString(baseName);
str.addAttribute(TextAttribute.FONT, boxTextFontNarrow); str.addAttribute(TextAttribute.FONT, boxTextFontNarrow);
measure = new TextMeasurer(str.getIterator(), g.getFontRenderContext()); measure = new TextMeasurer(str.getIterator(), g.getFontRenderContext());
breakIndex = measure.getLineBreakIndex(0, availableWidth); breakIndex = measure.getLineBreakIndex(0, availableWidth);

View file

@ -157,7 +157,8 @@ public class ModernSplitCardRenderer extends ModernCardRenderer {
@Override @Override
protected void drawBackground(Graphics2D g) { protected void drawBackground(Graphics2D g) {
if (cardView.isFaceDown()) { if (false && cardView.isFaceDown()) {
// TODO: delete un-used code?!
drawCardBack(g); drawCardBack(g);
} if (isAdventure()) { } if (isAdventure()) {
super.drawBackground(g); super.drawBackground(g);
@ -206,7 +207,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer {
protected void drawArt(Graphics2D g) { protected void drawArt(Graphics2D g) {
if (isAdventure) { if (isAdventure) {
super.drawArt(g); super.drawArt(g);
} else if (artImage != null && !cardView.isFaceDown()) { } else if (artImage != null) {
if (isAftermath()) { if (isAftermath()) {
Rectangle2D topRect = ArtRect.AFTERMATH_TOP.rect; Rectangle2D topRect = ArtRect.AFTERMATH_TOP.rect;
int topLineY = (int) (leftHalf.ch * TYPE_LINE_Y_FRAC); int topLineY = (int) (leftHalf.ch * TYPE_LINE_Y_FRAC);

View file

@ -4,6 +4,7 @@ import mage.cards.MageCard;
import mage.cards.MagePermanent; import mage.cards.MagePermanent;
import mage.cards.action.ActionCallback; import mage.cards.action.ActionCallback;
import mage.client.util.GUISizeHelper; import mage.client.util.GUISizeHelper;
import mage.client.util.ImageCaches;
import mage.interfaces.plugin.CardPlugin; import mage.interfaces.plugin.CardPlugin;
import mage.view.CardView; import mage.view.CardView;
import mage.view.CounterView; import mage.view.CounterView;
@ -667,7 +668,7 @@ public class CardPluginImpl implements CardPlugin {
LOGGER.info("Symbols download finished"); LOGGER.info("Symbols download finished");
dialog.dispose(); dialog.dispose();
ManaSymbols.loadImages(); ManaSymbols.loadImages();
ImageCache.clearCache(); GUISizeHelper.refreshGUIAndCards();
} }
} }
} }
@ -710,6 +711,6 @@ public class CardPluginImpl implements CardPlugin {
@Override @Override
public BufferedImage getOriginalImage(CardView card) { public BufferedImage getOriginalImage(CardView card) {
return ImageCache.getImageOriginal(card).getImage(); return ImageCache.getCardImageOriginal(card).getImage();
} }
} }

View file

@ -24,15 +24,17 @@ public class DownloadJob extends AbstractLaternaBean {
private final String name; private final String name;
private Source source; private Source source;
private final Destination destination; private final Destination destination;
private final boolean forceToDownload; // download image everytime, do not keep old image
private final Property<State> state = properties.property("state", State.NEW); private final Property<State> state = properties.property("state", State.NEW);
private final Property<String> message = properties.property("message"); private final Property<String> message = properties.property("message");
private final Property<Exception> error = properties.property("error"); private final Property<Exception> error = properties.property("error");
private final BoundedRangeModel progress = new DefaultBoundedRangeModel(); private final BoundedRangeModel progress = new DefaultBoundedRangeModel();
public DownloadJob(String name, Source source, Destination destination) { public DownloadJob(String name, Source source, Destination destination, boolean forceToDownload) {
this.name = name; this.name = name;
this.source = source; this.source = source;
this.destination = destination; this.destination = destination;
this.forceToDownload = forceToDownload;
} }
/** /**
@ -155,6 +157,10 @@ public class DownloadJob extends AbstractLaternaBean {
return destination; return destination;
} }
public boolean isForceToDownload() {
return forceToDownload;
}
public static Source fromURL(final String url) { public static Source fromURL(final String url) {
return fromURL(CardImageUtils.getProxyFromPreferences(), url); return fromURL(CardImageUtils.getProxyFromPreferences(), url);
} }

View file

@ -146,7 +146,7 @@ public class Downloader extends AbstractLaternaBean {
Destination dst = job.getDestination(); Destination dst = job.getDestination();
BoundedRangeModel progress = job.getProgress(); BoundedRangeModel progress = job.getProgress();
if (dst.isValid()) { if (dst.isValid() && !job.isForceToDownload()) {
// already done // already done
progress.setMaximum(1); progress.setMaximum(1);
progress.setValue(1); progress.setValue(1);

View file

@ -1,76 +0,0 @@
package org.mage.plugins.card.dl.sources;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
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;
/**
*
* @author LevelX2
*/
public class CardFrames implements Iterable<DownloadJob> {
private static final String FRAMES_PATH = File.separator + "frames";
private static final File DEFAULT_OUT_DIR = new File("plugins" + File.separator + "images" + FRAMES_PATH);
private static File outDir = DEFAULT_OUT_DIR;
static final String BASE_DOWNLOAD_URL = "http://ct-magefree.rhcloud.com/resources/img/";
static final String TEXTURES_FOLDER = "textures";
static final String PT_BOXES_FOLDER = "pt";
private static final String[] TEXTURES = {"U", "R", "G", "B", "W", "A",
"BG_LAND", "BR_LAND", "WU_LAND", "WB_LAND", "UB_LAND", "GW_LAND", "RW_LAND",
"RG_LAND", "GU_LAND", "UR_LAND"
// NOT => "BW_LAND","BU_LAND","WG_LAND","WR_LAND",
};
private static final String[] PT_BOXES = {"U", "R", "G", "B", "W", "A"};
public CardFrames(String path) {
if (path == null) {
useDefaultDir();
} else {
changeOutDir(path);
}
}
@Override
public Iterator<DownloadJob> iterator() {
List<DownloadJob> jobs = new ArrayList<>();
for (String texture : TEXTURES) {
jobs.add(generateDownloadJob(TEXTURES_FOLDER, texture));
}
for (String pt_box : PT_BOXES) {
jobs.add(generateDownloadJob(PT_BOXES_FOLDER, pt_box));
}
return jobs.iterator();
}
private DownloadJob generateDownloadJob(String dirName, String name) {
File dst = new File(outDir, name + ".png");
String url = BASE_DOWNLOAD_URL + dirName + '/' + name + ".png";
return new DownloadJob("frames-" + dirName + '-' + name, fromURL(url), toFile(dst));
}
private void useDefaultDir() {
outDir = DEFAULT_OUT_DIR;
}
private void changeOutDir(String path) {
File file = new File(path + FRAMES_PATH);
if (file.exists()) {
outDir = file;
} else {
file.mkdirs();
if (file.exists()) {
outDir = file;
}
}
}
}

View file

@ -11,6 +11,7 @@ import static org.mage.plugins.card.dl.DownloadJob.toFile;
import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir;
/** /**
* TODO: outdated, delete and use xmage tokens instead ?!
* Used when we need to point to direct links to download resources from. * Used when we need to point to direct links to download resources from.
* *
* @author noxx * @author noxx
@ -20,11 +21,9 @@ public class DirectLinksForDownload implements Iterable<DownloadJob> {
private static final Map<String, String> directLinks = new LinkedHashMap<>(); private static final Map<String, String> directLinks = new LinkedHashMap<>();
public static final String cardbackFilename = "cardback.jpg"; public static final String cardbackFilename = "cardback.jpg";
public static final String foretellFilename = "foretell.jpg";
static { static {
directLinks.put(cardbackFilename, "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"); directLinks.put(cardbackFilename, "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg");
directLinks.put(foretellFilename, "https://api.scryfall.com/cards/tkhm/23/en?format=image");
} }
private final File outDir; private final File outDir;
@ -42,7 +41,8 @@ public class DirectLinksForDownload implements Iterable<DownloadJob> {
for (Map.Entry<String, String> url : directLinks.entrySet()) { for (Map.Entry<String, String> url : directLinks.entrySet()) {
File dst = new File(outDir, url.getKey()); File dst = new File(outDir, url.getKey());
jobs.add(new DownloadJob(url.getKey(), fromURL(url.getValue()), toFile(dst))); // download images every time (need to update low quality image)
jobs.add(new DownloadJob(url.getKey(), fromURL(url.getValue()), toFile(dst), true));
} }
return jobs.iterator(); return jobs.iterator();
} }

View file

@ -337,6 +337,6 @@ public class GathererSets implements Iterable<DownloadJob> {
set = codeReplacements.get(set); set = codeReplacements.get(set);
} }
String url = "https://gatherer.wizards.com/Handlers/Image.ashx?type=symbol&set=" + set + "&size=small&rarity=" + urlRarity; String url = "https://gatherer.wizards.com/Handlers/Image.ashx?type=symbol&set=" + set + "&size=small&rarity=" + urlRarity;
return new DownloadJob(set + '-' + rarity, fromURL(url), toFile(dst)); return new DownloadJob(set + '-' + rarity, fromURL(url), toFile(dst), false);
} }
} }

View file

@ -125,7 +125,7 @@ public class GathererSymbols implements Iterable<DownloadJob> {
String url = format(urlFmt, sizes[modSizeIndex], symbol); String url = format(urlFmt, sizes[modSizeIndex], symbol);
return new DownloadJob(sym, fromURL(url), toFile(dst)); return new DownloadJob(sym, fromURL(url), toFile(dst), false);
} }
} }
}; };

View file

@ -96,7 +96,6 @@ public enum ScryfallImageSource implements CardImageSource {
} }
// double faced cards (modal double faces cards too) // double faced cards (modal double faces cards too)
if (card.isTwoFacedCard()) {
if (card.isSecondSide()) { if (card.isSecondSide()) {
// back face - must be prepared before // back face - must be prepared before
logger.warn("Can't find back face info in prepared list " logger.warn("Can't find back face info in prepared list "
@ -105,7 +104,6 @@ public enum ScryfallImageSource implements CardImageSource {
} else { } else {
// front face - can be downloaded normally as basic card // front face - can be downloaded normally as basic card
} }
}
// basic cards by api call (redirect to img link) // basic cards by api call (redirect to img link)
// example: https://api.scryfall.com/cards/xln/121/en?format=image // example: https://api.scryfall.com/cards/xln/121/en?format=image
@ -219,7 +217,7 @@ public enum ScryfallImageSource implements CardImageSource {
int needPrepareCount = 0; int needPrepareCount = 0;
int currentPrepareCount = 0; int currentPrepareCount = 0;
for (CardDownloadData card : downloadList) { for (CardDownloadData card : downloadList) {
if (card.isTwoFacedCard() && card.isSecondSide()) { if (card.isSecondSide()) {
needPrepareCount++; needPrepareCount++;
} }
} }
@ -232,7 +230,7 @@ public enum ScryfallImageSource implements CardImageSource {
} }
// prepare the back face URL // prepare the back face URL
if (card.isTwoFacedCard() && card.isSecondSide()) { if (card.isSecondSide()) {
currentPrepareCount++; currentPrepareCount++;
try { try {
String url = getFaceImageUrl(proxy, card, card.isToken()); String url = getFaceImageUrl(proxy, card, card.isToken());

View file

@ -1275,9 +1275,9 @@ public class ScryfallImageSupportTokens {
put("DDE/Saproling", "https://api.scryfall.com/cards/tdde/3/en?format=image"); put("DDE/Saproling", "https://api.scryfall.com/cards/tdde/3/en?format=image");
// DDD // DDD
put("DDD/Beast/1", "https://api.scryfall.com/cards/tddd/1/en?format=image"); put("DDD/Beast/1", "https://api.scryfall.com/cards/tddd/T1/en?format=image");
put("DDD/Beast/2", "https://api.scryfall.com/cards/tddd/2/en?format=image"); put("DDD/Beast/2", "https://api.scryfall.com/cards/tddd/T2/en?format=image");
put("DDD/Elephant", "https://api.scryfall.com/cards/tddd/3/en?format=image"); put("DDD/Elephant", "https://api.scryfall.com/cards/tddd/T3/en?format=image");
// SOM // SOM
put("SOM/Cat", "https://api.scryfall.com/cards/tsom/1/en?format=image"); put("SOM/Cat", "https://api.scryfall.com/cards/tsom/1/en?format=image");

View file

@ -1,18 +1,19 @@
package org.mage.plugins.card.dl.sources; package org.mage.plugins.card.dl.sources;
import org.mage.plugins.card.dl.DownloadJob;
import org.mage.plugins.card.utils.CardImageUtils;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.io.*; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.*; import java.util.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.mage.plugins.card.dl.DownloadJob;
import org.mage.plugins.card.utils.CardImageUtils;
import static org.mage.card.arcane.ManaSymbols.getSymbolFileNameAsSVG; import static org.mage.card.arcane.ManaSymbols.getSymbolFileNameAsSVG;
import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir;
@ -166,16 +167,14 @@ public class ScryfallSymbolsSource implements Iterable<DownloadJob> {
} }
} }
private String destFile = "";
public ScryfallSymbolsDownloadJob() { public ScryfallSymbolsDownloadJob() {
// download init // download init
super("Scryfall symbols source", fromURL(""), toFile(DOWNLOAD_TEMP_FILE)); // url setup on preparing stage super("Scryfall symbols source", fromURL(""), toFile(DOWNLOAD_TEMP_FILE), true); // url setup on preparing stage
this.destFile = DOWNLOAD_TEMP_FILE; String destFile = DOWNLOAD_TEMP_FILE;
this.addPropertyChangeListener(STATE_PROP_NAME, new ScryfallDownloadOnFinishedListener(this.destFile)); this.addPropertyChangeListener(STATE_PROP_NAME, new ScryfallDownloadOnFinishedListener(destFile));
// clear dest file (always download new data) // duplicate a forceToDownload param above, but it's ok to clear temp file anyway
File file = new File(this.destFile); File file = new File(destFile);
if (file.exists()) { if (file.exists()) {
file.delete(); file.delete();
} }

View file

@ -14,37 +14,18 @@ public class CardDownloadData {
private String set; private String set;
private final String collectorId; private final String collectorId;
private final Integer imageNumber; private final Integer imageNumber;
private boolean token; private boolean isToken;
private final boolean twoFacedCard; private boolean isSecondSide;
private final boolean secondSide; private boolean isFlippedSide;
private boolean flipCard; private boolean isSplitCard;
private boolean flippedSide; private final boolean isUsesVariousArt;
private boolean splitCard;
private final boolean usesVariousArt;
private String tokenClassName;
public CardDownloadData(String name, String setCode, String collectorId, boolean usesVariousArt, Integer imageNumber) { public CardDownloadData(String name, String setCode, String collectorId, boolean isUsesVariousArt, Integer imageNumber) {
this(name, setCode, collectorId, usesVariousArt, imageNumber, false);
}
public CardDownloadData(String name, String setCode, String collectorId, boolean usesVariousArt, Integer imageNumber, boolean token) {
this(name, setCode, collectorId, usesVariousArt, imageNumber, token, false, false, "");
}
public CardDownloadData(String name, String setCode, String collectorId, boolean usesVariousArt, Integer imageNumber, boolean token, boolean twoFacedCard, boolean secondSide) {
this(name, setCode, collectorId, usesVariousArt, imageNumber, token, twoFacedCard, secondSide, "");
}
public CardDownloadData(String name, String setCode, String collectorId, boolean usesVariousArt, Integer imageNumber, boolean token, boolean twoFacedCard, boolean secondSide, String tokenClassName) {
this.name = name; this.name = name;
this.set = setCode; this.set = setCode;
this.collectorId = collectorId; this.collectorId = collectorId;
this.usesVariousArt = usesVariousArt; this.isUsesVariousArt = isUsesVariousArt;
this.imageNumber = imageNumber; this.imageNumber = imageNumber;
this.token = token;
this.twoFacedCard = twoFacedCard;
this.secondSide = secondSide;
this.tokenClassName = tokenClassName;
} }
public CardDownloadData(final CardDownloadData card) { public CardDownloadData(final CardDownloadData card) {
@ -53,14 +34,11 @@ public class CardDownloadData {
this.set = card.set; this.set = card.set;
this.collectorId = card.collectorId; this.collectorId = card.collectorId;
this.imageNumber = card.imageNumber; this.imageNumber = card.imageNumber;
this.token = card.token; this.isToken = card.isToken;
this.twoFacedCard = card.twoFacedCard; this.isSecondSide = card.isSecondSide;
this.secondSide = card.secondSide; this.isFlippedSide = card.isFlippedSide;
this.flipCard = card.flipCard; this.isSplitCard = card.isSplitCard;
this.flippedSide = card.flippedSide; this.isUsesVariousArt = card.isUsesVariousArt;
this.splitCard = card.splitCard;
this.usesVariousArt = card.usesVariousArt;
this.tokenClassName = card.tokenClassName;
} }
@Override @Override
@ -81,14 +59,11 @@ public class CardDownloadData {
if (!Objects.equals(this.collectorId, other.collectorId)) { if (!Objects.equals(this.collectorId, other.collectorId)) {
return false; return false;
} }
if (this.token != other.token) { if (this.isToken != other.isToken) {
return false;
}
if (this.twoFacedCard != other.twoFacedCard) {
return false; return false;
} }
return this.secondSide == other.secondSide; return this.isSecondSide == other.isSecondSide;
} }
@Override @Override
@ -98,9 +73,8 @@ public class CardDownloadData {
hash = 47 * hash + (this.set != null ? this.set.hashCode() : 0); hash = 47 * hash + (this.set != null ? this.set.hashCode() : 0);
hash = 47 * hash + (this.collectorId != null ? this.collectorId.hashCode() : 0); hash = 47 * hash + (this.collectorId != null ? this.collectorId.hashCode() : 0);
hash = 47 * hash + (this.imageNumber != null ? this.imageNumber.hashCode() : 0); hash = 47 * hash + (this.imageNumber != null ? this.imageNumber.hashCode() : 0);
hash = 47 * hash + (this.token ? 1 : 0); hash = 47 * hash + (this.isToken ? 1 : 0);
hash = 47 * hash + (this.twoFacedCard ? 1 : 0); hash = 47 * hash + (this.isSecondSide ? 1 : 0);
hash = 47 * hash + (this.secondSide ? 1 : 0);
return hash; return hash;
} }
@ -149,28 +123,20 @@ public class CardDownloadData {
this.set = set; this.set = set;
} }
public void setTokenClassName(String tokenClassName) {
this.tokenClassName = tokenClassName;
}
public String getAffectedClassName() {
return tokenClassName.isEmpty() ? name.replaceAll("[^a-zA-Z0-9]", "") : tokenClassName;
}
public boolean isToken() { public boolean isToken() {
return token; return isToken;
} }
public void setToken(boolean token) { public void setToken(boolean token) {
this.token = token; this.isToken = token;
}
public boolean isTwoFacedCard() {
return twoFacedCard;
} }
public boolean isSecondSide() { public boolean isSecondSide() {
return secondSide; return isSecondSide;
}
public void setSecondSide(boolean isSecondSide) {
this.isSecondSide = isSecondSide;
} }
public String getDownloadName() { public String getDownloadName() {
@ -181,20 +147,12 @@ public class CardDownloadData {
this.downloadName = downloadName; this.downloadName = downloadName;
} }
public boolean isFlipCard() {
return flipCard;
}
public void setFlipCard(boolean flipCard) {
this.flipCard = flipCard;
}
public boolean isSplitCard() { public boolean isSplitCard() {
return splitCard; return isSplitCard;
} }
public void setSplitCard(boolean splitCard) { public void setSplitCard(boolean splitCard) {
this.splitCard = splitCard; this.isSplitCard = splitCard;
} }
public Integer getImageNumber() { public Integer getImageNumber() {
@ -202,14 +160,14 @@ public class CardDownloadData {
} }
public boolean getUsesVariousArt() { public boolean getUsesVariousArt() {
return usesVariousArt; return isUsesVariousArt;
} }
public boolean isFlippedSide() { public boolean isFlippedSide() {
return flippedSide; return isFlippedSide;
} }
public void setFlippedSide(boolean flippedSide) { public void setFlippedSide(boolean flippedSide) {
this.flippedSide = flippedSide; this.isFlippedSide = flippedSide;
} }
} }

View file

@ -10,6 +10,8 @@ import mage.client.MageFrame;
import mage.client.dialog.DownloadImagesDialog; import mage.client.dialog.DownloadImagesDialog;
import mage.client.dialog.PreferencesDialog; import mage.client.dialog.PreferencesDialog;
import mage.client.util.CardLanguage; import mage.client.util.CardLanguage;
import mage.client.util.GUISizeHelper;
import mage.client.util.ImageCaches;
import mage.client.util.sets.ConstructedFormats; import mage.client.util.sets.ConstructedFormats;
import mage.remote.Connection; import mage.remote.Connection;
import net.java.truevfs.access.TFile; import net.java.truevfs.access.TFile;
@ -437,14 +439,19 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
&& !"0".equals(card.getCardNumber()) && !"0".equals(card.getCardNumber())
&& !card.getSetCode().isEmpty()) { && !card.getSetCode().isEmpty()) {
String cardName = card.getName(); String cardName = card.getName();
CardDownloadData url = new CardDownloadData(cardName, card.getSetCode(), card.getCardNumber(), card.usesVariousArt(), 0, false, card.isDoubleFaced(), card.isNightCard()); CardDownloadData url = new CardDownloadData(
cardName,
card.getSetCode(),
card.getCardNumber(),
card.usesVariousArt(),
0);
url.setSecondSide(card.isNightCard());
// variations must have diff file names with additional postfix // variations must have diff file names with additional postfix
if (url.getUsesVariousArt()) { if (url.getUsesVariousArt()) {
url.setDownloadName(createDownloadName(card)); url.setDownloadName(createDownloadName(card));
} }
url.setFlipCard(card.isFlipCard());
url.setSplitCard(card.isSplitCard()); url.setSplitCard(card.isSplitCard());
// main side // main side
@ -467,7 +474,9 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
card.getSetCode(), card.getSetCode(),
secondSideCard.getCardNumber(), secondSideCard.getCardNumber(),
card.usesVariousArt(), card.usesVariousArt(),
0, false, card.isDoubleFaced(), true); 0
);
url.setSecondSide(true);
allCardsUrls.add(url); allCardsUrls.add(url);
} }
if (card.isFlipCard()) { if (card.isFlipCard()) {
@ -479,9 +488,10 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
card.getSetCode(), card.getSetCode(),
card.getCardNumber(), card.getCardNumber(),
card.usesVariousArt(), card.usesVariousArt(),
0, false, card.isDoubleFaced(), card.isNightCard()); 0
cardDownloadData.setFlipCard(true); );
cardDownloadData.setFlippedSide(true); cardDownloadData.setFlippedSide(true);
cardDownloadData.setSecondSide(card.isNightCard());
allCardsUrls.add(cardDownloadData); allCardsUrls.add(cardDownloadData);
} }
if (card.getMeldsToCardName() != null) { if (card.getMeldsToCardName() != null) {
@ -500,7 +510,8 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
card.getSetCode(), card.getSetCode(),
meldsToCard.getCardNumber(), meldsToCard.getCardNumber(),
card.usesVariousArt(), card.usesVariousArt(),
0, false, false, false); 0
);
allCardsUrls.add(url); allCardsUrls.add(url);
} }
if (card.isModalDoubleFacedCard()) { if (card.isModalDoubleFacedCard()) {
@ -512,7 +523,9 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
card.getSetCode(), card.getSetCode(),
card.getCardNumber(), card.getCardNumber(),
card.usesVariousArt(), card.usesVariousArt(),
0, false, true, true); 0
);
cardDownloadData.setSecondSide(true);
allCardsUrls.add(cardDownloadData); allCardsUrls.add(cardDownloadData);
} }
} else if (card.getCardNumber().isEmpty() || "0".equals(card.getCardNumber())) { } else if (card.getCardNumber().isEmpty() || "0".equals(card.getCardNumber())) {
@ -531,9 +544,8 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
token.getSetCode(), token.getSetCode(),
"0", "0",
false, false,
token.getImageNumber(), token.getImageNumber());
true card.setToken(true);
);
allCardsUrls.add(card); allCardsUrls.add(card);
}); });
} catch (Exception e) { } catch (Exception e) {
@ -674,8 +686,8 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
reloadCardsToDownload(uiDialog.getSetsCombo().getSelectedItem().toString()); reloadCardsToDownload(uiDialog.getSetsCombo().getSelectedItem().toString());
enableDialogButtons(); enableDialogButtons();
// reset images cache // reset GUI and cards to use new images
ImageCache.clearCache(); GUISizeHelper.refreshGUIAndCards();
} }
static String convertStreamToString(InputStream is) { static String convertStreamToString(InputStream is) {

View file

@ -1,14 +1,15 @@
package org.mage.plugins.card.images; package org.mage.plugins.card.images;
import com.google.common.collect.ComputationException;
import mage.abilities.icon.CardIconColor; import mage.abilities.icon.CardIconColor;
import mage.client.constants.Constants; import mage.client.constants.Constants;
import mage.client.util.ImageCaches;
import mage.client.util.SoftValuesLoadingCache; import mage.client.util.SoftValuesLoadingCache;
import mage.client.util.TransformedImageCache; import mage.client.util.TransformedImageCache;
import mage.view.CardView; import mage.view.CardView;
import net.java.truevfs.access.TFile; import net.java.truevfs.access.TFile;
import net.java.truevfs.access.TFileInputStream; import net.java.truevfs.access.TFileInputStream;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.mage.card.arcane.CardPanelRenderModeImage;
import org.mage.plugins.card.dl.sources.DirectLinksForDownload; import org.mage.plugins.card.dl.sources.DirectLinksForDownload;
import org.mage.plugins.card.utils.CardImageUtils; import org.mage.plugins.card.utils.CardImageUtils;
import org.mage.plugins.card.utils.impl.ImageManagerImpl; import org.mage.plugins.card.utils.impl.ImageManagerImpl;
@ -21,17 +22,9 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* This class stores ALL card images in a cache with soft values. this means * This class stores ALL card images in a cache with soft values. This means
* that the images may be garbage collected when they are not needed any more, * that the images may be garbage collected when they are not needed any more,
* but will be kept as long as possible. * but will be kept as long as possible.
* <p>
* Key format: "[cardname]#[setname]#[type]#[collectorID]#[image size]#[additional data]"
*
* <li>#Normal: request for unrotated image</li>
* <li>#Tapped: request for rotated image</li>
* <li>#Cropped: request for cropped image that is used for Shandalar like card
* look</li>
* </ul>
* *
* @author JayDi85 * @author JayDi85
*/ */
@ -39,39 +32,37 @@ public final class ImageCache {
private static final Logger LOGGER = Logger.getLogger(ImageCache.class); private static final Logger LOGGER = Logger.getLogger(ImageCache.class);
private static final SoftValuesLoadingCache<String, ImageCacheData> IMAGE_CACHE; // cards and tokens // global cache for both mtgo and image render modes
private static final SoftValuesLoadingCache<String, ImageCacheData> FACE_IMAGE_CACHE; private static final SoftValuesLoadingCache<String, ImageCacheData> SHARED_CARD_IMAGES_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(ImageCache::createCardOrTokenImage));
private static final SoftValuesLoadingCache<String, ImageCacheData> CARD_ICONS_CACHE; private static final SoftValuesLoadingCache<String, ImageCacheData> SHARED_CARD_ICONS_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(ImageCache::createIcon));
/** // format: name #setcode #imagenumber #cardnumber #size #usesVariousArt
* Common pattern for keys. See ImageCache.getKey for structure info private static final Pattern CARD_IMAGE_KEY_PATTERN = Pattern.compile("(.*)#(.*)#(.*)#(.*)#(.*)");
*/
private static final Pattern KEY_PATTERN = Pattern.compile("(.*)#(.*)#(.*)#(.*)#(.*)"); // format: size #icon #color
private static final Pattern CARD_ICON_KEY_PATTERN = Pattern.compile("(.*)#(.*)#(.*)"); private static final Pattern CARD_ICON_KEY_PATTERN = Pattern.compile("(.*)#(.*)#(.*)");
static { private ImageCache() {
// softValues() = Specifies that each value (not key) stored in the map should be wrapped in a SoftReference }
// (by default, strong references are used). Softly-referenced objects will be garbage-collected in a
// globally least-recently-used manner, in response to memory demand. private static ImageCacheData createCardOrTokenImage(String key) {
IMAGE_CACHE = SoftValuesLoadingCache.from(key -> {
try {
boolean usesVariousArt = false; boolean usesVariousArt = false;
if (key.matches(".*#usesVariousArt.*")) { if (key.matches(".*#usesVariousArt.*")) {
usesVariousArt = true; usesVariousArt = true;
key = key.replace("#usesVariousArt", ""); key = key.replace("#usesVariousArt", "");
} }
Matcher m = KEY_PATTERN.matcher(key); Matcher m = CARD_IMAGE_KEY_PATTERN.matcher(key);
if (m.matches()) { if (m.matches()) {
String name = m.group(1); String name = m.group(1);
String setCode = m.group(2); String setCode = m.group(2);
Integer type = Integer.parseInt(m.group(3)); Integer imageNumber = Integer.parseInt(m.group(3));
String collectorId = m.group(4); String collectorId = m.group(4);
if (collectorId.equals("null")) { if (collectorId.equals("null")) {
collectorId = "0"; collectorId = "0";
} }
CardDownloadData info = new CardDownloadData(name, setCode, collectorId, usesVariousArt, type); CardDownloadData info = new CardDownloadData(name, setCode, collectorId, usesVariousArt, imageNumber);
boolean cardback = false; boolean cardback = false;
String path; String path;
@ -86,6 +77,7 @@ public final class ImageCache {
tokenFile = getTFile(path); tokenFile = getTFile(path);
// try token from card // try token from card
// TODO: unused code?
// TODO: return image from another set on empty image? // TODO: return image from another set on empty image?
if (tokenFile == null || !tokenFile.exists()) { if (tokenFile == null || !tokenFile.exists()) {
CardDownloadData tempInfo = new CardDownloadData(info); CardDownloadData tempInfo = new CardDownloadData(info);
@ -124,53 +116,12 @@ public final class ImageCache {
return new ImageCacheData(path, image); return new ImageCacheData(path, image);
} }
} else { } else {
throw new RuntimeException( throw new IllegalArgumentException("Unknown card image's key format: " + key);
"Requested image doesn't fit the requirement for key (<cardname>#<setname>#<collectorID>): " + key);
} }
} catch (Exception ex) {
if (ex instanceof ComputationException) {
throw (ComputationException) ex;
} else {
throw new ComputationException(ex);
}
}
});
FACE_IMAGE_CACHE = SoftValuesLoadingCache.from(key -> {
try {
Matcher m = KEY_PATTERN.matcher(key);
if (m.matches()) {
String name = m.group(1);
String setCode = m.group(2);
// skip type
// skip collectorId
String path = CardImageUtils.generateFaceImagePath(name, setCode);
TFile file = getTFile(path);
if (file == null) {
return new ImageCacheData(path, null);
} }
BufferedImage image = loadImage(file); private static ImageCacheData createIcon(String key) {
return new ImageCacheData(path, image);
} else {
throw new RuntimeException(
"Requested face image doesn't fit the requirement for key (<cardname>#<artid>#: " + key);
}
} catch (Exception ex) {
if (ex instanceof ComputationException) {
throw (ComputationException) ex;
} else {
throw new ComputationException(ex);
}
}
});
CARD_ICONS_CACHE = SoftValuesLoadingCache.from(key -> {
try {
Matcher m = CARD_ICON_KEY_PATTERN.matcher(key); Matcher m = CARD_ICON_KEY_PATTERN.matcher(key);
if (m.matches()) { if (m.matches()) {
int cardSize = Integer.parseInt(m.group(1)); int cardSize = Integer.parseInt(m.group(1));
String resourceName = m.group(2); String resourceName = m.group(2);
@ -178,56 +129,8 @@ public final class ImageCache {
BufferedImage image = ImageManagerImpl.instance.getCardIcon(resourceName, cardSize, cardIconColor); BufferedImage image = ImageManagerImpl.instance.getCardIcon(resourceName, cardSize, cardIconColor);
return new ImageCacheData(resourceName, image); return new ImageCacheData(resourceName, image);
} else { } else {
throw new RuntimeException("Wrong card icons image key format: " + key); throw new IllegalArgumentException("Unknown card icon's key format: " + key);
} }
} catch (Exception ex) {
if (ex instanceof ComputationException) {
throw (ComputationException) ex;
} else {
throw new ComputationException(ex);
}
}
});
}
public static void clearCache() {
IMAGE_CACHE.invalidateAll();
FACE_IMAGE_CACHE.invalidateAll();
CARD_ICONS_CACHE.invalidateAll();
}
private ImageCache() {
}
public static ImageCacheData getCardbackImage() {
String path = CardImageUtils.buildImagePathToDefault(DirectLinksForDownload.cardbackFilename);
BufferedImage image = ImageCache.loadImage(getTFile(path));
image = getRoundCorner(image);
return new ImageCacheData(path, image);
}
public static ImageCacheData getMorphImage() {
// TODO: replace by downloadable morth image
CardDownloadData info = new CardDownloadData("Morph", "KTK", "0", false, 0);
info.setToken(true);
String path = CardImageUtils.buildImagePathToCardOrToken(info);
TFile file = getTFile(path);
BufferedImage image = loadImage(file);
image = getRoundCorner(image);
return new ImageCacheData(path, image);
}
public static ImageCacheData getManifestImage() {
// TODO: replace by downloadable manifestest image
CardDownloadData info = new CardDownloadData("Manifest", "FRF", "0", false, 0);
info.setToken(true);
String path = CardImageUtils.buildImagePathToCardOrToken(info);
TFile file = getTFile(path);
BufferedImage image = loadImage(file);
image = getRoundCorner(image);
return new ImageCacheData(path, image);
} }
public static BufferedImage getRoundCorner(BufferedImage image) { public static BufferedImage getRoundCorner(BufferedImage image) {
@ -269,61 +172,52 @@ public final class ImageCache {
} }
} }
public static ImageCacheData getImageOriginal(CardView card) { /** Find image for current side
return getImage(getKey(card, card.getName(), 0)); */
public static ImageCacheData getCardImageOriginal(CardView card) {
return getCardImage(getKey(card, card.getName(), 0));
} }
public static ImageCacheData getImageOriginalAlternateName(CardView card) { /**
return getImage(getKey(card, card.getAlternateName(), 0)); * Find image for other side
*/
public static ImageCacheData getCardImageAlternate(CardView card) {
return getCardImage(getKey(card, card.getAlternateName(), 0));
} }
public static ImageCacheData getCardIconImage(String resourceName, int iconSize, String cardColorName) { public static ImageCacheData getCardIconImage(String resourceName, int iconSize, String cardColorName) {
return getCardIconImage(getCardIconKey(resourceName, iconSize, cardColorName)); return getCardIconImage(getCardIconKey(resourceName, iconSize, cardColorName));
} }
/** private static ImageCacheData getCardImage(String key) {
* Returns the Image corresponding to the key
*/
private static ImageCacheData getImage(String key) {
try { try {
ImageCacheData data = IMAGE_CACHE.getOrNull(key); ImageCacheData data = SHARED_CARD_IMAGES_CACHE.getOrNull(key);
return data != null ? data : new ImageCacheData("ERROR: key - " + key, null); return data != null ? data : new ImageCacheData("ERROR: key - " + key, null);
} catch (ComputationException ex) { } catch (Exception e) {
// too low memory if (e.getCause() instanceof NullPointerException) {
if (ex.getCause() instanceof NullPointerException) { // low memory error???
return new ImageCacheData("ERROR: low memory?", null); return new ImageCacheData("ERROR: possible low memory", null);
} else {
// other error
LOGGER.error("Error while loading card image: " + e, e);
return new ImageCacheData("ERROR: see client logs for details", null);
} }
LOGGER.error(ex, ex);
return new ImageCacheData("ERROR: see logs", null);
}
}
/**
* Returns the Image corresponding to the key
*/
private static ImageCacheData getFaceImage(String key) {
try {
ImageCacheData data = FACE_IMAGE_CACHE.getOrNull(key);
return data != null ? data : new ImageCacheData("ERROR: key " + key, null);
} catch (ComputationException ex) {
if (ex.getCause() instanceof NullPointerException) {
return new ImageCacheData("ERROR: low memory?", null);
}
LOGGER.error(ex, ex);
return new ImageCacheData("ERROR: see logs", null);
} }
} }
private static ImageCacheData getCardIconImage(String key) { private static ImageCacheData getCardIconImage(String key) {
try { try {
ImageCacheData data = CARD_ICONS_CACHE.getOrNull(key); ImageCacheData data = SHARED_CARD_ICONS_CACHE.getOrNull(key);
return data != null ? data : new ImageCacheData("ERROR: key - " + key, null); return data != null ? data : new ImageCacheData("ERROR: key - " + key, null);
} catch (ComputationException ex) { } catch (Exception e) {
if (ex.getCause() instanceof NullPointerException) { if (e.getCause() instanceof NullPointerException) {
return new ImageCacheData("ERROR: low memory?", null); // low memory error???
return new ImageCacheData("ERROR: possible low memory", null);
} else {
// other error
LOGGER.error("Error while loading card icon: " + e, e);
return new ImageCacheData("ERROR: see client logs for details", null);
} }
LOGGER.error(ex, ex);
return new ImageCacheData("ERROR: see logs", null);
} }
} }
@ -332,7 +226,7 @@ public final class ImageCache {
* the cache. * the cache.
*/ */
private static ImageCacheData tryGetImage(String key) { private static ImageCacheData tryGetImage(String key) {
return IMAGE_CACHE.peekIfPresent(key); return SHARED_CARD_IMAGES_CACHE.peekIfPresent(key);
} }
/** /**
@ -343,7 +237,11 @@ public final class ImageCache {
* @param imageSize - size info, 0 to use original image (with max size) * @param imageSize - size info, 0 to use original image (with max size)
*/ */
private static String getKey(CardView card, String cardName, int imageSize) { private static String getKey(CardView card, String cardName, int imageSize) {
return (card.isToken() ? cardName.replace(" Token", "") : cardName) String imageFileName = card.getImageFileName();
if (imageFileName.isEmpty()) {
imageFileName = cardName;
}
return imageFileName.replace(" Token", "")
+ '#' + card.getExpansionSetCode() + '#' + card.getExpansionSetCode()
+ '#' + card.getImageNumber() + '#' + card.getImageNumber()
+ '#' + card.getCardNumber() + '#' + card.getCardNumber()
@ -420,9 +318,9 @@ public final class ImageCache {
* @param height * @param height
* @return * @return
*/ */
public static ImageCacheData getImage(CardView card, int width, int height) { public static ImageCacheData getCardImage(CardView card, int width, int height) {
String key = getKey(card, card.getName(), width); String key = getKey(card, card.getName(), width);
ImageCacheData data = getImage(key); ImageCacheData data = getCardImage(key);
if (data.getImage() == null) { if (data.getImage() == null) {
LOGGER.debug("Image doesn't exists in the cache: " + key); LOGGER.debug("Image doesn't exists in the cache: " + key);
return data; return data;
@ -438,23 +336,6 @@ public final class ImageCache {
return data; return data;
} }
/**
* Returns the image appropriate to display the card in the picture panel
*
* @param card
* @param width
* @param height
* @return
*/
public static ImageCacheData getFaceImage(CardView card, int width, int height) {
String key = getFaceKey(card, card.getName(), card.getExpansionSetCode());
ImageCacheData data = getFaceImage(key);
if (data.getImage() == null) {
LOGGER.debug(key + " (faceimage) not found");
}
return data;
}
/** /**
* Returns the image appropriate to display for a card in a picture panel, * 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 * but only it was ALREADY LOADED. That is, the call is immediate and will

View file

@ -146,37 +146,47 @@ public final class CardImageUtils {
*/ */
public static String buildImagePathToCardView(CardView card) { public static String buildImagePathToCardView(CardView card) {
String imageFile; String imageFile;
if (card.getMageObjectType().isUseTokensRepository()) { String imageFileName = card.getImageFileName();
// token images if (imageFileName.isEmpty()) {
imageFileName = card.getName();
}
if (imageFileName.isEmpty()) {
return "ERROR: empty image file name, object type - " + card.getMageObjectType();
}
if (card.getMageObjectType().isUseTokensRepository()
|| card.getExpansionSetCode().equals(TokenRepository.XMAGE_TOKENS_SET_CODE)) {
// token images or inner cards like face down
CardDownloadData cardData = new CardDownloadData( CardDownloadData cardData = new CardDownloadData(
card.getName().replace(" Token", ""), imageFileName.replace(" Token", ""),
card.getExpansionSetCode(), card.getExpansionSetCode(),
"0", card.getCardNumber(),
false, card.getUsesVariousArt(),
card.getImageNumber(), card.getImageNumber());
true); cardData.setToken(true);
imageFile = CardImageUtils.buildImagePathToCardOrToken(cardData); imageFile = CardImageUtils.buildImagePathToCardOrToken(cardData);
} else { } else {
TokenRepository.instance.getAll();
// card images // card images
// workaround to find various art settings first // workaround to find various art settings first
// TODO: no needs in workaround?! ?!
boolean usesVariousArt = false;
CardInfo cardInfo = CardRepository.instance.findCardWithPreferredSetAndNumber( CardInfo cardInfo = CardRepository.instance.findCardWithPreferredSetAndNumber(
card.getName(), card.getName(),
card.getExpansionSetCode(), card.getExpansionSetCode(),
card.getCardNumber() card.getCardNumber()
); );
if (cardInfo != null) { if (cardInfo != null) {
usesVariousArt = cardInfo.usesVariousArt();
}
CardDownloadData cardData = new CardDownloadData( CardDownloadData cardData = new CardDownloadData(
cardInfo.getName(), imageFileName,
cardInfo.getSetCode(), card.getExpansionSetCode(),
cardInfo.getCardNumber(), card.getCardNumber(),
cardInfo.usesVariousArt(), card.getUsesVariousArt(), // TODO: need to use usesVariousArt instead card?
card.getImageNumber() card.getImageNumber()
); );
imageFile = CardImageUtils.buildImagePathToCardOrToken(cardData); imageFile = CardImageUtils.buildImagePathToCardOrToken(cardData);
} else {
imageFile = "ERROR: can't find card info in repository - " + card.getName();
}
} }
return imageFile; return imageFile;
} }

View file

@ -0,0 +1,97 @@
package mage.view;
import mage.cards.Card;
import mage.game.command.CommandObject;
import mage.game.permanent.token.Token;
import mage.util.Copyable;
import java.io.Serializable;
/**
* TODO: delete, no needs?!
*
* GUI: card drawing info
* Can be different from real card name, set code, etc - see morph, copy, etc)
*
* @author JayDi85
*/
public class CardImageView implements Serializable, Copyable<CardImageView> {
private boolean isTokenRepository; // card or token database
private String cardName; // card or token
private String setCode; // card or token
private String cardNumber; // card only, token has "0"
private Integer imageNumber; // token only
private boolean isUseVariousArt; // card only
public CardImageView() {
}
public CardImageView(final CardImageView cardImageView) {
this.isTokenRepository = cardImageView.isTokenRepository;
this.cardName = cardImageView.cardName;
this.setCode = cardImageView.setCode;
this.cardNumber = cardImageView.cardNumber;
this.imageNumber = cardImageView.imageNumber;
this.isUseVariousArt = cardImageView.isUseVariousArt;
}
public CardImageView fromCard(Card card) {
this.isTokenRepository = false;
this.cardName = card.getName();
this.setCode = card.getExpansionSetCode();
this.cardNumber = card.getCardNumber();
this.imageNumber = card.getImageNumber();
this.isUseVariousArt = card.getUsesVariousArt();
return this;
}
public CardImageView fromToken(Token token) {
this.isTokenRepository = true;
this.cardName = token.getName();
this.setCode = token.getExpansionSetCode();
this.cardNumber = token.getCardNumber();
this.imageNumber = token.getImageNumber();
this.isUseVariousArt = false;
return this;
}
public CardImageView fromCommandObject(CommandObject commandObject) {
this.isTokenRepository = true;
this.cardName = commandObject.getName();
this.setCode = commandObject.getExpansionSetCode();
this.cardNumber = commandObject.getCardNumber();
this.imageNumber = commandObject.getImageNumber();
this.isUseVariousArt = false;
return this;
}
@Override
public CardImageView copy() {
return new CardImageView(this);
}
public boolean isTokenRepository() {
return isTokenRepository;
}
public String getCardName() {
return cardName;
}
public String getSetCode() {
return setCode;
}
public String getCardNumber() {
return cardNumber;
}
public Integer getImageNumber() {
return imageNumber;
}
public boolean isUseVariousArt() {
return isUseVariousArt;
}
}

View file

@ -16,9 +16,12 @@ import mage.abilities.icon.CardIcon;
import mage.abilities.icon.CardIconImpl; import mage.abilities.icon.CardIconImpl;
import mage.abilities.icon.CardIconType; import mage.abilities.icon.CardIconType;
import mage.abilities.keyword.AftermathAbility; import mage.abilities.keyword.AftermathAbility;
import mage.abilities.keyword.ForetellAbility;
import mage.cards.*; import mage.cards.*;
import mage.cards.mock.MockCard; import mage.cards.mock.MockCard;
import mage.cards.repository.CardInfo; import mage.cards.repository.CardInfo;
import mage.cards.repository.TokenInfo;
import mage.cards.repository.TokenRepository;
import mage.constants.*; import mage.constants.*;
import mage.counters.Counter; import mage.counters.Counter;
import mage.counters.CounterType; import mage.counters.CounterType;
@ -89,7 +92,8 @@ public class CardView extends SimpleCardView {
protected boolean isToken; protected boolean isToken;
protected CardView ability; protected CardView ability;
protected int imageNumber; protected String imageFileName = "";
protected int imageNumber = 0;
protected boolean extraDeckCard; protected boolean extraDeckCard;
protected boolean transformable; // can toggle one card side to another (transformable cards, modal double faces) protected boolean transformable; // can toggle one card side to another (transformable cards, modal double faces)
@ -134,12 +138,12 @@ public class CardView extends SimpleCardView {
protected List<CardIcon> cardIcons = new ArrayList<>(); // additional icons to render protected List<CardIcon> cardIcons = new ArrayList<>(); // additional icons to render
// GUI related: additional info about current object (example: real PT) // GUI related: additional info about current object (example: real PT)
// warning, do not send full object, use some fields only (client must not get any server side data)
// warning, don't forget to hide it in face down cards (null)
protected MageInt originalPower = null; protected MageInt originalPower = null;
protected MageInt originalToughness = null; protected MageInt originalToughness = null;
protected FilterMana originalColorIdentity = null; protected String originalColorIdentity = null; // GUI related info for sorting, searching, etc
protected UUID originalId = null;
protected boolean originalIsCopy = false; protected boolean originalIsCopy = false;
protected boolean originalIsCard = false;
/** /**
* Non game usage like deck editor * Non game usage like deck editor
@ -154,6 +158,17 @@ public class CardView extends SimpleCardView {
this(card, game, false); this(card, game, false);
} }
/**
* @param card
* @param game
* @param showAsControlled is the card view created for the card controller - used
* for morph / face down cards to know which player may see information for
* the card TODO: turn controller can be here too?
*/
public CardView(Card card, Game game, boolean showAsControlled) {
this(card, game, showAsControlled, false);
}
public CardView(Card card, SimpleCardView simpleCardView) { public CardView(Card card, SimpleCardView simpleCardView) {
this(card, null, false); this(card, null, false);
this.id = simpleCardView.getId(); this.id = simpleCardView.getId();
@ -163,11 +178,6 @@ public class CardView extends SimpleCardView {
this.isSelected = simpleCardView.isSelected; this.isSelected = simpleCardView.isSelected;
} }
public CardView(Card card, Game game, UUID cardId) {
this(card, game, false);
this.id = cardId;
}
public CardView(final CardView cardView) { public CardView(final CardView cardView) {
super(cardView); super(cardView);
@ -192,6 +202,7 @@ public class CardView extends SimpleCardView {
this.expansionSetCode = cardView.expansionSetCode; this.expansionSetCode = cardView.expansionSetCode;
this.cardNumber = cardView.cardNumber; this.cardNumber = cardView.cardNumber;
this.imageFileName = cardView.imageFileName;
this.imageNumber = cardView.imageNumber; this.imageNumber = cardView.imageNumber;
this.color = cardView.color.copy(); this.color = cardView.color.copy();
@ -246,32 +257,21 @@ public class CardView extends SimpleCardView {
this.canAttack = cardView.canAttack; this.canAttack = cardView.canAttack;
this.canBlock = cardView.canBlock; this.canBlock = cardView.canBlock;
this.inViewerOnly = cardView.inViewerOnly; this.inViewerOnly = cardView.inViewerOnly;
this.originalPower = cardView.originalPower;
this.originalToughness = cardView.originalToughness;
this.originalColorIdentity = cardView.originalColorIdentity;
this.originalId = cardView.originalId;
this.originalIsCard = cardView.originalIsCard;
this.originalIsCopy = cardView.originalIsCopy;
if (cardView.cardIcons != null) { if (cardView.cardIcons != null) {
cardView.cardIcons.forEach(icon -> this.cardIcons.add(icon.copy())); cardView.cardIcons.forEach(icon -> this.cardIcons.add(icon.copy()));
} }
this.originalPower = cardView.originalPower;
this.originalToughness = cardView.originalToughness;
this.originalColorIdentity = cardView.originalColorIdentity;
this.originalIsCopy = cardView.originalIsCopy;
this.playableStats = cardView.playableStats.copy(); this.playableStats = cardView.playableStats.copy();
this.isChoosable = cardView.isChoosable; this.isChoosable = cardView.isChoosable;
this.isSelected = cardView.isSelected; this.isSelected = cardView.isSelected;
} }
/**
* @param card
* @param game
* @param controlled is the card view created for the card controller - used
* for morph / face down cards to know which player may see information for
* the card
*/
public CardView(Card card, Game game, boolean controlled) {
this(card, game, controlled, false, false);
}
private static String getCardTypeLine(Game game, Card card) { private static String getCardTypeLine(Game game, Card card) {
StringBuilder sbType = new StringBuilder(); StringBuilder sbType = new StringBuilder();
for (SuperType superType : card.getSuperType(game)) { for (SuperType superType : card.getSuperType(game)) {
@ -290,67 +290,104 @@ public class CardView extends SimpleCardView {
} }
/** /**
* @param card * @param sourceCard
* @param game * @param game
* @param controlled is the card view created for the card controller - used * @param showAsControlled is the card view created for the card controller/owner - used
* for morph / face down cards to know which player may see information for * for morph / face down cards to know which player may see information for
* the card * the card
* @param showFaceDownCard if true and the card is not on the battlefield,
* also a face down card is shown in the view, face down cards will be shown
* @param storeZone if true the card zone will be set in the zone attribute. * @param storeZone if true the card zone will be set in the zone attribute.
*/ */
public CardView(Card card, Game game, boolean controlled, boolean showFaceDownCard, boolean storeZone) { public CardView(Card sourceCard, Game game, boolean showAsControlled, boolean storeZone) {
super(card.getId(), card.getExpansionSetCode(), card.getCardNumber(), card.getUsesVariousArt(), game != null); super(sourceCard.getId(), sourceCard.getExpansionSetCode(), sourceCard.getCardNumber(), sourceCard.getUsesVariousArt(), game != null);
this.setOriginalValues(card);
this.imageNumber = card.getImageNumber(); // TODO: it's too big and can be buggy (something miss?) - must check and refactor: setup face down/up params, setup shared data like counters and targets
// Visible logic:
// * Normal card:
// - original name, original image
// * Face down card:
// * my cards or game end:
// - face down status + original name, face down image, day/night button
// * opponent cards:
// - face down status, face down image
// find real name from original card, cause face down status can be applied to card/spell
String sourceName = sourceCard.getMainCard().getName();
// find real spell characteristics before resolve
Card card = sourceCard.copy();
if (game != null && card instanceof Spell) {
card = ((Spell) card).getSpellAbility().getCharacteristics(game);
}
// use isFaceDown(game) only here to find real status, all other code must use this.faceDown
this.faceDown = game != null && sourceCard.isFaceDown(game);
boolean showFaceUp = !this.faceDown;
// show real name and day/night button for controller or any player at the game's end
boolean showHiddenFaceDownData = showAsControlled || (game != null && game.hasEnded());
// default image info
this.expansionSetCode = card.getExpansionSetCode();
this.cardNumber = card.getCardNumber();
this.imageFileName = card.getImageFileName();
this.imageNumber = card.getImageNumber();
this.usesVariousArt = card.getUsesVariousArt();
// permanent data
if (showFaceUp) {
this.setOriginalValues(card);
}
// no information available for face down cards as long it's not a controlled face down morph card
// TODO: Better handle this in Framework (but currently I'm not sure how to do it there) LevelX2
boolean showFaceUp = true;
if (game != null) { if (game != null) {
Zone cardZone = game.getState().getZone(card.getId()); Zone cardZone = game.getState().getZone(card.getId());
if (card.isFaceDown(game)) {
showFaceUp = false;
if (Zone.BATTLEFIELD != cardZone) {
if (showFaceDownCard) {
showFaceUp = true;
}
}
}
if (storeZone) { if (storeZone) {
// TODO: research, why it used here?
this.zone = cardZone; this.zone = cardZone;
} }
} }
// boolean showFaceUp = game == null || !card.isFaceDown(game) || (!game.getState().getZone(card.getId()).equals(Zone.BATTLEFIELD) && showFaceDownCard);
// FACE DOWN
if (!showFaceUp) { if (!showFaceUp) {
this.fillEmpty(card, controlled); this.fillEmptyWithImageInfo(game, card, true);
if (card instanceof Spell) {
// TODO: add face down image here??? // can show face up card name for controller or game end
// special handling for casting of Morph cards String visibleName = CardUtil.getCardNameForGUI(showHiddenFaceDownData ? sourceName : "", this.imageFileName);
if (controlled) { this.name = visibleName;
this.name = card.getName(); this.displayName = visibleName;
this.displayName = card.getName(); this.displayFullName = visibleName;
this.displayFullName = card.getName(); this.alternateName = visibleName;
this.alternateName = card.getName();
} // workaround to add PT, creature type and face up ability text (for stack and battlefield zones only)
this.power = "2"; // in other zones it has only face down status/name
this.toughness = "2"; if (sourceCard instanceof Spell
this.rules.add("You may cast this card as a 2/2 face-down creature, with no text," || card instanceof Permanent) {
+ " no name, no subtypes, and no mana cost by paying {3} rather than paying its mana cost.");
return;
} else if (card instanceof Permanent) {
this.power = Integer.toString(card.getPower().getValue()); this.power = Integer.toString(card.getPower().getValue());
this.toughness = Integer.toString(card.getToughness().getValue()); this.toughness = Integer.toString(card.getToughness().getValue());
this.cardTypes = new ArrayList<>(card.getCardType(game)); this.cardTypes = new ArrayList<>(card.getCardType());
this.faceDown = card.isFaceDown(game); this.rules = new ArrayList<>(card.getRules());
} else {
// this.hideInfo = true; // additional rules for stack (example: morph ability text)
return; if (sourceCard instanceof Spell) {
List<String> extraRules = sourceCard.getSpellAbility().getSpellAbilityCastMode().getAdditionalRulesOnStack();
if (extraRules != null) {
this.rules.addAll(extraRules);
}
} }
} }
// GUI: enable day/night button to view original face up card
if (showHiddenFaceDownData) {
this.transformable = true;
this.secondCardFace = new CardView(sourceCard.getMainCard()); // do not use game param, so it will take default card
this.alternateName = sourceCard.getMainCard().getName();
}
}
// FACE UP and shared data like counters
if (showFaceUp) {
SplitCard splitCard = null; SplitCard splitCard = null;
if (card instanceof SplitCard) { if (card instanceof SplitCard) {
splitCard = (SplitCard) card; splitCard = (SplitCard) card;
@ -428,7 +465,9 @@ public class CardView extends SimpleCardView {
this.displayFullName = fullCardName; this.displayFullName = fullCardName;
this.rules = new ArrayList<>(card.getRules(game)); this.rules = new ArrayList<>(card.getRules(game));
this.manaValue = card.getManaValue(); this.manaValue = card.getManaValue();
}
// shared info - counters and other
if (card instanceof Permanent) { if (card instanceof Permanent) {
this.mageObjectType = MageObjectType.PERMANENT; this.mageObjectType = MageObjectType.PERMANENT;
Permanent permanent = (Permanent) card; Permanent permanent = (Permanent) card;
@ -466,6 +505,8 @@ public class CardView extends SimpleCardView {
} }
} }
// FACE UP INFO
if (showFaceUp) {
this.power = Integer.toString(card.getPower().getValue()); this.power = Integer.toString(card.getPower().getValue());
this.toughness = Integer.toString(card.getToughness().getValue()); this.toughness = Integer.toString(card.getToughness().getValue());
this.cardTypes = new ArrayList<>(card.getCardType(game)); this.cardTypes = new ArrayList<>(card.getCardType(game));
@ -473,12 +514,11 @@ public class CardView extends SimpleCardView {
this.superTypes = card.getSuperType(game); this.superTypes = card.getSuperType(game);
this.color = card.getColor(game).copy(); this.color = card.getColor(game).copy();
this.flipCard = card.isFlipCard(); this.flipCard = card.isFlipCard();
this.faceDown = !showFaceUp;
if (card instanceof PermanentToken) { if (card instanceof PermanentToken) {
this.isToken = true; this.isToken = true;
this.mageObjectType = MageObjectType.TOKEN; this.mageObjectType = MageObjectType.TOKEN;
this.rarity = Rarity.COMMON; this.rarity = Rarity.SPECIAL;
this.rules = new ArrayList<>(card.getRules(game)); this.rules = new ArrayList<>(card.getRules(game));
} else { } else {
this.rarity = card.getRarity(); this.rarity = card.getRarity();
@ -520,7 +560,9 @@ public class CardView extends SimpleCardView {
this.secondCardFace = new CardView(backFace, game); this.secondCardFace = new CardView(backFace, game);
this.alternateName = backFace.getName(); this.alternateName = backFace.getName();
} }
}
// shared info - targets
if (card instanceof Spell) { if (card instanceof Spell) {
this.mageObjectType = MageObjectType.SPELL; this.mageObjectType = MageObjectType.SPELL;
Spell spell = (Spell) card; Spell spell = (Spell) card;
@ -533,31 +575,6 @@ public class CardView extends SimpleCardView {
} }
} }
// Determine what part of the art to slice out for spells on the stack which originate
// from a split, fuse, or aftermath split card.
// Modal double faces cards draws as normal cards
SpellAbilityType ty = spell.getSpellAbility().getSpellAbilityType();
if (ty == SpellAbilityType.SPLIT_RIGHT || ty == SpellAbilityType.SPLIT_LEFT || ty == SpellAbilityType.SPLIT_FUSED) {
// Needs a special art rect
if (ty == SpellAbilityType.SPLIT_FUSED) {
artRect = ArtRect.SPLIT_FUSED;
} else if (spell.getCard() != null) {
SplitCard wholeCard = ((SplitCardHalf) spell.getCard()).getParentCard();
Abilities<Ability> aftermathHalfAbilities = wholeCard.getRightHalfCard().getAbilities(game);
if (aftermathHalfAbilities.stream().anyMatch(AftermathAbility.class::isInstance)) {
if (ty == SpellAbilityType.SPLIT_RIGHT) {
artRect = ArtRect.AFTERMATH_BOTTOM;
} else {
artRect = ArtRect.AFTERMATH_TOP;
}
} else if (ty == SpellAbilityType.SPLIT_RIGHT) {
artRect = ArtRect.SPLIT_RIGHT;
} else {
artRect = ArtRect.SPLIT_LEFT;
}
}
}
// show for modal spell, which mode was chosen // show for modal spell, which mode was chosen
if (spell.getSpellAbility().isModal()) { if (spell.getSpellAbility().isModal()) {
for (UUID modeId : spell.getSpellAbility().getModes().getSelectedModes()) { for (UUID modeId : spell.getSpellAbility().getModes().getSelectedModes()) {
@ -580,7 +597,36 @@ public class CardView extends SimpleCardView {
} }
} }
} }
}
}
// render info
if (showFaceUp) {
if (card instanceof Spell) {
Spell spell = (Spell) card;
// Determine what part of the art to slice out for spells on the stack which originate
// from a split, fuse, or aftermath split card.
// Modal double faces cards draws as normal cards
SpellAbilityType ty = spell.getSpellAbility().getSpellAbilityType();
if (ty == SpellAbilityType.SPLIT_RIGHT || ty == SpellAbilityType.SPLIT_LEFT || ty == SpellAbilityType.SPLIT_FUSED) {
// Needs a special art rect
if (ty == SpellAbilityType.SPLIT_FUSED) {
artRect = ArtRect.SPLIT_FUSED;
} else if (spell.getCard() != null) {
SplitCard wholeCard = ((SplitCardHalf) spell.getCard()).getParentCard();
Abilities<Ability> aftermathHalfAbilities = wholeCard.getRightHalfCard().getAbilities(game);
if (aftermathHalfAbilities.stream().anyMatch(AftermathAbility.class::isInstance)) {
if (ty == SpellAbilityType.SPLIT_RIGHT) {
artRect = ArtRect.AFTERMATH_BOTTOM;
} else {
artRect = ArtRect.AFTERMATH_TOP;
}
} else if (ty == SpellAbilityType.SPLIT_RIGHT) {
artRect = ArtRect.SPLIT_RIGHT;
} else {
artRect = ArtRect.SPLIT_LEFT;
}
}
} }
} }
@ -607,6 +653,7 @@ public class CardView extends SimpleCardView {
// add card icons at the end, so it will have full card view data // add card icons at the end, so it will have full card view data
this.generateCardIcons(null, card, game); this.generateCardIcons(null, card, game);
} }
}
/** /**
* Generate card icons for current object (support card, permanent or stack ability) * Generate card icons for current object (support card, permanent or stack ability)
@ -740,11 +787,14 @@ public class CardView extends SimpleCardView {
} }
} }
@Deprecated // TODO: research and raplace all usages to normal calls, see constructors for EmblemView and other
public CardView(MageObject object, Game game) { public CardView(MageObject object, Game game) {
super(object.getId(), object.getExpansionSetCode(), object.getCardNumber(), false, true); super(object.getId(), object.getExpansionSetCode(), object.getCardNumber(), false, true);
this.setOriginalValues(object); this.setOriginalValues(object);
this.imageFileName = object.getImageFileName();
this.imageNumber = object.getImageNumber(); this.imageNumber = object.getImageNumber();
this.name = object.getName(); this.name = object.getName();
this.displayName = object.getName(); this.displayName = object.getName();
this.displayFullName = object.getName(); this.displayFullName = object.getName();
@ -770,7 +820,7 @@ public class CardView extends SimpleCardView {
if (object instanceof PermanentToken) { if (object instanceof PermanentToken) {
this.mageObjectType = MageObjectType.TOKEN; this.mageObjectType = MageObjectType.TOKEN;
PermanentToken permanentToken = (PermanentToken) object; PermanentToken permanentToken = (PermanentToken) object;
this.rarity = Rarity.COMMON; this.rarity = Rarity.SPECIAL;
this.rules = new ArrayList<>(permanentToken.getRules(game)); this.rules = new ArrayList<>(permanentToken.getRules(game));
} else if (object instanceof Emblem) { } else if (object instanceof Emblem) {
this.mageObjectType = MageObjectType.EMBLEM; this.mageObjectType = MageObjectType.EMBLEM;
@ -837,9 +887,10 @@ public class CardView extends SimpleCardView {
this.frameStyle = FrameStyle.M15_NORMAL; this.frameStyle = FrameStyle.M15_NORMAL;
this.expansionSetCode = emblem.getExpansionSetCode(); this.expansionSetCode = emblem.getExpansionSetCode();
this.cardNumber = emblem.getCardNumber(); this.cardNumber = emblem.getCardNumber();
this.imageFileName = emblem.getImageFileName();
this.imageNumber = emblem.getImageNumber(); this.imageNumber = emblem.getImageNumber();
this.usesVariousArt = emblem.getUsesVariousArt(); this.usesVariousArt = emblem.getUsesVariousArt();
this.rarity = Rarity.COMMON; this.rarity = Rarity.SPECIAL;
this.playableStats = emblem.playableStats.copy(); this.playableStats = emblem.playableStats.copy();
this.isChoosable = emblem.isChoosable(); this.isChoosable = emblem.isChoosable();
@ -859,8 +910,9 @@ public class CardView extends SimpleCardView {
this.frameStyle = FrameStyle.M15_NORMAL; this.frameStyle = FrameStyle.M15_NORMAL;
this.expansionSetCode = dungeon.getExpansionSetCode(); this.expansionSetCode = dungeon.getExpansionSetCode();
this.cardNumber = ""; this.cardNumber = "";
this.imageFileName = "";
this.imageNumber = 0; this.imageNumber = 0;
this.rarity = Rarity.COMMON; this.rarity = Rarity.SPECIAL;
this.playableStats = dungeon.playableStats.copy(); this.playableStats = dungeon.playableStats.copy();
this.isChoosable = dungeon.isChoosable(); this.isChoosable = dungeon.isChoosable();
@ -881,8 +933,9 @@ public class CardView extends SimpleCardView {
this.frameStyle = FrameStyle.M15_NORMAL; this.frameStyle = FrameStyle.M15_NORMAL;
this.expansionSetCode = plane.getExpansionSetCode(); this.expansionSetCode = plane.getExpansionSetCode();
this.cardNumber = ""; this.cardNumber = "";
this.imageFileName = "";
this.imageNumber = 0; this.imageNumber = 0;
this.rarity = Rarity.COMMON; this.rarity = Rarity.SPECIAL;
this.playableStats = plane.playableStats.copy(); this.playableStats = plane.playableStats.copy();
this.isChoosable = plane.isChoosable(); this.isChoosable = plane.isChoosable();
@ -903,8 +956,9 @@ public class CardView extends SimpleCardView {
this.cardNumber = designation.getCardNumber(); this.cardNumber = designation.getCardNumber();
this.expansionSetCode = designation.getExpansionSetCode(); this.expansionSetCode = designation.getExpansionSetCode();
this.cardNumber = ""; this.cardNumber = "";
this.imageFileName = "";
this.imageNumber = 0; this.imageNumber = 0;
this.rarity = Rarity.COMMON; this.rarity = Rarity.SPECIAL;
// no playable/chooseable marks for designations // no playable/chooseable marks for designations
} }
@ -913,7 +967,7 @@ public class CardView extends SimpleCardView {
if (!empty) { if (!empty) {
throw new IllegalArgumentException("Not supported."); throw new IllegalArgumentException("Not supported.");
} }
fillEmpty(null, false); fillEmptyWithImageInfo(null, null, false);
} }
public static boolean cardViewEquals(CardView a, CardView b) { // TODO: This belongs in CardView public static boolean cardViewEquals(CardView a, CardView b) { // TODO: This belongs in CardView
@ -936,14 +990,21 @@ public class CardView extends SimpleCardView {
&& a.getManaCostStr().equals(b.getManaCostStr()) && a.getManaCostStr().equals(b.getManaCostStr())
&& a.getRules().equals(b.getRules()) && a.getRules().equals(b.getRules())
&& Objects.equals(a.getRarity(), b.getRarity()) && Objects.equals(a.getRarity(), b.getRarity())
&& Objects.equals(a.getCardNumber(), b.getCardNumber())
&& Objects.equals(a.getExpansionSetCode(), b.getExpansionSetCode())
&& a.getFrameStyle() == b.getFrameStyle() && a.getFrameStyle() == b.getFrameStyle()
&& Objects.equals(a.getCounters(), b.getCounters()) && Objects.equals(a.getCounters(), b.getCounters())
&& a.isFaceDown() == b.isFaceDown())) { && a.isFaceDown() == b.isFaceDown())) {
return false; return false;
} }
if (!(Objects.equals(a.getExpansionSetCode(), b.getExpansionSetCode())
&& Objects.equals(a.getCardNumber(), b.getCardNumber())
&& Objects.equals(a.getImageNumber(), b.getImageNumber())
&& Objects.equals(a.getImageFileName(), b.getImageFileName())
)) {
return false;
}
if (!(a instanceof PermanentView)) { if (!(a instanceof PermanentView)) {
return true; return true;
} }
@ -953,10 +1014,16 @@ public class CardView extends SimpleCardView {
&& aa.getDamage() == bb.getDamage(); && aa.getDamage() == bb.getDamage();
} }
private void fillEmpty(Card card, boolean controlled) { private void fillEmptyWithImageInfo(Game game, Card imageSourceCard, boolean isFaceDown) {
this.name = "Face Down"; this.name = "";
this.displayName = name; this.displayName = "";
this.displayFullName = name; this.displayFullName = "";
this.expansionSetCode = "";
this.cardNumber = "0";
this.imageFileName = "";
this.imageNumber = 0;
this.usesVariousArt = false;
this.rules = new ArrayList<>(); this.rules = new ArrayList<>();
this.power = ""; this.power = "";
this.toughness = ""; this.toughness = "";
@ -973,33 +1040,68 @@ public class CardView extends SimpleCardView {
this.manaCostLeftStr = new ArrayList<>(); this.manaCostLeftStr = new ArrayList<>();
this.manaCostRightStr = new ArrayList<>(); this.manaCostRightStr = new ArrayList<>();
this.manaValue = 0; this.manaValue = 0;
this.rarity = Rarity.SPECIAL; // hide rarity info
// the controller can see more information (e.g. enlarged image) than other players for face down cards (e.g. Morph played face down) if (imageSourceCard != null) {
if (!controlled) { // keep inner images info (server side card already contain actual info)
this.rarity = Rarity.COMMON; String imageSetCode = imageSourceCard.getExpansionSetCode();
this.expansionSetCode = ""; String imageCardNumber = imageSourceCard.getCardNumber();
this.cardNumber = "0"; String imageFileName = imageSourceCard.getImageFileName();
this.imageNumber = 0; Integer imageNumber = imageSourceCard.getImageNumber();
} else { boolean imageUsesVariousArt = imageSourceCard.getUsesVariousArt();
this.rarity = card.getRarity(); if (imageSetCode.equals(TokenRepository.XMAGE_TOKENS_SET_CODE)) {
this.expansionSetCode = imageSetCode;
this.cardNumber = imageCardNumber;
this.imageFileName = imageFileName;
this.imageNumber = imageNumber;
this.usesVariousArt = imageUsesVariousArt;
} }
if (card != null) { if (imageSourceCard instanceof PermanentToken) {
if (card instanceof Permanent) { this.mageObjectType = MageObjectType.TOKEN;
} else if (imageSourceCard instanceof Permanent) {
this.mageObjectType = MageObjectType.PERMANENT; this.mageObjectType = MageObjectType.PERMANENT;
} else if (card.isCopy()) { } else if (imageSourceCard.isCopy()) {
this.mageObjectType = MageObjectType.COPY_CARD; this.mageObjectType = MageObjectType.COPY_CARD;
} else if (imageSourceCard instanceof Spell) {
this.mageObjectType = MageObjectType.SPELL;
} else { } else {
this.mageObjectType = MageObjectType.CARD; this.mageObjectType = MageObjectType.CARD;
} }
if (card instanceof PermanentToken) {
this.mageObjectType = MageObjectType.TOKEN;
}
if (card instanceof Spell) {
this.mageObjectType = MageObjectType.SPELL;
}
} }
// make default face down image
// TODO: implement diff backface images someday and insert here (user data + card owner)
if (isFaceDown && this.imageFileName.isEmpty()) {
this.name = "";
this.displayName = this.name;
this.displayFullName = this.name;
// as foretell face down
// TODO: it's not ok to use that code - server side objects must has all data, see BecomesFaceDownCreatureEffect.makeFaceDownObject
// it must be a more global bug for card characteristics, not client side viewer
if (game != null && imageSourceCard != null && ForetellAbility.isCardInForetell(imageSourceCard, game)) {
TokenInfo tokenInfo = TokenRepository.instance.findPreferredTokenInfoForXmage(TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_FORETELL, this.getId());
if (tokenInfo != null) {
this.expansionSetCode = tokenInfo.getSetCode();
this.cardNumber = "0";
this.imageFileName = tokenInfo.getName();
this.imageNumber = tokenInfo.getImageNumber();
this.usesVariousArt = false;
}
return;
}
// as normal face down
TokenInfo tokenInfo = TokenRepository.instance.findPreferredTokenInfoForXmage(TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANUAL, this.getId());
if (tokenInfo != null) {
this.expansionSetCode = tokenInfo.getSetCode();
this.cardNumber = "0";
this.imageFileName = tokenInfo.getName();
this.imageNumber = tokenInfo.getImageNumber();
this.usesVariousArt = false;
}
}
} }
CardView(Token token, Game game) { CardView(Token token, Game game) {
@ -1026,9 +1128,9 @@ public class CardView extends SimpleCardView {
this.manaCostRightStr = new ArrayList<>(); this.manaCostRightStr = new ArrayList<>();
this.rarity = Rarity.SPECIAL; this.rarity = Rarity.SPECIAL;
// source object is a token, so no card number
this.expansionSetCode = token.getExpansionSetCode(); this.expansionSetCode = token.getExpansionSetCode();
this.cardNumber = token.getCardNumber(); this.cardNumber = token.getCardNumber();
this.imageFileName = token.getImageFileName();
this.imageNumber = token.getImageNumber(); this.imageNumber = token.getImageNumber();
} }
@ -1063,18 +1165,15 @@ public class CardView extends SimpleCardView {
if (object == null) { if (object == null) {
return; return;
} }
// Only valid objects to transfer original values are Card and Token // only valid objects to transfer original values are Card and Token
if (object instanceof Card || object instanceof Token) { if (object instanceof Card || object instanceof Token) {
this.originalPower = object.getPower(); this.originalPower = object.getPower();
this.originalToughness = object.getToughness(); this.originalToughness = object.getToughness();
this.originalIsCopy = object.isCopy(); this.originalIsCopy = object.isCopy();
this.originalId = object.getId();
if (object instanceof Card) { if (object instanceof Card) {
this.originalColorIdentity = ((Card) object).getColorIdentity(); this.originalColorIdentity = findColorIdentityStr(((Card) object).getColorIdentity());
this.originalIsCard = true; } else {
} else if (object instanceof Token) { this.originalColorIdentity = findColorIdentityStr(ManaUtil.getColorIdentity((Token) object));
this.originalColorIdentity = ManaUtil.getColorIdentity((Token) object);
} }
} }
} }
@ -1190,9 +1289,8 @@ public class CardView extends SimpleCardView {
return rarity; return rarity;
} }
public String getColorIdentityStr() { public String findColorIdentityStr(FilterMana colorInfo) {
FilterMana colorInfo = this.originalColorIdentity; if (colorInfo == null) {
if (colorInfo != null) {
colorInfo = new FilterMana(); colorInfo = new FilterMana();
} }
@ -1350,6 +1448,10 @@ public class CardView extends SimpleCardView {
return bandedCards; return bandedCards;
} }
public String getImageFileName() {
return imageFileName;
}
public int getImageNumber() { public int getImageNumber() {
return imageNumber; return imageNumber;
} }
@ -1488,18 +1590,14 @@ public class CardView extends SimpleCardView {
return this.originalToughness; return this.originalToughness;
} }
public UUID getOriginalId() { public String getOriginalColorIdentity() {
return this.originalId; return this.originalColorIdentity != null ? this.originalColorIdentity : "";
} }
public boolean isOriginalACopy() { public boolean isOriginalACopy() {
return this.originalIsCopy; return this.originalIsCopy;
} }
public boolean isOriginalACard() {
return this.originalIsCard;
}
public List<CardIcon> getCardIcons() { public List<CardIcon> getCardIcons() {
return this.cardIcons; return this.cardIcons;
} }

View file

@ -12,6 +12,7 @@ import mage.game.command.Plane;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentToken; import mage.game.permanent.PermanentToken;
import mage.target.targetpointer.TargetPointer; import mage.target.targetpointer.TargetPointer;
import mage.util.CardUtil;
import mage.util.GameLog; import mage.util.GameLog;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
@ -50,15 +51,15 @@ public class CardsView extends LinkedHashMap<UUID, CardView> {
} }
} }
public CardsView(Game game, Collection<? extends Card> cards) { public CardsView(Game game, Collection<? extends Card> cards, UUID createdForPlayerId) {
for (Card card : cards) { for (Card card : cards) {
this.put(card.getId(), new CardView(card, game, false)); this.put(card.getId(), new CardView(card, game, CardUtil.canShowAsControlled(card, createdForPlayerId)));
} }
} }
public CardsView(Game game, Collection<? extends Card> cards, boolean showFaceDown, boolean storeZone) { public CardsView(Game game, Collection<? extends Card> cards, UUID createdForPlayerId, boolean storeZone) {
for (Card card : cards) { for (Card card : cards) {
this.put(card.getId(), new CardView(card, game, false, showFaceDown, storeZone)); this.put(card.getId(), new CardView(card, game, CardUtil.canShowAsControlled(card, createdForPlayerId), storeZone));
} }
} }

View file

@ -14,6 +14,8 @@ public interface CommandObjectView extends SelectableObjectView {
UUID getId(); UUID getId();
String getImageFileName();
int getImageNumber(); int getImageNumber();
List<String> getRules(); List<String> getRules();

View file

@ -2,10 +2,13 @@
package mage.view; package mage.view;
import java.io.Serializable; import java.io.Serializable;
import java.util.UUID;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.MageObjectType; import mage.constants.MageObjectType;
import mage.game.Game; import mage.game.Game;
import mage.game.command.Commander; import mage.game.command.Commander;
import mage.util.CardUtil;
/** /**
* *
@ -13,8 +16,8 @@ import mage.game.command.Commander;
*/ */
public class CommanderView extends CardView implements CommandObjectView, Serializable{ public class CommanderView extends CardView implements CommandObjectView, Serializable{
public CommanderView(Commander commander, Card sourceCard, Game game) { public CommanderView(Commander commander, Card sourceCard, Game game, UUID createdForPlayerId) {
super(sourceCard, game, false); super(sourceCard, game, CardUtil.canShowAsControlled(sourceCard, createdForPlayerId));
this.mageObjectType = MageObjectType.COMMANDER; this.mageObjectType = MageObjectType.COMMANDER;
} }
} }

View file

@ -14,15 +14,17 @@ public class DungeonView implements CommandObjectView, Serializable {
protected UUID id; protected UUID id;
protected String name; protected String name;
protected int imageNum; protected String imageFileName = "";
protected String expansionSetCode; protected int imageNumber = 0;
protected String expansionSetCode = "";
protected List<String> rules; protected List<String> rules;
protected PlayableObjectStats playableStats = new PlayableObjectStats(); protected PlayableObjectStats playableStats = new PlayableObjectStats();
public DungeonView(Dungeon dungeon) { public DungeonView(Dungeon dungeon) {
this.id = dungeon.getId(); this.id = dungeon.getId();
this.name = dungeon.getName(); this.name = dungeon.getName();
this.imageNum = dungeon.getImageNumber(); this.imageFileName = dungeon.getImageFileName();
this.imageNumber = dungeon.getImageNumber();
this.expansionSetCode = dungeon.getExpansionSetCode(); this.expansionSetCode = dungeon.getExpansionSetCode();
this.rules = dungeon.getRules(); this.rules = dungeon.getRules();
} }
@ -42,9 +44,14 @@ public class DungeonView implements CommandObjectView, Serializable {
return id; return id;
} }
@Override
public String getImageFileName() {
return imageFileName;
}
@Override @Override
public int getImageNumber() { public int getImageNumber() {
return imageNum; return imageNumber;
} }
@Override @Override

View file

@ -16,7 +16,8 @@ public class EmblemView implements CommandObjectView, Serializable {
protected UUID id; protected UUID id;
protected String name; protected String name;
protected String cardNumber = ""; protected String cardNumber = "";
protected int imageNum; protected String imageFileName = "";
protected int imageNumber;
protected boolean usesVariousArt = false; protected boolean usesVariousArt = false;
protected String expansionSetCode; protected String expansionSetCode;
protected List<String> rules; protected List<String> rules;
@ -25,7 +26,8 @@ public class EmblemView implements CommandObjectView, Serializable {
public EmblemView(Emblem emblem) { public EmblemView(Emblem emblem) {
this.id = emblem.getId(); this.id = emblem.getId();
this.name = emblem.getName(); this.name = emblem.getName();
this.imageNum = emblem.getImageNumber(); this.imageFileName = emblem.getImageFileName();
this.imageNumber = emblem.getImageNumber();
this.expansionSetCode = emblem.getExpansionSetCode(); this.expansionSetCode = emblem.getExpansionSetCode();
this.rules = emblem.getAbilities().getRules(emblem.getName()); this.rules = emblem.getAbilities().getRules(emblem.getName());
if (emblem instanceof EmblemOfCard) { if (emblem instanceof EmblemOfCard) {
@ -54,9 +56,15 @@ public class EmblemView implements CommandObjectView, Serializable {
} }
@Override @Override
public int getImageNumber() { public String getImageFileName() {
return imageNum; return imageFileName;
} }
@Override
public int getImageNumber() {
return imageNumber;
}
public boolean getUsesVariousArt() { public boolean getUsesVariousArt() {
return this.usesVariousArt; return this.usesVariousArt;
} }

View file

@ -6,6 +6,7 @@ import java.util.UUID;
import mage.cards.Card; import mage.cards.Card;
import mage.game.ExileZone; import mage.game.ExileZone;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil;
/** /**
* *
@ -17,11 +18,11 @@ public class ExileView extends CardsView {
private final String name; private final String name;
private final UUID id; private final UUID id;
public ExileView(ExileZone exileZone, Game game) { public ExileView(ExileZone exileZone, Game game, UUID createdForPlayerId) {
this.name = exileZone.getName(); this.name = exileZone.getName();
this.id = exileZone.getId(); this.id = exileZone.getId();
for (Card card: exileZone.getCards(game)) { for (Card card: exileZone.getCards(game)) {
this.put(card.getId(), new CardView(card, game, false)); this.put(card.getId(), new CardView(card, game, CardUtil.canShowAsControlled(card, createdForPlayerId)));
} }
} }

View file

@ -24,6 +24,7 @@ import mage.game.stack.StackAbility;
import mage.game.stack.StackObject; import mage.game.stack.StackObject;
import mage.players.PlayableObjectsList; import mage.players.PlayableObjectsList;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable; import java.io.Serializable;
@ -73,15 +74,16 @@ public class GameView implements Serializable {
if (player.getId().equals(createdForPlayerId)) { if (player.getId().equals(createdForPlayerId)) {
createdForPlayer = player; createdForPlayer = player;
this.myPlayerId = player.getId(); this.myPlayerId = player.getId();
this.myHand.putAll(new CardsView(game, player.getHand().getCards(game))); this.myHand.putAll(new CardsView(game, player.getHand().getCards(game), createdForPlayerId));
} }
} }
for (StackObject stackObject : state.getStack()) { for (StackObject stackObject : state.getStack()) {
if (stackObject instanceof Spell) { if (stackObject instanceof Spell) {
// Spell // Spell
CardView spellView = new CardView((Spell) stackObject, game, stackObject.getControllerId().equals(createdForPlayerId)); Spell spell = (Spell) stackObject;
spellView.paid = ((Spell) stackObject).getSpellAbility().getManaCostsToPay().isPaid(); CardView spellView = new CardView(spell, game, CardUtil.canShowAsControlled(spell, createdForPlayerId));
stack.put(stackObject.getId(), spellView); spellView.paid = spell.getSpellAbility().getManaCostsToPay().isPaid();
stack.put(spell.getId(), spellView);
} else if (stackObject instanceof StackAbility) { } else if (stackObject instanceof StackAbility) {
// Stack Ability // Stack Ability
MageObject object = game.getObject(stackObject.getSourceId()); MageObject object = game.getObject(stackObject.getSourceId());
@ -93,9 +95,9 @@ public class GameView implements Serializable {
if (object != null) { if (object != null) {
if (object instanceof Permanent) { if (object instanceof Permanent) {
boolean controlled = ((Permanent) object).getControllerId().equals(createdForPlayerId); boolean controlled = ((Permanent) object).getControllerId().equals(createdForPlayerId);
stack.put(stackObject.getId(), new StackAbilityView(game, (StackAbility) stackObject, object.getName(), object, new CardView(((Permanent) object), game, controlled, false, false))); stack.put(stackObject.getId(), new StackAbilityView(game, (StackAbility) stackObject, object.getName(), object, new CardView(((Permanent) object), game, controlled, false)));
} else { } else {
stack.put(stackObject.getId(), new StackAbilityView(game, (StackAbility) stackObject, card.getName(), card, new CardView(card, game, false, false, false))); stack.put(stackObject.getId(), new StackAbilityView(game, (StackAbility) stackObject, card.getName(), card, new CardView(card, game, false, false)));
} }
} else { } else {
stack.put(stackObject.getId(), new StackAbilityView(game, (StackAbility) stackObject, "", card, new CardView(card, game))); stack.put(stackObject.getId(), new StackAbilityView(game, (StackAbility) stackObject, "", card, new CardView(card, game)));
@ -132,9 +134,9 @@ public class GameView implements Serializable {
} else if (object instanceof Designation) { } else if (object instanceof Designation) {
Designation designation = (Designation) game.getObject(object.getId()); Designation designation = (Designation) game.getObject(object.getId());
if (designation != null) { if (designation != null) {
stack.put(stackObject.getId(), new StackAbilityView(game, (StackAbility) stackObject, designation.getName(), designation, new CardView(designation, game))); stack.put(stackObject.getId(), new StackAbilityView(game, (StackAbility) stackObject, designation.getName(), designation, new CardView(designation, (StackAbility) stackObject)));
} else { } else {
LOGGER.fatal("Designation object not found: " + object.getName() + ' ' + object.toString() + ' ' + object.getClass().toString()); throw new IllegalArgumentException("Designation object not found: " + object + " - " + object.getClass().toString());
} }
} else if (object instanceof StackAbility) { } else if (object instanceof StackAbility) {
StackAbility stackAbility = ((StackAbility) object); StackAbility stackAbility = ((StackAbility) object);
@ -142,20 +144,19 @@ public class GameView implements Serializable {
stack.put(stackObject.getId(), new CardView(stackObject, game)); stack.put(stackObject.getId(), new CardView(stackObject, game));
checkPaid(stackObject.getId(), ((StackAbility) stackObject)); checkPaid(stackObject.getId(), ((StackAbility) stackObject));
} else { } else {
LOGGER.fatal("Object can't be cast to StackAbility: " + object.getName() + ' ' + object.toString() + ' ' + object.getClass().toString()); throw new IllegalArgumentException("Object can't be cast to StackAbility: " + object + " - " + object.getClass().toString());
} }
} else { } else {
// can happen if a player times out while ability is on the stack // can happen if a player times out while ability is on the stack
LOGGER.debug("Stack Object for stack ability not found: " + stackObject.getStackAbility().getRule()); LOGGER.debug("Stack Object for stack ability not found: " + stackObject.getStackAbility().getRule());
} }
} else if (stackObject != null) { } else if (stackObject != null) {
LOGGER.fatal("Unknown type of StackObject: " + stackObject.getName() + ' ' + stackObject.toString() + ' ' + stackObject.getClass().toString()); throw new IllegalArgumentException("Unknown type of StackObject: " + stackObject + " - " + stackObject.getClass().toString());
} }
//stackOrder.add(stackObject.getId());
} }
//Collections.reverse(stackOrder);
for (ExileZone exileZone : state.getExile().getExileZones()) { for (ExileZone exileZone : state.getExile().getExileZones()) {
exiles.add(new ExileView(exileZone, game)); exiles.add(new ExileView(exileZone, game, createdForPlayerId));
} }
for (String name : state.getRevealed().keySet()) { for (String name : state.getRevealed().keySet()) {
revealed.add(new RevealedView(name, state.getRevealed().get(name), game)); revealed.add(new RevealedView(name, state.getRevealed().get(name), game));

View file

@ -1,17 +1,15 @@
package mage.view; package mage.view;
import mage.abilities.Ability;
import mage.abilities.common.TurnFaceUpAbility;
import mage.cards.Card; import mage.cards.Card;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentToken; import mage.game.permanent.PermanentToken;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
@ -39,9 +37,8 @@ public class PermanentView extends CardView {
private final boolean attachedControllerDiffers; private final boolean attachedControllerDiffers;
public PermanentView(Permanent permanent, Card card, UUID createdForPlayerId, Game game) { public PermanentView(Permanent permanent, Card card, UUID createdForPlayerId, Game game) {
super(permanent, game, permanent.getControllerId() != null && permanent.getControllerId().equals(createdForPlayerId)); super(permanent, game, CardUtil.canShowAsControlled(permanent, createdForPlayerId));
this.controlled = permanent.getControllerId() != null && permanent.getControllerId().equals(createdForPlayerId); this.controlled = permanent.getControllerId() != null && permanent.getControllerId().equals(createdForPlayerId);
this.rules = permanent.getRules(game);
this.tapped = permanent.isTapped(); this.tapped = permanent.isTapped();
this.flipped = permanent.isFlipped(); this.flipped = permanent.isFlipped();
this.phasedIn = permanent.isPhasedIn(); this.phasedIn = permanent.isPhasedIn();
@ -52,26 +49,26 @@ public class PermanentView extends CardView {
this.attachments = new ArrayList<>(permanent.getAttachments()); this.attachments = new ArrayList<>(permanent.getAttachments());
this.attachedTo = permanent.getAttachedTo(); this.attachedTo = permanent.getAttachedTo();
// show face down cards to all players at the game end // store original card, e.g. for sides switch in GUI
boolean showFaceDownInfo = controlled || (game != null && game.hasEnded());
if (isToken()) { if (isToken()) {
original = new CardView(((PermanentToken) permanent).getToken().copy(), (Game) null); original = new CardView(((PermanentToken) permanent).getToken().copy(), (Game) null);
original.expansionSetCode = permanent.getExpansionSetCode(); original.expansionSetCode = permanent.getExpansionSetCode(); // TODO: miss card number and other?
expansionSetCode = permanent.getExpansionSetCode(); expansionSetCode = permanent.getExpansionSetCode();
} else { } else {
if (card != null && showFaceDownInfo) {
// face down card must be hidden from opponent, but shown on game end for all // face down card must be hidden from opponent, but shown on game end for all
boolean showFaceDownInfo = controlled || (game != null && game.hasEnded());
if (card != null && showFaceDownInfo) {
original = new CardView(card.copy(), (Game) null); original = new CardView(card.copy(), (Game) null);
} else { } else {
original = null; original = null;
} }
} }
this.transformed = permanent.isTransformed(); //this.transformed = permanent.isTransformed();
this.copy = permanent.isCopy(); this.copy = permanent.isCopy();
// for fipped, transformed or copied cards, switch the names // for fipped, transformed or copied cards, switch the names
if (original != null && !original.getName().equals(this.getName())) { if (original != null && !original.getName().equals(this.getName())) {
// TODO: wtf, why copy check here?! Need research
if (permanent.isCopy() && permanent.isFlipCard()) { if (permanent.isCopy() && permanent.isFlipCard()) {
this.alternateName = permanent.getFlipCardName(); this.alternateName = permanent.getFlipCardName();
} else { } else {
@ -98,31 +95,16 @@ public class PermanentView extends CardView {
} }
this.nameController = nameController; this.nameController = nameController;
// add info for face down permanents // add additional info for face down permanents
if (permanent.isFaceDown(game) && card != null) { if (permanent.isFaceDown(game)) {
if (showFaceDownInfo) { //if (permanent.isManifested()) {
// must be a morphed or manifested card // this.rules.add("A manifested creature card can be turned face up any time for it's mana cost."
for (Ability permanentAbility : permanent.getAbilities(game)) { // + " A face-down card can also be turned face up for its morph cost.");
if (permanentAbility.getWorksFaceDown()) { //} else if (permanent.isMorphed()) {
this.rules.add(permanentAbility.getRule(true)); // this.rules.add("If the controller has priority, they may turn this permanent face up."
} else if (permanentAbility instanceof TurnFaceUpAbility && !permanentAbility.getRuleVisible()) { // + " This is a special action; it doesn't use the stack. To do this they pay the morph costs,"
this.rules.add(permanentAbility.getRule()); // + " then turns this permanent face up.");
} //}
}
this.name = card.getName();
this.displayName = card.getName();
this.expansionSetCode = card.getExpansionSetCode();
this.cardNumber = card.getCardNumber();
} else {
if (permanent.isManifested()) {
this.rules.add("A manifested creature card can be turned face up any time for it's mana cost."
+ " A face-down card can also be turned face up for its morph cost.");
} else if (permanent.isMorphed()) {
this.rules.add("If the controller has priority, they may turn this permanent face up."
+ " This is a special action; it doesn't use the stack. To do this they pay the morph costs,"
+ " then turns this permanent face up.");
}
}
} }
// determines if shown in it's own column // determines if shown in it's own column

View file

@ -1,6 +1,5 @@
package mage.view; package mage.view;
import mage.cards.Card;
import mage.game.command.Plane; import mage.game.command.Plane;
import mage.players.PlayableObjectStats; import mage.players.PlayableObjectStats;
@ -15,15 +14,17 @@ public class PlaneView implements CommandObjectView, Serializable {
protected UUID id; protected UUID id;
protected String name; protected String name;
protected int imageNum; protected String imageFileName = "";
protected String expansionSetCode; protected int imageNumber = 0;
protected String expansionSetCode = "";
protected List<String> rules; protected List<String> rules;
protected PlayableObjectStats playableStats = new PlayableObjectStats(); protected PlayableObjectStats playableStats = new PlayableObjectStats();
public PlaneView(Plane plane) { public PlaneView(Plane plane) {
this.id = plane.getId(); this.id = plane.getId();
this.name = plane.getName(); this.name = plane.getName();
this.imageNum = plane.getImageNumber(); this.imageFileName = plane.getImageFileName();
this.imageNumber = plane.getImageNumber();
this.expansionSetCode = plane.getExpansionSetCode(); this.expansionSetCode = plane.getExpansionSetCode();
this.rules = plane.getAbilities().getRules(plane.getName()); this.rules = plane.getAbilities().getRules(plane.getName());
} }
@ -43,9 +44,14 @@ public class PlaneView implements CommandObjectView, Serializable {
return id; return id;
} }
@Override
public String getImageFileName() {
return imageFileName;
}
@Override @Override
public int getImageNumber() { public int getImageNumber() {
return imageNum; return imageNumber;
} }
@Override @Override

View file

@ -10,6 +10,7 @@ import mage.game.command.*;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import mage.players.net.UserData; import mage.players.net.UserData;
import mage.util.CardUtil;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.*;
@ -82,19 +83,19 @@ public class PlayerView implements Serializable {
this.hasLeft = player.hasLeft(); this.hasLeft = player.hasLeft();
for (Card card : player.getGraveyard().getCards(game)) { for (Card card : player.getGraveyard().getCards(game)) {
graveyard.put(card.getId(), new CardView(card, game, false)); graveyard.put(card.getId(), new CardView(card, game, CardUtil.canShowAsControlled(card, createdForPlayerId)));
} }
for (ExileZone exileZone : game.getExile().getExileZones()) { for (ExileZone exileZone : game.getExile().getExileZones()) {
for (Card card : exileZone.getCards(game)) { for (Card card : exileZone.getCards(game)) {
if (player.getId().equals(card.getOwnerId())) { if (player.getId().equals(card.getOwnerId())) {
exile.put(card.getId(), new CardView(card, game, false)); // unnown if it's allowed to look under a face down card exile.put(card.getId(), new CardView(card, game, CardUtil.canShowAsControlled(card, createdForPlayerId)));
} }
} }
} }
if (this.controlled || !player.isHuman()) { if (this.controlled || !player.isHuman()) {
// sideboard available for itself or for computer only // sideboard available for itself or for computer only
for (Card card : player.getSideboard().getCards(game)) { for (Card card : player.getSideboard().getCards(game)) {
sideboard.put(card.getId(), new CardView(card, game, false)); sideboard.put(card.getId(), new CardView(card, game, CardUtil.canShowAsControlled(card, createdForPlayerId)));
} }
} }
@ -137,7 +138,7 @@ public class PlayerView implements Serializable {
if (commander.getControllerId().equals(this.playerId)) { if (commander.getControllerId().equals(this.playerId)) {
Card sourceCard = game.getCard(commander.getSourceId()); Card sourceCard = game.getCard(commander.getSourceId());
if (sourceCard != null) { if (sourceCard != null) {
commandList.add(new CommanderView(commander, sourceCard, game)); commandList.add(new CommanderView(commander, sourceCard, game, createdForPlayerId));
} }
} }
} }

View file

@ -19,7 +19,7 @@ public class RevealedView implements Serializable {
public RevealedView(String name, Cards cards, Game game) { public RevealedView(String name, Cards cards, Game game) {
this.name = name; this.name = name;
for (Card card : cards.getCards(game)) { for (Card card : cards.getCards(game)) {
this.cards.put(card.getId(), new CardView(card, game, card.getId())); this.cards.put(card.getId(), new CardView(card, game));
} }
} }

View file

@ -85,6 +85,7 @@ public final class ArtificialScoringSystem {
MageObject object = game.getObject(uuid); MageObject object = game.getObject(uuid);
if (object instanceof Card) { if (object instanceof Card) {
Card card = (Card) object; Card card = (Card) object;
// TODO: implement getOutcomeTotal for permanents and cards too (not only attachments)
int outcomeScore = card.getAbilities(game).getOutcomeTotal(); int outcomeScore = card.getAbilities(game).getOutcomeTotal();
if (card.getCardType(game).contains(CardType.ENCHANTMENT)) { if (card.getCardType(game).contains(CardType.ENCHANTMENT)) {
enchantments = enchantments + outcomeScore * 100; enchantments = enchantments + outcomeScore * 100;

View file

@ -822,7 +822,7 @@ public class GameController implements GameCallback {
} }
private synchronized void choosePile(UUID playerId, final String message, final List<? extends Card> pile1, final List<? extends Card> pile2) throws MageException { private synchronized void choosePile(UUID playerId, final String message, final List<? extends Card> pile1, final List<? extends Card> pile2) throws MageException {
perform(playerId, playerId1 -> getGameSession(playerId1).choosePile(message, new CardsView(game, pile1), new CardsView(game, pile2))); perform(playerId, playerId1 -> getGameSession(playerId1).choosePile(message, new CardsView(game, pile1, playerId), new CardsView(game, pile2, playerId)));
} }
private synchronized void chooseMode(UUID playerId, final Map<UUID, String> modes, final String message) throws MageException { private synchronized void chooseMode(UUID playerId, final Map<UUID, String> modes, final String message) throws MageException {
@ -836,12 +836,7 @@ public class GameController implements GameCallback {
private synchronized void target(UUID playerId, final String question, final Cards cards, final List<Permanent> perms, final Set<UUID> targets, final boolean required, final Map<String, Serializable> options) throws MageException { private synchronized void target(UUID playerId, final String question, final Cards cards, final List<Permanent> perms, final Set<UUID> targets, final boolean required, final Map<String, Serializable> options) throws MageException {
perform(playerId, playerId1 -> { perform(playerId, playerId1 -> {
if (cards != null) { if (cards != null) {
// Zone targetZone = (Zone) options.get("targetZone"); getGameSession(playerId1).target(question, new CardsView(game, cards.getCards(game), playerId, true), targets, required, options);
// Are there really situations where a player selects from a list of face down cards?
// So always show face up for selection
// boolean showFaceDown = targetZone != null && targetZone.equals(Zone.PICK);
boolean showFaceDown = true;
getGameSession(playerId1).target(question, new CardsView(game, cards.getCards(game), showFaceDown, true), targets, required, options);
} else if (perms != null) { } else if (perms != null) {
CardsView permsView = new CardsView(); CardsView permsView = new CardsView();
for (Permanent perm : perms) { for (Permanent perm : perms) {

View file

@ -29,8 +29,6 @@ public final class AerieBowmasters extends CardImpl {
// Megamorph {5}{G} <i>(You may cast this card face down as a 2/2 creature for {3}. Turn it face up at any time for its megamorph cost and put a +1/+1 counter on it.)</i>) // Megamorph {5}{G} <i>(You may cast this card face down as a 2/2 creature for {3}. Turn it face up at any time for its megamorph cost and put a +1/+1 counter on it.)</i>)
this.addAbility(new MorphAbility(this, new ManaCostsImpl<>("{5}{G}"), true)); this.addAbility(new MorphAbility(this, new ManaCostsImpl<>("{5}{G}"), true));
} }
private AerieBowmasters(final AerieBowmasters card) { private AerieBowmasters(final AerieBowmasters card) {

View file

@ -64,7 +64,7 @@ public final class BaneAlleyBroker extends CardImpl {
this.power = new MageInt(0); this.power = new MageInt(0);
this.toughness = new MageInt(3); this.toughness = new MageInt(3);
// {tap}: Draw a card, then exile a card from your hand face down. // {T}: Draw a card, then exile a card from your hand face down.
this.addAbility(new SimpleActivatedAbility(new BaneAlleyBrokerDrawExileEffect(), new TapSourceCost())); this.addAbility(new SimpleActivatedAbility(new BaneAlleyBrokerDrawExileEffect(), new TapSourceCost()));
// You may look at cards exiled with Bane Alley Broker. // You may look at cards exiled with Bane Alley Broker.

View file

@ -103,9 +103,20 @@ class TurnOverEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Permanent creature = game.getPermanent(source.getFirstTarget()); Permanent creature = game.getPermanent(source.getFirstTarget());
if (creature != null) { if (creature != null) {
// To turn over a creature means to physically turn the card over. If the result is a face-down card, its
// a colorless 2/2 creature, the same as one would get from using the morph ability. Turning a face-down
// card over results in it being turned face up. Any abilities that trigger when its turned face up will
// work. Turning a double-faced card over is the same as transforming it. Any abilities that trigger when
// you transform it will work. Turning a combined host/augment creature over will result in a big colorless
// 2/2 creature represented by two cards. Turning a melded creature over will result in the two cards
// breaking apart and forming two separate creatures, but theyll probably just get right back together.
// Turning B.F.M. (Big Furry Monster) over is the same as turning a combined creature over.
// (2018-01-19)
if (creature.isFaceDown(game)) { if (creature.isFaceDown(game)) {
// face down -> face up
creature.turnFaceUp(source, game, source.getControllerId()); creature.turnFaceUp(source, game, source.getControllerId());
} else { } else {
// face up -> face down without face up ability
creature.turnFaceDown(source, game, source.getControllerId()); creature.turnFaceDown(source, game, source.getControllerId());
MageObjectReference objectReference = new MageObjectReference(creature.getId(), creature.getZoneChangeCounter(game), game); MageObjectReference objectReference = new MageObjectReference(creature.getId(), creature.getZoneChangeCounter(game), game);
game.addEffect(new BecomesFaceDownCreatureEffect(null, objectReference, Duration.Custom, FaceDownType.MANUAL), source); game.addEffect(new BecomesFaceDownCreatureEffect(null, objectReference, Duration.Custom, FaceDownType.MANUAL), source);

View file

@ -22,8 +22,8 @@ public class BloodMoonTest extends CardTestPlayerBase {
// which replacement effects apply and how they apply, check the characteristics of the permanent as it // which replacement effects apply and how they apply, check the characteristics of the permanent as it
// would exist on the battlefield, taking into account replacement effects that have already modified how // would exist on the battlefield, taking into account replacement effects that have already modified how
// it enters the battlefield (see rule 616.1), continuous effects generated by the resolution of spells // it enters the battlefield (see rule 616.1), continuous effects generated by the resolution of spells
// or abilities that changed the permanent’s characteristics on the stack (see rule 400.7a), and continuous // or abilities that changed the permanent's characteristics on the stack (see rule 400.7a), and continuous
// effects from the permanent’s own static abilities, but ignoring continuous effects from any other source // effects from the permanent's own static abilities, but ignoring continuous effects from any other source
// that would affect it. // that would affect it.
// Grassland has to enter the battlefield tapped, because // Grassland has to enter the battlefield tapped, because
// the Blood Moon does not prevent ETB Replacement Effects // the Blood Moon does not prevent ETB Replacement Effects

View file

@ -1,11 +1,14 @@
package org.mage.test.cards.abilities.keywords; package org.mage.test.cards.abilities.keywords;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.repository.TokenRepository;
import mage.constants.EmptyNames; import mage.constants.EmptyNames;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.util.CardUtil;
import mage.view.CardView; import mage.view.CardView;
import mage.view.GameView; import mage.view.GameView;
import mage.view.PermanentView; import mage.view.PermanentView;
@ -535,9 +538,14 @@ public class ManifestTest extends CardTestPlayerBase {
Permanent perm = game.getBattlefield().getAllPermanents() Permanent perm = game.getBattlefield().getAllPermanents()
.stream() .stream()
.filter(permanent -> permanent.isFaceDown(game)) .filter(permanent -> permanent.isFaceDown(game))
.filter(permanent -> {
Assert.assertEquals("face down permanent must have not name", "", permanent.getName());
return true;
})
.findFirst() .findFirst()
.orElse(null); .orElse(null);
Assert.assertNotNull(perm); Assert.assertNotNull(perm);
Assert.assertEquals("server side face down permanent must have empty name", EmptyNames.FACE_DOWN_CREATURE.toString(), perm.getName());
GameView gameView = new GameView(game.getState(), game, viewFromPlayer.getId(), null); GameView gameView = new GameView(game.getState(), game, viewFromPlayer.getId(), null);
PlayerView playerView = gameView.getPlayers() PlayerView playerView = gameView.getPlayers()
.stream() .stream()
@ -548,29 +556,34 @@ public class ManifestTest extends CardTestPlayerBase {
PermanentView permanentView = playerView.getBattlefield().values() PermanentView permanentView = playerView.getBattlefield().values()
.stream() .stream()
.filter(CardView::isFaceDown) .filter(CardView::isFaceDown)
.filter(p -> {
CardView debugView = new CardView((PermanentCard) currentGame.getPermanent(p.getId()), currentGame, false, false);
Assert.assertNotEquals("face down view must have name", "", p.getName());
return true;
})
.findFirst() .findFirst()
.orElse(null); .orElse(null);
Assert.assertNotNull(permanentView); Assert.assertNotNull(permanentView);
return permanentView; return permanentView;
} }
private void assertFaceDown(String info, PermanentView faceDownPermanent, String realPermanentName, boolean realInfoMustBeVisible) { private void assertFaceDown(String checkInfo, PermanentView faceDownPermanentView, String needRealName, String needFaceDownStatus, boolean needShowRealInfo) {
if (realInfoMustBeVisible) { String info = checkInfo + " - " + faceDownPermanentView;
// show all info String needName = CardUtil.getCardNameForGUI(needShowRealInfo ? needRealName : "", needFaceDownStatus);
Assert.assertEquals(realPermanentName, faceDownPermanent.getName()); // show real name
Assert.assertEquals("2", faceDownPermanent.getPower());
Assert.assertEquals("2", faceDownPermanent.getToughness());
//
Assert.assertNotNull(faceDownPermanent.getOriginal());
Assert.assertEquals(realPermanentName, faceDownPermanent.getOriginal().getName());
} else {
// hide original info
Assert.assertEquals(info, "", faceDownPermanent.getName());
Assert.assertEquals(info, "2", faceDownPermanent.getPower());
Assert.assertEquals(info, "2", faceDownPermanent.getToughness());
Assert.assertNull(info, faceDownPermanent.getOriginal());
}
// check view
Assert.assertTrue(info + " - wrong face down status", faceDownPermanentView.isFaceDown());
Assert.assertEquals(info + " - wrong name", needName, faceDownPermanentView.getName()); // show real name
Assert.assertEquals(info + " - wrong power", "2", faceDownPermanentView.getPower());
Assert.assertEquals(info + " - wrong toughness", "2", faceDownPermanentView.getToughness());
// check original info
if (needShowRealInfo) {
Assert.assertNotNull(info + " - miss original card data", faceDownPermanentView.getOriginal());
Assert.assertEquals(info + " - wrong original card name", needRealName, faceDownPermanentView.getOriginal().getName());
} else {
Assert.assertNull(info + " - original data must be hidden", faceDownPermanentView.getOriginal());
}
} }
@Test @Test
@ -587,27 +600,26 @@ public class ManifestTest extends CardTestPlayerBase {
runCode("on active game", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { runCode("on active game", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
// hide from opponent // hide from opponent
PermanentView permanent = findFaceDownPermanent(game, playerA, playerB); PermanentView permanent = findFaceDownPermanent(game, playerA, playerB);
assertFaceDown("in game: must hide from opponent", permanent, "Mountain", false); assertFaceDown("in game: must hide from opponent", permanent, "Mountain", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, false);
// show for yourself // show for yourself
permanent = findFaceDownPermanent(game, playerB, playerB); permanent = findFaceDownPermanent(game, playerB, playerB);
assertFaceDown("in game: must show for yourself", permanent, "Mountain", true); assertFaceDown("in game: must show for yourself", permanent, "Mountain", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, true);
}); });
setStrictChooseMode(true); setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
// workaround to force end game (can't use other test commands after that) // workaround to force end game (can't use other test commands after that)
playerA.won(currentGame); playerA.won(currentGame);
Assert.assertTrue(currentGame.hasEnded()); Assert.assertTrue(currentGame.hasEnded());
// show all after game end // show all after game end
PermanentView permanent = findFaceDownPermanent(currentGame, playerA, playerB); PermanentView permanent = findFaceDownPermanent(currentGame, playerA, playerB);
assertFaceDown("end game: must show for opponent", permanent, "Mountain", true); assertFaceDown("end game: must show for opponent", permanent, "Mountain", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, true);
// //
permanent = findFaceDownPermanent(currentGame, playerB, playerB); permanent = findFaceDownPermanent(currentGame, playerB, playerB);
assertFaceDown("end game: must show for yourself", permanent, "Mountain", true); assertFaceDown("end game: must show for yourself", permanent, "Mountain", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, true);
} }
} }

View file

@ -21,7 +21,7 @@ public class MegamorphTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Aerie Bowmasters", 1); addCard(Zone.HAND, playerA, "Aerie Bowmasters", 1);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6); addCard(Zone.BATTLEFIELD, playerA, "Forest", 6);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aerie Bowmasters using Morph"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aerie Bowmasters using Megamorph");
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}: Turn"); activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}: Turn");

View file

@ -521,14 +521,10 @@ public class MorphTest extends CardTestPlayerBase {
setStrictChooseMode(true); setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akroma, Angel of Fury using Morph"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akroma, Angel of Fury using Morph");
// showBattlefield("A battle", 1, PhaseStep.POSTCOMBAT_MAIN, playerA);
// showBattlefield("B battle", 1, PhaseStep.POSTCOMBAT_MAIN, playerB);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Supplant Form"); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Supplant Form");
addTarget(playerB, EmptyNames.FACE_DOWN_CREATURE.toString()); addTarget(playerB, EmptyNames.FACE_DOWN_CREATURE.toString());
// showBattlefield("A battle end", 1, PhaseStep.END_TURN, playerA);
// showBattlefield("B battle end", 1, PhaseStep.END_TURN, playerB);
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
@ -1103,10 +1099,17 @@ public class MorphTest extends CardTestPlayerBase {
@Test @Test
public void test_MorphIsColorlessFlash() { public void test_MorphIsColorlessFlash() {
// creature
// Morph {4}{G}
addCard(Zone.HAND, playerA, "Pine Walker", 1); addCard(Zone.HAND, playerA, "Pine Walker", 1);
// land
// Morph {2}
addCard(Zone.HAND, playerA, "Zoetic Cavern", 1); addCard(Zone.HAND, playerA, "Zoetic Cavern", 1);
addCard(Zone.BATTLEFIELD, playerA, "Liberator, Urza's Battlethopter", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 6); addCard(Zone.BATTLEFIELD, playerA, "Island", 6);
//
// You may cast colorless spells and artifact spells as though they had flash.
addCard(Zone.BATTLEFIELD, playerA, "Liberator, Urza's Battlethopter", 1);
castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Pine Walker using Morph"); castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Pine Walker using Morph");
castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Zoetic Cavern using Morph"); castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Zoetic Cavern using Morph");

View file

@ -23,9 +23,11 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
*/ */
public class PrototypeTest extends CardTestPlayerBase { public class PrototypeTest extends CardTestPlayerBase {
// Prototype {2}{R} - 3/2
private static final String automaton = "Blitz Automaton"; private static final String automaton = "Blitz Automaton";
private static final String withPrototype = " using Prototype"; private static final String withPrototype = " using Prototype";
private static final String automatonWithPrototype = automaton+withPrototype; private static final String automatonWithPrototype = automaton+withPrototype;
private static final String bolt = "Lightning Bolt"; private static final String bolt = "Lightning Bolt";
private static final String cloudshift = "Cloudshift"; private static final String cloudshift = "Cloudshift";
private static final String clone = "Clone"; private static final String clone = "Clone";
@ -89,6 +91,7 @@ public class PrototypeTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
addCard(Zone.HAND, playerA, automaton); addCard(Zone.HAND, playerA, automaton);
showAvailableAbilities("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);

View file

@ -2,6 +2,7 @@ package org.mage.test.cards.cost.sacrifice;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.util.RandomUtil;
import org.junit.Test; import org.junit.Test;
import org.mage.test.sba.PlaneswalkerRuleTest; import org.mage.test.sba.PlaneswalkerRuleTest;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;
@ -22,16 +23,15 @@ public class SacrificeLandTest extends CardTestPlayerBase {
*/ */
@Test @Test
public void testRollback() { public void testRollback() {
// If Soldevi Excavations would enter the battlefield, sacrifice an untapped Island instead. // If Soldevi Excavations entered the battlefield, sacrifice an untapped Island instead.
// If you do, put Soldevi Excavations onto the battlefield. If you don't, put it into its owner's graveyard. // If you do, put Soldevi Excavations onto the battlefield. If you don't, put it into its owner's graveyard.
String soldeviExcavations = "Soldevi Excavations"; String soldeviExcavations = "Soldevi Excavations";
addCard(Zone.HAND, playerA, soldeviExcavations); addCard(Zone.HAND, playerA, soldeviExcavations);
addCard(Zone.BATTLEFIELD, playerA, "Island"); addCard(Zone.BATTLEFIELD, playerA, "Island");
Random random = new Random(); boolean sacFirstLand = RandomUtil.nextBoolean();
boolean sacFirstLand = random.nextBoolean(); boolean sacSecondLand = RandomUtil.nextBoolean();
boolean sacSecondLand = random.nextBoolean();
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, soldeviExcavations); playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, soldeviExcavations);
setChoice(playerA, sacFirstLand); setChoice(playerA, sacFirstLand);

View file

@ -2,21 +2,26 @@ package org.mage.test.serverside;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectImpl; import mage.MageObjectImpl;
import mage.ObjectColor;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.CreateTokenEffect;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.repository.TokenRepository; import mage.cards.repository.TokenRepository;
import mage.constants.EmptyNames;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentToken; import mage.game.permanent.PermanentToken;
import mage.game.permanent.token.HumanToken; import mage.game.permanent.token.HumanToken;
import mage.game.permanent.token.SoldierToken; import mage.game.permanent.token.SoldierToken;
import mage.game.permanent.token.Token; import mage.game.permanent.token.Token;
import mage.game.permanent.token.TokenImpl; import mage.game.permanent.token.TokenImpl;
import mage.game.permanent.token.custom.CreatureToken; import mage.game.permanent.token.custom.CreatureToken;
import mage.game.stack.Spell;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.view.CardView; import mage.view.CardView;
import mage.view.GameView; import mage.view.GameView;
@ -261,13 +266,13 @@ public class TokenImagesTest extends CardTestPlayerBase {
} }
} }
private void assert_TokenImageNumber(String tokenName, List<Integer> needUniqueImages) { private void assert_TokenOrCardImageNumber(String tokenOrCardName, List<Integer> needUniqueImages) {
Set<Integer> serverStats = currentGame.getBattlefield().getAllPermanents() Set<Integer> serverStats = currentGame.getBattlefield().getAllPermanents()
.stream() .stream()
.filter(card -> card.getName().equals(tokenName)) .filter(card -> card.getName().equals(tokenOrCardName))
.filter(card -> card instanceof PermanentToken) .filter(card -> card instanceof MageObjectImpl)
.sorted(Comparator.comparing(Card::getExpansionSetCode)) .sorted(Comparator.comparing(Card::getExpansionSetCode))
.map(card -> (PermanentToken) card) .map(card -> (MageObjectImpl) card)
.map(MageObjectImpl::getImageNumber) .map(MageObjectImpl::getImageNumber)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -280,7 +285,7 @@ public class TokenImagesTest extends CardTestPlayerBase {
Assert.assertNotNull(playerView); Assert.assertNotNull(playerView);
Set<Integer> clientStats = playerView.getBattlefield().values() Set<Integer> clientStats = playerView.getBattlefield().values()
.stream() .stream()
.filter(card -> card.getName().equals(tokenName)) .filter(card -> card.getName().equals(tokenOrCardName))
.sorted(Comparator.comparing(CardView::getExpansionSetCode)) .sorted(Comparator.comparing(CardView::getExpansionSetCode))
.map(CardView::getImageNumber) .map(CardView::getImageNumber)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -289,8 +294,79 @@ public class TokenImagesTest extends CardTestPlayerBase {
String imagesNeed = needUniqueImages.stream().sorted().map(Object::toString).collect(Collectors.joining(", ")); String imagesNeed = needUniqueImages.stream().sorted().map(Object::toString).collect(Collectors.joining(", "));
String imagesServer = serverStats.stream().sorted().map(Object::toString).collect(Collectors.joining(", ")); String imagesServer = serverStats.stream().sorted().map(Object::toString).collect(Collectors.joining(", "));
String imagesClient = clientStats.stream().sorted().map(Object::toString).collect(Collectors.joining(", ")); String imagesClient = clientStats.stream().sorted().map(Object::toString).collect(Collectors.joining(", "));
Assert.assertEquals(imagesNeed, imagesServer); Assert.assertEquals("server side", imagesNeed, imagesServer);
Assert.assertEquals(imagesNeed, imagesClient); Assert.assertEquals("client side", imagesNeed, imagesClient);
}
private void assertFaceDownCharacteristics(String info, MageObject object, String faceDownTypeName) {
String prefix = info + " - " + object;
// image info
Assert.assertEquals(prefix + " - wrong set code", TokenRepository.XMAGE_TOKENS_SET_CODE, object.getExpansionSetCode());
Assert.assertEquals(prefix + " - wrong card number", "0", object.getCardNumber());
Assert.assertEquals(prefix + " - wrong image file name", faceDownTypeName, object.getImageFileName());
Assert.assertNotEquals(prefix + " - wrong image number", Integer.valueOf(0), object.getImageNumber());
// characteristic checks instead new test
Assert.assertEquals(prefix + " - wrong name", EmptyNames.FACE_DOWN_CREATURE.toString(), object.getName());
Assert.assertEquals(prefix + " - wrong power", 2, object.getPower().getValue());
Assert.assertEquals(prefix + " - wrong toughness", 2, object.getToughness().getValue());
Assert.assertEquals(prefix + " - wrong color", "", object.getColor(currentGame).toString());
Assert.assertEquals(prefix + " - wrong supertypes", "[]", object.getSuperType(currentGame).toString());
Assert.assertEquals(prefix + " - wrong types", "[Creature]", object.getCardType(currentGame).toString());
Assert.assertEquals(prefix + " - wrong subtypes", "[]", object.getSubtype(currentGame).toString());
Assert.assertEquals(prefix + " - wrong abilities", 2, object.getAbilities().size()); // become face down + face up abilities only
}
private void assertOriginalData(String info, CardView cardView, int needPower, int needToughness, String needColor) {
String prefix = info + " - " + cardView;
int currentPower = cardView.getOriginalPower() == null ? 0 : cardView.getOriginalPower().getValue();
int currentToughness = cardView.getOriginalToughness() == null ? 0 : cardView.getOriginalToughness().getValue();
Assert.assertEquals(prefix + " - wrong power", needPower, currentPower);
Assert.assertEquals(prefix + " - wrong toughness", needToughness, currentToughness);
if (needColor != null) {
Assert.assertEquals(prefix + " - wrong color", needColor, cardView.getOriginalColorIdentity());
}
}
private void assert_FaceDownMorphImageNumber(List<Integer> needUniqueImages) {
Set<Integer> serverStats = currentGame.getBattlefield().getAllPermanents()
.stream()
.filter(card -> card.isFaceDown(currentGame))
.filter(card -> {
Assert.assertEquals("server side - wrong set code - " + card, TokenRepository.XMAGE_TOKENS_SET_CODE, card.getExpansionSetCode());
return true;
})
.sorted(Comparator.comparing(Card::getExpansionSetCode))
.map(card -> (MageObjectImpl) card)
.map(MageObjectImpl::getImageNumber)
.collect(Collectors.toSet());
// use another player to hide card view names in face down
GameView gameView = new GameView(currentGame.getState(), currentGame, playerB.getId(), null);
PlayerView playerView = gameView.getPlayers()
.stream()
.filter(p -> p.getName().equals(playerA.getName()))
.findFirst()
.orElse(null);
Assert.assertNotNull(playerView);
Set<Integer> clientStats = playerView.getBattlefield().values()
.stream()
.filter(CardView::isFaceDown)
.filter(card -> {
Assert.assertEquals("client side - wrong set code - " + card, TokenRepository.XMAGE_TOKENS_SET_CODE, card.getExpansionSetCode());
return true;
})
.sorted(Comparator.comparing(CardView::getExpansionSetCode))
.map(CardView::getImageNumber)
.collect(Collectors.toSet());
// server and client sides must have same data
String imagesNeed = needUniqueImages.stream().sorted().map(Object::toString).collect(Collectors.joining(", "));
String imagesServer = serverStats.stream().sorted().map(Object::toString).collect(Collectors.joining(", "));
String imagesClient = clientStats.stream().sorted().map(Object::toString).collect(Collectors.joining(", "));
Assert.assertEquals("server side", imagesNeed, imagesServer);
Assert.assertEquals("client side", imagesNeed, imagesClient);
} }
@Test @Test
@ -317,7 +393,7 @@ public class TokenImagesTest extends CardTestPlayerBase {
// x2 tokens // x2 tokens
assert_MemorialToGlory(20, "40K=40"); assert_MemorialToGlory(20, "40K=40");
assert_TokenImageNumber("Soldier Token", Arrays.asList(1, 2, 3)); // 40K set contains 3 diffrent soldiers assert_TokenOrCardImageNumber("Soldier Token", Arrays.asList(1, 2, 3)); // 40K set contains 3 diffrent soldiers
} }
@Test @Test
@ -381,7 +457,7 @@ public class TokenImagesTest extends CardTestPlayerBase {
execute(); execute();
assertPermanentCount(playerA, 1 + 10); // 1 test card + 10 tokens assertPermanentCount(playerA, 1 + 10); // 1 test card + 10 tokens
assert_TokenImageNumber("Soldier Token", Arrays.asList(realImageNumber.get())); // one ability's call must generate tokens with same image assert_TokenOrCardImageNumber("Soldier Token", Arrays.asList(realImageNumber.get())); // one ability's call must generate tokens with same image
assert_Inner("test", 0, 0, 1, assert_Inner("test", 0, 0, 1,
"Soldier Token", 10, false, "40K=10"); "Soldier Token", 10, false, "40K=10");
} }
@ -479,7 +555,7 @@ public class TokenImagesTest extends CardTestPlayerBase {
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
assert_TokenImageNumber("Human Token", Arrays.asList(2)); // one ability's call must generate tokens with same image assert_TokenOrCardImageNumber("Human Token", Arrays.asList(2)); // one ability's call must generate tokens with same image
assert_Inner("test", 0, 0, 1, assert_Inner("test", 0, 0, 1,
"Human Token", 10, false, "MOC=10"); "Human Token", 10, false, "MOC=10");
} }
@ -614,9 +690,9 @@ public class TokenImagesTest extends CardTestPlayerBase {
@Test // it's ok for fail in 1 of 50 @Test // it's ok for fail in 1 of 50
// TODO: implement mock or test command to setup "random" images in TokenImpl.generateTokenInfo // TODO: implement mock or test command to setup "random" images in TokenImpl.generateTokenInfo
// (see setFlipCoinResult and setDieRollResult), so no needs in big amout // (see setFlipCoinResult and setDieRollResult), so no needs in big amount
public void test_Abilities_Incubator_MustTransformWithSameSettings() { public void test_Abilities_Incubator_MustTransformWithSameSettings() {
// bug with miss image data in tranformed incubator token: https://github.com/magefree/mage/issues/11535 // bug with miss image data in transformed incubator token: https://github.com/magefree/mage/issues/11535
// make sure random images take all 3 diff images // make sure random images take all 3 diff images
int needIncubatorTokens = 30; int needIncubatorTokens = 30;
@ -656,8 +732,261 @@ public class TokenImagesTest extends CardTestPlayerBase {
"Phyrexian Token", needPhyrexianTokens, false, "MOM=" + needPhyrexianTokens); "Phyrexian Token", needPhyrexianTokens, false, "MOM=" + needPhyrexianTokens);
// MOM-Incubator has 1 image (number is 0) // MOM-Incubator has 1 image (number is 0)
assert_TokenImageNumber("Incubator Token", Arrays.asList(0)); assert_TokenOrCardImageNumber("Incubator Token", Arrays.asList(0));
// MOM-Phyrexian has 3 images // MOM-Phyrexian has 3 images
assert_TokenImageNumber("Phyrexian Token", Arrays.asList(1, 2, 3)); assert_TokenOrCardImageNumber("Phyrexian Token", Arrays.asList(1, 2, 3));
}
@Test // it's ok for fail in very rare random
// TODO: implement mock or test command to setup "random" images in TokenImpl.generateTokenInfo
// (see setFlipCoinResult and setDieRollResult), so no needs in big amount
public void test_FaceDown_CardWithMorph_MustGetDefaultImage() {
int faceDownAmount = 15;
addCard(Zone.HAND, playerA, "Ainok Tracker", faceDownAmount); // {5}{R}, Morph {4}{R}, face up {3}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5 * faceDownAmount);
IntStream.range(0, faceDownAmount).forEach(i -> {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ainok Tracker using Morph");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), faceDownAmount);
assert_FaceDownMorphImageNumber(Arrays.asList(1, 2, 3));
}
@Test // it's ok for fail in very rare random
public void test_FaceDown_LandWithMorph_MustGetDefaultImage() {
int faceDownAmount = 15;
addCard(Zone.HAND, playerA, "Zoetic Cavern", faceDownAmount);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 * faceDownAmount);
IntStream.range(0, faceDownAmount).forEach(i -> {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zoetic Cavern using Morph");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), faceDownAmount);
assert_FaceDownMorphImageNumber(Arrays.asList(1, 2, 3));
}
@Test
public void test_FaceDown_Spell() {
addCard(Zone.HAND, playerA, "Zoetic Cavern", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zoetic Cavern using Morph");
runCode("stack check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
Assert.assertEquals("stack must be active", 1, game.getState().getStack().size());
// server side spell before resolve contains full info, not empty
// so real data will be full, but view data will be hidden by face down status
String cardName = "Zoetic Cavern";
String needClientControllerName = CardUtil.getCardNameForGUI(cardName, TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MORPH);
String needClientOpponentName = CardUtil.getCardNameForGUI("", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MORPH);
Spell spell = (Spell) game.getState().getStack().stream().findFirst().orElse(null);
Assert.assertNotNull("server - spell must exists", spell);
// make sure image from object's id works fine
IntStream.of(5).forEach(i -> {
UUID objectId = UUID.randomUUID();
int objectImageNumber = TokenRepository.instance.findPreferredTokenInfoForXmage(TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MORPH, objectId).getImageNumber();
Assert.assertNotEquals("wrong image number", 0, objectImageNumber);
IntStream.of(5).forEach(j -> {
int newImageNumber = TokenRepository.instance.findPreferredTokenInfoForXmage(TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MORPH, objectId).getImageNumber();
Assert.assertEquals("generated image numbers must be same for same id", objectImageNumber, newImageNumber);
});
});
// debug
//CardView debugViewOpponent = new CardView(spell, currentGame, false, false);
//CardView debugViewController = new CardView(spell, currentGame, true, false);
// server side (full data)
Assert.assertTrue("server - wrong face down status", spell.isFaceDown(game));
Assert.assertEquals("server - wrong color", spell.getColor(game), new ObjectColor());
Assert.assertEquals("server - wrong name", cardName, spell.getName());
//
// workaround to find image number (from id) - it must be same on each generate
int serverImageNumber = spell.getSpellAbility().getCharacteristics(game).getImageNumber();
Assert.assertNotEquals("server - wrong set code", TokenRepository.XMAGE_TOKENS_SET_CODE, spell.getExpansionSetCode());
Assert.assertNotEquals("server - wrong image number", 0, serverImageNumber);
// client side - controller (hidden + card name)
GameView gameView = getGameView(playerA);
CardView spellView = gameView.getStack().values().stream().findFirst().orElse(null);
Assert.assertNotNull("client, controller - spell must exists", spellView);
Assert.assertTrue("client, controller - wrong face down status", spellView.isFaceDown());
Assert.assertEquals("client, controller - wrong color", spellView.getColor(), new ObjectColor());
Assert.assertEquals("client, controller - wrong spell name", needClientControllerName, spellView.getName());
//
Assert.assertEquals("client, controller - wrong set code", TokenRepository.XMAGE_TOKENS_SET_CODE, spellView.getExpansionSetCode());
Assert.assertEquals("client, controller - wrong card number", "0", spellView.getCardNumber());
Assert.assertEquals("client, controller - wrong image file", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MORPH, spellView.getImageFileName());
Assert.assertEquals("client, controller - wrong image number", serverImageNumber, spellView.getImageNumber());
// client side - opponent (hidden)
gameView = getGameView(playerB);
spellView = gameView.getStack().values().stream().findFirst().orElse(null);
Assert.assertNotNull("client, opponent - spell must exists", spellView);
Assert.assertTrue("client, opponent - wrong face down status", spellView.isFaceDown());
Assert.assertEquals("client, opponent - wrong color", spellView.getColor(), new ObjectColor());
Assert.assertEquals("client, opponent - wrong spell name", needClientOpponentName, spellView.getName());
//
Assert.assertEquals("client, opponent - wrong set code", TokenRepository.XMAGE_TOKENS_SET_CODE, spellView.getExpansionSetCode());
Assert.assertEquals("client, opponent - wrong card number", "0", spellView.getCardNumber());
Assert.assertEquals("client, opponent - wrong image file", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MORPH, spellView.getImageFileName());
Assert.assertEquals("client, opponent - wrong image number", serverImageNumber, spellView.getImageNumber());
});
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1);
}
@Test
public void test_FaceDown_Megamorph_MustGetDefaultImage() {
addCard(Zone.HAND, playerA, "Aerie Bowmasters", 1);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6 + 3);
// prepare face down permanent
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aerie Bowmasters using Megamorph");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
runCode("on face down", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1);
assertPermanentCount(playerA, "Aerie Bowmasters", 0);
Permanent permanent = getPermanent(EmptyNames.FACE_DOWN_CREATURE.toString(), playerA);
assertFaceDownCharacteristics("permanent", permanent, TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH);
});
// face up it and find counter
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}: Turn this");
runCode("on face up", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 0);
assertPermanentCount(playerA, "Aerie Bowmasters", 1);
assertCounterCount(playerA, "Aerie Bowmasters", CounterType.P1P1, 1);
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
}
@Test
public void test_FaceDown_ExileZone_MustGetDefaultImage() {
// {T}: Draw a card, then exile a card from your hand face down.
addCard(Zone.BATTLEFIELD, playerA, "Bane Alley Broker", 1);
addCard(Zone.HAND, playerA, "Forest", 1);
// exile face down
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Draw a card");
addTarget(playerA, "Forest");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// check face down card in exile
runCode("on face down", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
Card card = currentGame.getExile().getAllCards(currentGame, playerA.getId()).get(0);
GameView gameView = getGameView(playerA);
CardView controllerCardView = gameView.getExile()
.stream()
.flatMap(e -> e.values().stream())
.findFirst()
.orElse(null);
gameView = getGameView(playerB);
CardView opponentCardView = gameView.getExile()
.stream()
.flatMap(e -> e.values().stream())
.findFirst()
.orElse(null);
// server side (full data)
// TODO: possible bugged?! Other abilities must not see faced-down card as real on server side!
String needName = "Forest";
Assert.assertTrue("server side - must be face down", card.isFaceDown(currentGame));
Assert.assertEquals("server side - wrong name", needName, card.getName());
Assert.assertEquals("server side - wrong abilities", 2, card.getAbilities(currentGame).size()); // play + add mana
// client side - controller (hidden data + original name)
needName = "Face Down: Forest";
Assert.assertEquals("controller - wrong name", needName, controllerCardView.getName());
Assert.assertTrue("controller - must be face down", controllerCardView.isFaceDown());
Assert.assertEquals("controller - must not have abilities", 0, controllerCardView.getRules().size());
assertOriginalData("controller, original data", controllerCardView, 0, 0, "");
// client side - opponent (hidden data)
needName = "Face Down";
Assert.assertTrue("opponent - must be face down", opponentCardView.isFaceDown());
Assert.assertEquals("opponent - wrong name", needName, opponentCardView.getName());
Assert.assertEquals("opponent - must not have abilities", 0, opponentCardView.getRules().size());
assertOriginalData("opponent, original data", opponentCardView, 0, 0, "");
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
}
@Test
public void test_FaceDown_ForetellInExile_MustGetDefaultImage() {
// Foretell {1}{U}
addCard(Zone.HAND, playerA, "Behold the Multiverse", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
// exile face down as foretell
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Foretell {1}{U}");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// check face down card
runCode("on face down", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
Card card = currentGame.getExile().getAllCards(currentGame, playerA.getId()).get(0);
GameView gameView = getGameView(playerA);
CardView controllerCardView = gameView.getExile()
.stream()
.flatMap(e -> e.values().stream())
.findFirst()
.orElse(null);
gameView = getGameView(playerB);
CardView opponentCardView = gameView.getExile()
.stream()
.flatMap(e -> e.values().stream())
.findFirst()
.orElse(null);
// server side (full data)
// TODO: possible bugged?! Other abilities must not see faced-down card as real on server side!
String needName = "Behold the Multiverse";
Assert.assertTrue("server side - must be face down", card.isFaceDown(currentGame));
Assert.assertEquals("server side - wrong name", needName, card.getName());
Assert.assertTrue("server side - wrong abilities", card.getAbilities(currentGame).size() > 0);
// client side - controller (hidden data + original name)
needName = "Foretell: Behold the Multiverse";
Assert.assertEquals("controller - wrong name", needName, controllerCardView.getName());
Assert.assertTrue("controller - must be face down", controllerCardView.isFaceDown());
Assert.assertEquals("controller - must not have abilities", 0, controllerCardView.getRules().size());
assertOriginalData("controller, original data", controllerCardView, 0, 0, "");
// client side - opponent (hidden data)
needName = "Foretell";
Assert.assertTrue("opponent - must be face down", opponentCardView.isFaceDown());
Assert.assertEquals("opponent - wrong name", needName, opponentCardView.getName());
Assert.assertEquals("opponent - must not have abilities", 0, opponentCardView.getRules().size());
assertOriginalData("opponent, original data", opponentCardView, 0, 0, "");
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
} }
} }

View file

@ -3,10 +3,12 @@ package org.mage.test.utils;
import mage.MageObject; import mage.MageObject;
import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.FlyingAbility;
import mage.constants.CommanderCardType; import mage.constants.CommanderCardType;
import mage.constants.EmptyNames;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.util.GameLog; import mage.util.GameLog;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Ignore; import org.junit.Ignore;
@ -29,6 +31,11 @@ public class CardHintsTest extends CardTestCommanderDuelBase {
// * client side: inject additional elements for popup support (e.g. "a" with "href") // * client side: inject additional elements for popup support (e.g. "a" with "href")
// * client side: process mouse move over a href and show object data like a card popup // * client side: process mouse move over a href and show object data like a card popup
private Document parseHtmlLog(String originalLog) {
// replace log's face down info by real empty name (need for compatibility)
return Jsoup.parse(originalLog.replace(EmptyNames.EMPTY_NAME_IN_LOGS, ""));
}
private void assertObjectHtmlLog(String originalLog, String needVisibleColorPart, String needVisibleNormalPart, String needId) { private void assertObjectHtmlLog(String originalLog, String needVisibleColorPart, String needVisibleNormalPart, String needId) {
String needVisibleFull = needVisibleColorPart; String needVisibleFull = needVisibleColorPart;
if (!needVisibleNormalPart.isEmpty()) { if (!needVisibleNormalPart.isEmpty()) {
@ -44,7 +51,7 @@ public class CardHintsTest extends CardTestCommanderDuelBase {
Assert.assertTrue(mesPrefix + "can't find id" + mesPostfix, originalLog.contains(needId)); Assert.assertTrue(mesPrefix + "can't find id" + mesPostfix, originalLog.contains(needId));
// html check // html check
Element html = Jsoup.parse(originalLog); Element html = parseHtmlLog(originalLog);
Assert.assertEquals(mesPrefix + "can't find full text" + mesPostfix, needVisibleFull, html.text()); Assert.assertEquals(mesPrefix + "can't find full text" + mesPostfix, needVisibleFull, html.text());
Element htmlFont = html.getElementsByTag("font").stream().findFirst().orElse(null); Element htmlFont = html.getElementsByTag("font").stream().findFirst().orElse(null);
Assert.assertNotNull(mesPrefix + "can't find tag [font]" + mesPostfix, htmlFont); Assert.assertNotNull(mesPrefix + "can't find tag [font]" + mesPostfix, htmlFont);
@ -53,7 +60,7 @@ public class CardHintsTest extends CardTestCommanderDuelBase {
// improved log from client (with href and popup support) // improved log from client (with href and popup support)
String popupLog = GameLog.injectPopupSupport(originalLog); String popupLog = GameLog.injectPopupSupport(originalLog);
html = Jsoup.parse(popupLog); html = parseHtmlLog(popupLog);
Assert.assertEquals(mesPrefix + "injected, can't find full text" + mesPostfix, needVisibleFull, html.text()); Assert.assertEquals(mesPrefix + "injected, can't find full text" + mesPostfix, needVisibleFull, html.text());
// href // href
Element htmlA = html.getElementsByTag("a").stream().findFirst().orElse(null); Element htmlA = html.getElementsByTag("a").stream().findFirst().orElse(null);
@ -99,7 +106,7 @@ public class CardHintsTest extends CardTestCommanderDuelBase {
} }
@Test @Test
@Ignore // TODO: Fix test failure related to e264457 @Ignore
public void test_ObjectNamesInHtml() { public void test_ObjectNamesInHtml() {
skipInitShuffling(); skipInitShuffling();
@ -127,7 +134,7 @@ public class CardHintsTest extends CardTestCommanderDuelBase {
.stream() .stream()
.map(c -> currentGame.getObject(c)) .map(c -> currentGame.getObject(c))
.collect(Collectors.toList())); .collect(Collectors.toList()));
Assert.assertEquals(3 + 7 + 1, sampleObjects.size()); // defaul commander game already contains +1 commander Assert.assertEquals(3 + 7 + 1, sampleObjects.size()); // default commander game already contains +1 commander
sampleObjects.forEach(this::assertObjectSupport); sampleObjects.forEach(this::assertObjectSupport);
} }

View file

@ -1,16 +1,14 @@
package mage; package mage;
import java.io.Serializable; import java.io.Serializable;
import java.util.UUID; import java.util.UUID;
/** /**
*
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@FunctionalInterface @FunctionalInterface
public interface MageItem extends Serializable { public interface MageItem extends Serializable {
UUID getId(); UUID getId();
} }

View file

@ -34,6 +34,15 @@ public interface MageObject extends MageItem, Serializable, Copyable<MageObject>
void setImageNumber(Integer imageNumber); void setImageNumber(Integer imageNumber);
/**
* Get image file name
* - empty for default name from a card
* - non-empty for face down objects like Morph (GUI show empty name, but image must show some image)
*/
String getImageFileName();
void setImageFileName(String imageFile);
String getName(); String getName();
/** /**

View file

@ -36,6 +36,7 @@ public abstract class MageObjectImpl implements MageObject {
private String expansionSetCode = ""; private String expansionSetCode = "";
private String cardNumber = ""; private String cardNumber = "";
private String imageFileName = "";
private int imageNumber = 0; private int imageNumber = 0;
protected List<SuperType> supertype = new ArrayList<>(); protected List<SuperType> supertype = new ArrayList<>();
@ -77,6 +78,7 @@ public abstract class MageObjectImpl implements MageObject {
frameStyle = object.frameStyle; frameStyle = object.frameStyle;
expansionSetCode = object.expansionSetCode; expansionSetCode = object.expansionSetCode;
cardNumber = object.cardNumber; cardNumber = object.cardNumber;
imageFileName = object.imageFileName;
imageNumber = object.imageNumber; imageNumber = object.imageNumber;
power = object.power.copy(); power = object.power.copy();
toughness = object.toughness.copy(); toughness = object.toughness.copy();
@ -266,6 +268,16 @@ public abstract class MageObjectImpl implements MageObject {
this.cardNumber = cardNumber; this.cardNumber = cardNumber;
} }
@Override
public String getImageFileName() {
return imageFileName;
}
@Override
public void setImageFileName(String imageFileName) {
this.imageFileName = imageFileName;
}
@Override @Override
public Integer getImageNumber() { public Integer getImageNumber() {
return imageNumber; return imageNumber;

View file

@ -35,15 +35,6 @@ import java.util.UUID;
*/ */
public interface Ability extends Controllable, Serializable { public interface Ability extends Controllable, Serializable {
/**
* Gets the globally unique id of the ability contained within the game.
*
* @return A {@link java.util.UUID} which the game will use to store and
* retrieve the exact instance of this ability.
*/
@Override
UUID getId();
/** /**
* Assigns a new {@link java.util.UUID} * Assigns a new {@link java.util.UUID}
* *
@ -71,14 +62,6 @@ public interface Ability extends Controllable, Serializable {
*/ */
AbilityType getAbilityType(); AbilityType getAbilityType();
/**
* Gets the id of the player in control of this ability.
*
* @return The {@link java.util.UUID} of the controlling player.
*/
@Override
UUID getControllerId();
/** /**
* Sets the id of the controller of this ability. * Sets the id of the controller of this ability.
* *

View file

@ -428,6 +428,7 @@ public abstract class AbilityImpl implements Ability {
case MORE_THAN_MEETS_THE_EYE: case MORE_THAN_MEETS_THE_EYE:
case BESTOW: case BESTOW:
case MORPH: case MORPH:
case MEGAMORPH:
// from Snapcaster Mage: // from Snapcaster Mage:
// If you cast a spell from a graveyard using its flashback ability, you can't pay other alternative costs // If you cast a spell from a graveyard using its flashback ability, you can't pay other alternative costs
// (such as that of Foil). (2018-12-07) // (such as that of Foil). (2018-12-07)
@ -649,6 +650,11 @@ public abstract class AbilityImpl implements Ability {
return controllerId; return controllerId;
} }
@Override
public UUID getControllerOrOwnerId() {
return getControllerId();
}
@Override @Override
public void setControllerId(UUID controllerId) { public void setControllerId(UUID controllerId) {
this.controllerId = controllerId; this.controllerId = controllerId;

View file

@ -184,8 +184,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
if (canChooseTarget(game, playerId)) { if (canChooseTarget(game, playerId)) {
if (approvingObjects == null || approvingObjects.isEmpty()) { if (approvingObjects == null || approvingObjects.isEmpty()) {
return ActivationStatus.withoutApprovingObject(true); return ActivationStatus.withoutApprovingObject(true);
} } else {
else {
return new ActivationStatus(approvingObjects); return new ActivationStatus(approvingObjects);
} }
} }
@ -308,22 +307,27 @@ public class SpellAbility extends ActivatedAbilityImpl {
} }
/** /**
* Returns a card object with the spell characteristics like color, types, * Returns combined card object with the spell characteristics like color, types,
* subtypes etc. E.g. if you cast a Bestow card as enchantment, the * subtypes etc. E.g. if you cast a Bestow card as enchantment, the
* characteristics don't include the creature type. * characteristics don't include the creature type.
* <p>
* Warning, it's not a real card - use it as a blueprint or characteristics searching
* *
* @param game
* @return card object with the spell characteristics * @return card object with the spell characteristics
*/ */
public Card getCharacteristics(Game game) { public Card getCharacteristics(Game game) {
Card spellCharacteristics = game.getSpell(this.getId()); Card spellCharacteristics = game.getSpell(this.getId());
if (spellCharacteristics == null) { if (spellCharacteristics == null) {
// playable check (without put to stack)
spellCharacteristics = game.getCard(this.getSourceId()); spellCharacteristics = game.getCard(this.getSourceId());
} }
if (spellCharacteristics != null) { if (spellCharacteristics != null) {
if (getSpellAbilityCastMode() != SpellAbilityCastMode.NORMAL) { if (getSpellAbilityCastMode() != SpellAbilityCastMode.NORMAL) {
spellCharacteristics = getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(spellCharacteristics, this); // transform characteristics (morph, transform, bestow, etc)
spellCharacteristics = getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(spellCharacteristics, this, game);
} }
spellCharacteristics = spellCharacteristics.copy();
} }
return spellCharacteristics; return spellCharacteristics;
} }

View file

@ -45,6 +45,8 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl {
@Override @Override
public void init(Ability source, Game game) { public void init(Ability source, Game game) {
super.init(source, game); super.init(source, game);
// save permanents to become face down (one time usage on resolve)
for (Permanent perm : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source, game)) { for (Permanent perm : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source, game)) {
if (!perm.isFaceDown(game) && !perm.isTransformable()) { if (!perm.isFaceDown(game) && !perm.isTransformable()) {
affectedObjectList.add(new MageObjectReference(perm, game)); affectedObjectList.add(new MageObjectReference(perm, game));
@ -66,6 +68,7 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl {
public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) {
boolean targetExists = false; boolean targetExists = false;
for (MageObjectReference mor : affectedObjectList) { for (MageObjectReference mor : affectedObjectList) {
// TODO: wtf, why it not use a BecomesFaceDownCreatureEffect.makeFaceDownObject and applied by layers?! Looks buggy
Permanent permanent = mor.getPermanent(game); Permanent permanent = mor.getPermanent(game);
if (permanent != null && permanent.isFaceDown(game)) { if (permanent != null && permanent.isFaceDown(game)) {
targetExists = true; targetExists = true;
@ -119,7 +122,6 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl {
permanent.getPower().setModifiedBaseValue(2); permanent.getPower().setModifiedBaseValue(2);
permanent.getToughness().setModifiedBaseValue(2); permanent.getToughness().setModifiedBaseValue(2);
} }
} }
} }
} }

View file

@ -1,5 +1,6 @@
package mage.abilities.effects.common.continuous; package mage.abilities.effects.common.continuous;
import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.ObjectColor; import mage.ObjectColor;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -9,27 +10,42 @@ import mage.abilities.costs.Costs;
import mage.abilities.costs.CostsImpl; import mage.abilities.costs.CostsImpl;
import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.ContinuousEffectImpl;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.repository.TokenInfo;
import mage.cards.repository.TokenRepository;
import mage.constants.*; import mage.constants.*;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.permanent.token.EmptyToken;
import mage.game.permanent.token.Token;
import mage.util.CardUtil;
import org.apache.log4j.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID;
/** /**
* This effect lets the card be a 2/2 face-down creature, with no text, no name, * This effect lets the card be a 2/2 face-down creature, with no text, no name,
* no subtypes, and no mana cost, if it's face down on the battlefield. And it * no subtypes, and no mana cost, if it's face down on the battlefield. And it
* adds the a TurnFaceUpAbility ability. * adds the a TurnFaceUpAbility ability.
* <p>
* Warning, if a card has multiple face down abilities then keep only one face up cost
* Example: Mischievous Quanar
* - a. Turn Mischievous Quanar face down - BecomesFaceDownCreatureEffect without turn up cost
* - b. Morph - BecomesFaceDownCreatureEffect with turn up cost inside
* *
* @author LevelX2 * @author LevelX2, JayDi85
*/ */
public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl { public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
private static final Logger logger = Logger.getLogger(BecomesFaceDownCreatureEffect.class);
public enum FaceDownType { public enum FaceDownType {
MANIFESTED, MANIFESTED,
MANUAL, MANUAL,
MEGAMORPHED,
MORPHED, MORPHED,
MEGAMORPHED,
DISGUISED, DISGUISED,
CLOAKED CLOAKED
} }
@ -134,17 +150,43 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
throw new UnsupportedOperationException("FaceDownType not yet supported: " + faceDownType); throw new UnsupportedOperationException("FaceDownType not yet supported: " + faceDownType);
} }
} }
makeFaceDownObject(game, source.getSourceId(), permanent, faceDownType, this.turnFaceUpAbility);
} else if (duration == Duration.Custom && foundPermanent) {
discard();
}
return true;
}
permanent.setName(EmptyNames.FACE_DOWN_CREATURE.toString()); public static FaceDownType findFaceDownType(Game game, Permanent permanent) {
permanent.removeAllSuperTypes(game); if (permanent.isMorphed()) {
permanent.removeAllCardTypes(game); return BecomesFaceDownCreatureEffect.FaceDownType.MORPHED;
permanent.addCardType(game, CardType.CREATURE); } else if (permanent.isManifested()) {
permanent.removeAllSubTypes(game); return BecomesFaceDownCreatureEffect.FaceDownType.MANIFESTED;
permanent.getColor(game).setColor(ObjectColor.COLORLESS); } else if (permanent.isFaceDown(game)) {
Card card = game.getCard(permanent.getId()); return BecomesFaceDownCreatureEffect.FaceDownType.MANUAL;
} else {
return null;
}
}
/**
* Convert any object (card, token) to face down (remove/hide all face up information and make it a 2/2 creature)
*/
public static void makeFaceDownObject(Game game, UUID sourceId, MageObject object, FaceDownType faceDownType, Ability turnFaceUpAbility) {
String originalObjectInfo = object.toString();
// warning, it's a direct changes to the object (without game state, so no game param here)
object.setName(EmptyNames.FACE_DOWN_CREATURE.toString());
object.removeAllSuperTypes();
object.getSubtype().clear();
object.removeAllCardTypes();
object.addCardType(CardType.CREATURE);
object.getColor().setColor(ObjectColor.COLORLESS);
// remove wrong abilities
Card card = game.getCard(object.getId());
List<Ability> abilitiesToRemove = new ArrayList<>(); List<Ability> abilitiesToRemove = new ArrayList<>();
for (Ability ability : permanent.getAbilities()) { for (Ability ability : object.getAbilities()) {
// keep gained abilities from other sources, removes only own (card text) // keep gained abilities from other sources, removes only own (card text)
if (card != null && !card.getAbilities().contains(ability)) { if (card != null && !card.getAbilities().contains(ability)) {
@ -158,7 +200,8 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
// //
// so keep all tune face up abilities and other face down compatible // so keep all tune face up abilities and other face down compatible
if (ability.getWorksFaceDown()) { if (ability.getWorksFaceDown()) {
ability.setRuleVisible(false); // only face up abilities hidden by default (see below), so no needs in setRuleVisible
//ability.setRuleVisible(true);
continue; continue;
} }
@ -169,16 +212,75 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
} }
abilitiesToRemove.add(ability); abilitiesToRemove.add(ability);
} }
permanent.removeAbilities(abilitiesToRemove, source.getSourceId(), game);
if (turnFaceUpAbility != null) { // TODO: shouldn't be added by this effect, but separately // add face up abilities
permanent.addAbility(turnFaceUpAbility, source.getSourceId(), game); // TODO: add here all possible face up like morph/disguis, manifest/cloak?
if (object instanceof Permanent) {
// as permanent
Permanent permanentObject = (Permanent) object;
permanentObject.removeAbilities(abilitiesToRemove, sourceId, game);
if (turnFaceUpAbility != null) {
Ability faceUp = turnFaceUpAbility.copy();
faceUp.setRuleVisible(true);
permanentObject.addAbility(faceUp, sourceId, game);
} }
permanent.getPower().setModifiedBaseValue(2); } else if (object instanceof CardImpl) {
permanent.getToughness().setModifiedBaseValue(2); // as card
} else if (duration == Duration.Custom && foundPermanent) { CardImpl cardObject = (CardImpl) object;
discard(); cardObject.getAbilities().removeAll(abilitiesToRemove);
if (turnFaceUpAbility != null) {
Ability faceUp = turnFaceUpAbility.copy();
faceUp.setRuleVisible(true);
cardObject.addAbility(faceUp);
}
}
object.getPower().setModifiedBaseValue(2);
object.getToughness().setModifiedBaseValue(2);
// image
String tokenName;
switch (faceDownType) {
case MORPHED:
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MORPH;
break;
case MEGAMORPHED:
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH;
break;
case MANIFESTED:
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST;
break;
case CLOAKED:
tokenName = "TODO-CLOAKED";
break;
case DISGUISED:
tokenName = "TODO-DISGUISED";
break;
case MANUAL:
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANUAL;
break;
default:
throw new IllegalArgumentException("Un-supported face down type for image: " + faceDownType);
}
Token faceDownToken = new EmptyToken();
TokenInfo faceDownInfo = TokenRepository.instance.findPreferredTokenInfoForXmage(tokenName, object.getId());
if (faceDownInfo != null) {
faceDownToken.setExpansionSetCode(faceDownInfo.getSetCode());
faceDownToken.setCardNumber("0");
faceDownToken.setImageFileName(faceDownInfo.getName());
faceDownToken.setImageNumber(faceDownInfo.getImageNumber());
} else {
logger.error("Can't find face down image for " + tokenName + ": " + originalObjectInfo);
// TODO: add default image like backface (warning, missing image info must be visible in card popup)?
}
CardUtil.copySetAndCardNumber(object, faceDownToken);
// hide rarity info
if (object instanceof Card) {
((Card) object).setRarity(Rarity.SPECIAL);
} }
return true;
} }
} }

View file

@ -115,7 +115,7 @@ public class BestowAbility extends SpellAbility {
public static void becomeCreature(Permanent permanent, Game game) { public static void becomeCreature(Permanent permanent, Game game) {
// permanently changes to the object // permanently changes to the object
if (permanent != null) { if (permanent != null) {
MageObject basicObject = permanent.getBasicMageObject(game); MageObject basicObject = permanent.getBasicMageObject();
if (basicObject != null) { if (basicObject != null) {
game.checkStateAndTriggered(); // Bug #8157 game.checkStateAndTriggered(); // Bug #8157
basicObject.getSubtype().remove(SubType.AURA); basicObject.getSubtype().remove(SubType.AURA);
@ -164,7 +164,7 @@ class BestowEntersBattlefieldEffect extends ReplacementEffectImpl {
} }
// change types permanently // change types permanently
MageObject basicObject = bestowPermanent.getBasicMageObject(game); MageObject basicObject = bestowPermanent.getBasicMageObject();
if (basicObject != null && !basicObject.getSubtype().contains(SubType.AURA)) { if (basicObject != null && !basicObject.getSubtype().contains(SubType.AURA)) {
basicObject.addSubType(SubType.AURA); basicObject.addSubType(SubType.AURA);
basicObject.removeCardType(CardType.CREATURE); basicObject.removeCardType(CardType.CREATURE);

View file

@ -463,4 +463,9 @@ public class ForetellAbility extends SpecialAction {
} }
} }
public static boolean isCardInForetell(Card card, Game game) {
// searching ForetellCostAbility - it adds for foretelled cards only after exile
return card.getAbilities(game).containsClass(ForetellCostAbility.class);
}
} }

View file

@ -1,7 +1,5 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import mage.MageObject;
import mage.ObjectColor;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
@ -13,18 +11,17 @@ import mage.abilities.costs.mana.ManaCost;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect; import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect.FaceDownType; import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect.FaceDownType;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.*; import mage.constants.SpellAbilityCastMode;
import mage.game.Game; import mage.constants.SpellAbilityType;
import mage.game.permanent.Permanent; import mage.constants.TimingRule;
import mage.game.permanent.token.EmptyToken;
import mage.game.permanent.token.Token;
import mage.util.CardUtil;
/** /**
* Morph and Megamorph
* <p>
* 702.36. Morph * 702.36. Morph
* <p> * <p>
* 702.36a Morph is a static ability that functions in any zone from which you * 702.36a Morph is a static ability that functions in any zone from which you
* could play the card it’s on, and the morph effect works any time the card is * could play the card it's on, and the morph effect works any time the card is
* face down. "Morph [cost]" means "You may cast this card as a 2/2 face-down * face down. "Morph [cost]" means "You may cast this card as a 2/2 face-down
* creature, with no text, no name, no subtypes, and no mana cost by paying {3} * creature, with no text, no name, no subtypes, and no mana cost by paying {3}
* rather than paying its mana cost." (See rule 707, "Face-Down Spells and * rather than paying its mana cost." (See rule 707, "Face-Down Spells and
@ -33,7 +30,7 @@ import mage.util.CardUtil;
* 702.36b To cast a card using its morph ability, turn it face down. It becomes * 702.36b To cast a card using its morph ability, turn it face down. It becomes
* a 2/2 face-down creature card, with no text, no name, no subtypes, and no * a 2/2 face-down creature card, with no text, no name, no subtypes, and no
* mana cost. Any effects or prohibitions that would apply to casting a card * mana cost. Any effects or prohibitions that would apply to casting a card
* with these characteristics (and not the face-up card’s characteristics) are * with these characteristics (and not the face-up card's characteristics) are
* applied to casting this card. These values are the copiable values of that * applied to casting this card. These values are the copiable values of that
* object's characteristics. (See rule 613, "Interaction of Continuous Effects," * object's characteristics. (See rule 613, "Interaction of Continuous Effects,"
* and rule 706, "Copying Objects.") Put it onto the stack (as a face-down spell * and rule 706, "Copying Objects.") Put it onto the stack (as a face-down spell
@ -48,54 +45,54 @@ import mage.util.CardUtil;
* <p> * <p>
* 702.36d If you have priority, you may turn a face-down permanent you control * 702.36d If you have priority, you may turn a face-down permanent you control
* face up. This is a special action; it doesn't use the stack (see rule 115). * face up. This is a special action; it doesn't use the stack (see rule 115).
* To do this, show all players what the permanent’s morph cost would be if it * To do this, show all players what the permanent's morph cost would be if it
* were face up, pay that cost, then turn the permanent face up. (If the * were face up, pay that cost, then turn the permanent face up. (If the
* permanent wouldn't have a morph cost if it were face up, it can’t be turned * permanent wouldn't have a morph cost if it were face up, it can't be turned
* face up this way.) The morph effect on it ends, and it regains its normal * face up this way.) The morph effect on it ends, and it regains its normal
* characteristics. Any abilities relating to the permanent entering the * characteristics. Any abilities relating to the permanent entering the
* battlefield don’t trigger when it’s turned face up and don’t have any effect, * battlefield don't trigger when it's turned face up and don't have any effect,
* because the permanent has already entered the battlefield. * because the permanent has already entered the battlefield.
* <p> * <p>
* 702.36e See rule 707, "Face-Down Spells and Permanents," for more information * 702.36e See rule 707, "Face-Down Spells and Permanents," for more information
* on how to cast cards with morph. * on how to cast cards with morph.
* *
* @author LevelX2 * @author LevelX2, JayDi85
*/ */
public class MorphAbility extends SpellAbility { public class MorphAbility extends SpellAbility {
protected static final String ABILITY_KEYWORD = "Morph"; protected static final String ABILITY_KEYWORD = "Morph";
protected static final String ABILITY_KEYWORD_MEGA = "Megamorph";
protected static final String REMINDER_TEXT = "You may cast this card face down as a " protected static final String REMINDER_TEXT = "You may cast this card face down as a "
+ "2/2 creature for {3}. Turn it face up any time for its morph cost."; + "2/2 creature for {3}. Turn it face up any time for its morph cost.";
protected static final String ABILITY_KEYWORD_MEGA = "Megamorph";
protected static final String REMINDER_TEXT_MEGA = "You may cast this card face down " protected static final String REMINDER_TEXT_MEGA = "You may cast this card face down "
+ "as a 2/2 creature for {3}. Turn it face up any time for its megamorph " + "as a 2/2 creature for {3}. Turn it face up any time for its megamorph "
+ "cost and put a +1/+1 counter on it."; + "cost and put a +1/+1 counter on it.";
protected Costs<Cost> morphCosts; protected Costs<Cost> morphCosts;
// needed to check activation status, if card changes zone after casting it
private final boolean megamorph;
public MorphAbility(Card card, Cost morphCost) { public MorphAbility(Card card, Cost morphCost) {
this(card, morphCost, false); this(card, morphCost, false);
} }
public MorphAbility(Card card, Cost morphCost, boolean megamorph) { public MorphAbility(Card card, Cost morphCost, boolean useMegamorph) {
super(new GenericManaCost(3), card.getName()); super(new GenericManaCost(3), card.getName());
this.timing = TimingRule.SORCERY;
this.morphCosts = new CostsImpl<>(); this.morphCosts = new CostsImpl<>();
this.morphCosts.add(morphCost); this.morphCosts.add(morphCost);
this.megamorph = megamorph; this.setSpellAbilityCastMode(useMegamorph ? SpellAbilityCastMode.MEGAMORPH : SpellAbilityCastMode.MORPH);
this.setSpellAbilityCastMode(SpellAbilityCastMode.MORPH);
this.setSpellAbilityType(SpellAbilityType.BASE_ALTERNATE); this.setSpellAbilityType(SpellAbilityType.BASE_ALTERNATE);
// face down effect (hidden by default, visible in face down objects)
Ability ability = new SimpleStaticAbility(new BecomesFaceDownCreatureEffect( Ability ability = new SimpleStaticAbility(new BecomesFaceDownCreatureEffect(
morphCosts, (megamorph ? FaceDownType.MEGAMORPHED : FaceDownType.MORPHED))); morphCosts, (useMegamorph ? FaceDownType.MEGAMORPHED : FaceDownType.MORPHED)));
ability.setWorksFaceDown(true); ability.setWorksFaceDown(true);
ability.setRuleVisible(false); ability.setRuleVisible(false);
this.timing = TimingRule.SORCERY;
addSubAbility(ability); addSubAbility(ability);
} }
protected MorphAbility(final MorphAbility ability) { protected MorphAbility(final MorphAbility ability) {
super(ability); super(ability);
this.morphCosts = ability.morphCosts; // can't be changed this.morphCosts = ability.morphCosts; // can't be changed
this.megamorph = ability.megamorph;
} }
@Override @Override
@ -110,55 +107,20 @@ public class MorphAbility extends SpellAbility {
@Override @Override
public String getRule() { public String getRule() {
boolean isMana = morphCosts.get(0) instanceof ManaCost; boolean isMana = morphCosts.get(0) instanceof ManaCost;
String name = megamorph ? ABILITY_KEYWORD_MEGA : ABILITY_KEYWORD; String text;
String reminder = " <i>(" + (megamorph ? REMINDER_TEXT_MEGA : REMINDER_TEXT) + ")</i>"; String reminder;
return name + (isMana ? " " : "&mdash;") + switch (this.getSpellAbilityCastMode()) {
morphCosts.getText() + (isMana ? ' ' : ". ") + reminder; case MORPH:
text = ABILITY_KEYWORD;
reminder = REMINDER_TEXT;
break;
case MEGAMORPH:
text = ABILITY_KEYWORD_MEGA;
reminder = REMINDER_TEXT_MEGA;
break;
default:
throw new IllegalArgumentException("Un-supported spell ability cast mode for morph: " + this.getSpellAbilityCastMode());
} }
return text + (isMana ? " " : "&mdash;") + morphCosts.getText() + (isMana ? ' ' : ". ") + " <i>(" + reminder + ")</i>";
/**
* Hide all info and make it a 2/2 creature
*
* @param targetObject
* @param sourcePermanent source of the face down status
* @param game
*/
public static void setPermanentToFaceDownCreature(MageObject targetObject, Permanent sourcePermanent, Game game) {
targetObject.getPower().setModifiedBaseValue(2);
targetObject.getToughness().setModifiedBaseValue(2);
targetObject.getAbilities().clear();
targetObject.getColor(game).setColor(new ObjectColor());
targetObject.setName("");
targetObject.removeAllCardTypes(game);
targetObject.addCardType(game, CardType.CREATURE);
targetObject.removeAllSubTypes(game);
targetObject.removeAllSuperTypes(game);
targetObject.getManaCost().clear();
Token emptyImage = new EmptyToken();
// TODO: add morph image here?
if (targetObject instanceof Permanent) {
// hide image info
CardUtil.copySetAndCardNumber(targetObject, emptyImage);
// hide rarity info
((Permanent) targetObject).setRarity(Rarity.SPECIAL);
} else if (targetObject instanceof Token) {
CardUtil.copySetAndCardNumber(targetObject, emptyImage);
} else {
throw new IllegalArgumentException("Wrong code usage: un-supported targetObject in face down method: " + targetObject.getClass().getSimpleName());
}
}
public static void setCardToFaceDownCreature(Card targetCard) {
targetCard.getPower().setModifiedBaseValue(2);
targetCard.getToughness().setModifiedBaseValue(2);
targetCard.getAbilities().clear();
targetCard.getColor().setColor(new ObjectColor());
targetCard.setName("");
targetCard.removeAllCardTypes();
targetCard.addCardType(CardType.CREATURE);
targetCard.getSubtype().clear();
targetCard.removeAllSuperTypes();
targetCard.getManaCost().clear();
} }
} }

View file

@ -80,27 +80,34 @@ public class ProtectionAbility extends StaticAbility {
} }
public boolean canTarget(MageObject source, Game game) { public boolean canTarget(MageObject source, Game game) {
// TODO: need research, protection ability can be bugged with aura and aura permanents, spells (see below)
// permanent restriction
if (filter instanceof FilterPermanent) { if (filter instanceof FilterPermanent) {
if (source instanceof Permanent) { if (source instanceof Permanent) {
return !filter.match(source, game); return !((FilterPermanent) filter).match((Permanent) source, game);
} }
// TODO: possible bugged, need token too?
return true; return true;
} }
// card restriction
if (filter instanceof FilterCard) { if (filter instanceof FilterCard) {
if (source instanceof Permanent) { if (source instanceof Card) {
return !((FilterCard) filter).match((Card) source, ((Permanent) source).getControllerId(), this, game); return !((FilterCard) filter).match((Card) source, ((Card) source).getControllerOrOwnerId(), this, game);
} else if (source instanceof Card) {
return !((FilterCard) filter).match((Card) source, ((Card) source).getOwnerId(), this, game);
} else if (source instanceof Token) { } else if (source instanceof Token) {
// Fake a permanent with the Token info. // make fake permanent cause it checked before real permanent create
PermanentToken token = new PermanentToken((Token) source, null, game); // warning, Token don't have controllerId info, so it can be a problem here
return !((FilterCard) filter).match((Card) token, game); // TODO: wtf, possible bugged for filters that checking controller/player (if so then use with controllerId param)
PermanentToken fakePermanent = new PermanentToken((Token) source, UUID.randomUUID(), game);
return !((FilterCard) filter).match(fakePermanent, game);
} }
return true; return true;
} }
// spell restriction
if (filter instanceof FilterSpell) { if (filter instanceof FilterSpell) {
// TODO: need research, possible bugged
// Problem here is that for the check if a player can play a Spell, the source // Problem here is that for the check if a player can play a Spell, the source
// object is still a card and not a spell yet. // object is still a card and not a spell yet.
if (source instanceof Spell || game.inCheckPlayableState() && source.isInstantOrSorcery(game)) { if (source instanceof Spell || game.inCheckPlayableState() && source.isInstantOrSorcery(game)) {
@ -109,16 +116,20 @@ public class ProtectionAbility extends StaticAbility {
return true; return true;
} }
// unknown restriction
if (filter instanceof FilterObject) { if (filter instanceof FilterObject) {
return !filter.match(source, game); return !((FilterObject) filter).match(source, game);
} }
// player restriction
if (filter instanceof FilterPlayer) { if (filter instanceof FilterPlayer) {
Player player = null; Player player = null;
if (source instanceof Permanent) { if (source instanceof Card) {
player = game.getPlayer(((Permanent) source).getControllerId()); player = game.getPlayer(((Card) source).getControllerOrOwnerId());
} else if (source instanceof Card) { } else if (source instanceof Token) {
player = game.getPlayer(((Card) source).getOwnerId()); // TODO: fakePermanent will not work here like above, so try to rework whole logic
throw new IllegalArgumentException("Wrong code usage: token can't be checked in player restriction filter");
} }
return !((FilterPlayer) filter).match(player, this.getControllerId(), this, game); return !((FilterPlayer) filter).match(player, this.getControllerId(), this, game);
} }

View file

@ -14,6 +14,7 @@ import mage.counters.Counters;
import mage.filter.FilterMana; import mage.filter.FilterMana;
import mage.game.Game; import mage.game.Game;
import mage.game.GameState; import mage.game.GameState;
import mage.game.Ownerable;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.util.ManaUtil; import mage.util.ManaUtil;
import mage.watchers.common.CommanderPlaysCountWatcher; import mage.watchers.common.CommanderPlaysCountWatcher;
@ -21,12 +22,12 @@ import mage.watchers.common.CommanderPlaysCountWatcher;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
public interface Card extends MageObject { public interface Card extends MageObject, Ownerable {
UUID getOwnerId();
Rarity getRarity(); // null for tokens Rarity getRarity(); // null for tokens
void setRarity(Rarity rarity);
void setOwnerId(UUID ownerId); void setOwnerId(UUID ownerId);
/** /**
@ -46,7 +47,11 @@ public interface Card extends MageObject {
List<String> getRules(Game game); // gets card rules + in game modifications List<String> getRules(Game game); // gets card rules + in game modifications
void checkForCountersToAdd(Permanent permanent, Ability source, Game game); /**
* Find ETB counters and apply it to permanent.
* Warning, it's one time action, use it before a put to battlefield only.
*/
void applyEnterWithCounters(Permanent permanent, Ability source, Game game);
void setFaceDown(boolean value, Game game); void setFaceDown(boolean value, Game game);
@ -143,10 +148,12 @@ public interface Card extends MageObject {
List<Mana> getMana(); List<Mana> getMana();
/** /**
* @return true if there exists various art images for this card * Set contains multiple cards with same card name but different images. Used for image path generation.
*/ */
boolean getUsesVariousArt(); boolean getUsesVariousArt();
void setUsesVariousArt(boolean usesVariousArt);
Counters getCounters(Game game); Counters getCounters(Game game);
Counters getCounters(GameState state); Counters getCounters(GameState state);

View file

@ -11,6 +11,7 @@ import mage.abilities.keyword.FlashbackAbility;
import mage.abilities.keyword.ReconfigureAbility; import mage.abilities.keyword.ReconfigureAbility;
import mage.abilities.keyword.SunburstAbility; import mage.abilities.keyword.SunburstAbility;
import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.cards.mock.MockableCard;
import mage.cards.repository.PluginClassloaderRegistery; import mage.cards.repository.PluginClassloaderRegistery;
import mage.constants.*; import mage.constants.*;
import mage.counters.Counter; import mage.counters.Counter;
@ -58,12 +59,15 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
protected CardImpl(UUID ownerId, CardSetInfo setInfo, CardType[] cardTypes, String costs) { protected CardImpl(UUID ownerId, CardSetInfo setInfo, CardType[] cardTypes, String costs) {
this(ownerId, setInfo, cardTypes, costs, SpellAbilityType.BASE); this(ownerId, setInfo, cardTypes, costs, SpellAbilityType.BASE);
} }
protected CardImpl(UUID ownerId, CardSetInfo setInfo, CardType[] cardTypes, String costs, SpellAbilityType spellAbilityType) { protected CardImpl(UUID ownerId, CardSetInfo setInfo, CardType[] cardTypes, String costs, SpellAbilityType spellAbilityType) {
this(ownerId, setInfo.getName()); this(ownerId, setInfo.getName());
this.rarity = setInfo.getRarity(); this.rarity = setInfo.getRarity();
this.setExpansionSetCode(setInfo.getExpansionSetCode()); this.setExpansionSetCode(setInfo.getExpansionSetCode());
this.setCardNumber(setInfo.getCardNumber()); this.setCardNumber(setInfo.getCardNumber());
this.setImageFileName(""); // use default
this.setImageNumber(0);
this.cardType.addAll(Arrays.asList(cardTypes)); this.cardType.addAll(Arrays.asList(cardTypes));
this.manaCost.load(costs); this.manaCost.load(costs);
setDefaultColor(); setDefaultColor();
@ -119,12 +123,24 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
ownerId = card.ownerId; ownerId = card.ownerId;
rarity = card.rarity; rarity = card.rarity;
// TODO: wtf, do not copy card sides cause it must be re-created each time (see details in getSecondCardFace)
// must be reworked to normal copy and workable transform without such magic
nightCard = card.nightCard;
secondSideCardClazz = card.secondSideCardClazz; secondSideCardClazz = card.secondSideCardClazz;
secondSideCard = null; // will be set on first getSecondCardFace call if card has one secondSideCard = null; // will be set on first getSecondCardFace call if card has one
nightCard = card.nightCard; if (card.secondSideCard instanceof MockableCard) {
// workaround to support gui's mock cards
secondSideCard = card.secondSideCard.copy();
}
meldsWithClazz = card.meldsWithClazz; meldsWithClazz = card.meldsWithClazz;
meldsToClazz = card.meldsToClazz; meldsToClazz = card.meldsToClazz;
meldsToCard = null; // will be set on first getMeldsToCard call if card has one meldsToCard = null; // will be set on first getMeldsToCard call if card has one
if (card.meldsToCard instanceof MockableCard) {
// workaround to support gui's mock cards
meldsToCard = card.meldsToCard.copy();
}
spellAbility = null; // will be set on first getSpellAbility call if card has one spellAbility = null; // will be set on first getSpellAbility call if card has one
flipCard = card.flipCard; flipCard = card.flipCard;
@ -203,6 +219,11 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
return rarity; return rarity;
} }
@Override
public void setRarity(Rarity rarity) {
this.rarity = rarity;
}
@Override @Override
public void addInfo(String key, String value, Game game) { public void addInfo(String key, String value, Game game) {
game.getState().getCardState(objectId).addInfo(key, value); game.getState().getCardState(objectId).addInfo(key, value);
@ -360,7 +381,12 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
@Override @Override
public void setOwnerId(UUID ownerId) { public void setOwnerId(UUID ownerId) {
this.ownerId = ownerId; this.ownerId = ownerId;
abilities.setControllerId(ownerId); this.abilities.setControllerId(ownerId);
}
@Override
public UUID getControllerOrOwnerId() {
return getOwnerId();
} }
@Override @Override
@ -549,7 +575,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
} }
@Override @Override
public void checkForCountersToAdd(Permanent permanent, Ability source, Game game) { public void applyEnterWithCounters(Permanent permanent, Ability source, Game game) {
Counters countersToAdd = game.getEnterWithCounters(permanent.getId()); Counters countersToAdd = game.getEnterWithCounters(permanent.getId());
if (countersToAdd != null) { if (countersToAdd != null) {
for (Counter counter : countersToAdd.values()) { for (Counter counter : countersToAdd.values()) {
@ -620,6 +646,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
if (secondSideCard == null) { if (secondSideCard == null) {
secondSideCard = initSecondSideCard(secondSideCardClazz); secondSideCard = initSecondSideCard(secondSideCardClazz);
if (secondSideCard != null && secondSideCard.getSpellAbility() != null) { if (secondSideCard != null && secondSideCard.getSpellAbility() != null) {
// TODO: wtf, why it set cast mode here?! Transform tests fails without it
// must be reworked without that magic, also see CardImpl'constructor for copy code
secondSideCard.getSpellAbility().setSourceId(this.getId()); secondSideCard.getSpellAbility().setSourceId(this.getId());
secondSideCard.getSpellAbility().setSpellAbilityType(SpellAbilityType.BASE_ALTERNATE); secondSideCard.getSpellAbility().setSpellAbilityType(SpellAbilityType.BASE_ALTERNATE);
secondSideCard.getSpellAbility().setSpellAbilityCastMode(SpellAbilityCastMode.TRANSFORMED); secondSideCard.getSpellAbility().setSpellAbilityCastMode(SpellAbilityCastMode.TRANSFORMED);
@ -693,6 +721,11 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
return usesVariousArt; return usesVariousArt;
} }
@Override
public void setUsesVariousArt(boolean usesVariousArt) {
this.usesVariousArt = usesVariousArt;
}
@Override @Override
public Counters getCounters(Game game) { public Counters getCounters(Game game) {
return getCounters(game.getState()); return getCounters(game.getState());
@ -703,13 +736,6 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
return state.getCardState(this.objectId).getCounters(); return state.getCardState(this.objectId).getCounters();
} }
/**
* @return The controller if available otherwise the owner.
*/
protected UUID getControllerOrOwner() {
return ownerId;
}
@Override @Override
public boolean addCounters(Counter counter, Ability source, Game game) { public boolean addCounters(Counter counter, Ability source, Game game) {
return addCounters(counter, source.getControllerId(), source, game); return addCounters(counter, source.getControllerId(), source, game);
@ -787,7 +813,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
if (!getCounters(game).removeCounter(name, 1)) { if (!getCounters(game).removeCounter(name, 1)) {
break; break;
} }
GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, objectId, source, getControllerOrOwner()); GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, objectId, source, getControllerOrOwnerId());
if (source != null if (source != null
&& source.getControllerId() != null) { && source.getControllerId() != null) {
event.setPlayerId(source.getControllerId()); // player who controls the source ability that removed the counter event.setPlayerId(source.getControllerId()); // player who controls the source ability that removed the counter
@ -796,7 +822,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
game.fireEvent(event); game.fireEvent(event);
finalAmount++; finalAmount++;
} }
GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, objectId, source, getControllerOrOwner()); GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, objectId, source, getControllerOrOwnerId());
if (source != null if (source != null
&& source.getControllerId() != null) { && source.getControllerId() != null) {
event.setPlayerId(source.getControllerId()); // player who controls the source ability that removed the counter event.setPlayerId(source.getControllerId()); // player who controls the source ability that removed the counter

View file

@ -18,7 +18,7 @@ import java.util.List;
* *
* @author North * @author North
*/ */
public class MockCard extends CardImpl { public class MockCard extends CardImpl implements MockableCard {
static public String ADVENTURE_NAME_SEPARATOR = " // "; static public String ADVENTURE_NAME_SEPARATOR = " // ";
static public String MODAL_DOUBLE_FACES_NAME_SEPARATOR = " // "; static public String MODAL_DOUBLE_FACES_NAME_SEPARATOR = " // ";
@ -42,6 +42,8 @@ public class MockCard extends CardImpl {
super(null, card.getName()); super(null, card.getName());
this.setExpansionSetCode(card.getSetCode()); this.setExpansionSetCode(card.getSetCode());
this.setCardNumber(card.getCardNumber()); this.setCardNumber(card.getCardNumber());
this.setImageFileName(""); // use default
this.setImageNumber(0);
this.power = mageIntFromString(card.getPower()); this.power = mageIntFromString(card.getPower());
this.toughness = mageIntFromString(card.getToughness()); this.toughness = mageIntFromString(card.getToughness());
this.rarity = card.getRarity(); this.rarity = card.getRarity();
@ -157,7 +159,7 @@ public class MockCard extends CardImpl {
if (adventureSpellName != null) { if (adventureSpellName != null) {
return getName() + ADVENTURE_NAME_SEPARATOR + adventureSpellName; return getName() + ADVENTURE_NAME_SEPARATOR + adventureSpellName;
} else if (isModalDoubleFacedCard) { } else if (isModalDoubleFacedCard) {
return getName() + MODAL_DOUBLE_FACES_NAME_SEPARATOR + this.secondSideCard.getName(); return getName() + MODAL_DOUBLE_FACES_NAME_SEPARATOR + this.getSecondCardFace().getName();
} else { } else {
return getName(); return getName();
} }

View file

@ -18,7 +18,8 @@ import java.util.List;
/** /**
* @author North * @author North
*/ */
public class MockSplitCard extends SplitCard { public class MockSplitCard extends SplitCard implements MockableCard {
public MockSplitCard(CardInfo card) { public MockSplitCard(CardInfo card) {
super(null, new CardSetInfo(card.getName(), card.getSetCode(), card.getCardNumber(), card.getRarity()), super(null, new CardSetInfo(card.getName(), card.getSetCode(), card.getCardNumber(), card.getRarity()),
card.getTypes().toArray(new CardType[0]), card.getTypes().toArray(new CardType[0]),

View file

@ -0,0 +1,9 @@
package mage.cards.mock;
/**
* Card is mock, e.g. used in GUI only like deck editor
*
* @author JayDi85
*/
public interface MockableCard {
}

View file

@ -18,6 +18,20 @@ public enum TokenRepository {
public static final String XMAGE_TOKENS_SET_CODE = "XMAGE"; public static final String XMAGE_TOKENS_SET_CODE = "XMAGE";
// All possible image names. Used for:
// - image name from tok/xmage folder
// - additional card name for controller like "Morph: face up name"
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MANUAL = "Face Down";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST = "Manifest";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MORPH = "Morph";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH = "Megamorph";
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_FORETELL = "Foretell";
public static final String XMAGE_IMAGE_NAME_COPY = "Copy";
public static final String XMAGE_IMAGE_NAME_CITY_BLESSING = "City's Blessing";
public static final String XMAGE_IMAGE_NAME_DAY = "Day";
public static final String XMAGE_IMAGE_NAME_NIGHT = "Night";
public static final String XMAGE_IMAGE_NAME_THE_MONARCH = "The Monarch";
private static final Logger logger = Logger.getLogger(TokenRepository.class); private static final Logger logger = Logger.getLogger(TokenRepository.class);
private ArrayList<TokenInfo> allTokens = new ArrayList<>(); private ArrayList<TokenInfo> allTokens = new ArrayList<>();
@ -229,44 +243,60 @@ public enum TokenRepository {
// Search by // Search by
// - https://tagger.scryfall.com/tags/card/assistant-cards // - https://tagger.scryfall.com/tags/card/assistant-cards
// - https://scryfall.com/search?q=otag%3Aassistant-cards&unique=cards&as=grid&order=name // - https://scryfall.com/search?q=otag%3Aassistant-cards&unique=cards&as=grid&order=name
// Must add only unique prints // Must add only unique images/prints
// TODO: add custom set in download window to download a custom tokens only // TODO: add custom set in download window to download a custom tokens only
// TODO: add custom set in card viewer to view a custom tokens only
ArrayList<TokenInfo> res = new ArrayList<>(); ArrayList<TokenInfo> res = new ArrayList<>();
// Backface
// TODO: can't find backface's api url so use direct link from third party site instead (must be replaced to scryfall someday)
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MANUAL, 1, "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"));
// Copy // Copy
// https://scryfall.com/search?q=include%3Aextras+unique%3Aprints+type%3Atoken+copy&unique=cards&as=grid&order=name // https://scryfall.com/search?q=include%3Aextras+unique%3Aprints+type%3Atoken+copy&unique=cards&as=grid&order=name
res.add(createXmageToken("Copy", 1, "https://api.scryfall.com/cards/tclb/19/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 1, "https://api.scryfall.com/cards/tclb/19/en?format=image"));
res.add(createXmageToken("Copy", 2, "https://api.scryfall.com/cards/tsnc/1/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 2, "https://api.scryfall.com/cards/tsnc/1/en?format=image"));
res.add(createXmageToken("Copy", 3, "https://api.scryfall.com/cards/tvow/19/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 3, "https://api.scryfall.com/cards/tvow/19/en?format=image"));
res.add(createXmageToken("Copy", 4, "https://api.scryfall.com/cards/tznr/12/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 4, "https://api.scryfall.com/cards/tznr/12/en?format=image"));
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 5, "https://api.scryfall.com/cards/twho/1/en?format=image"));
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 6, "https://api.scryfall.com/cards/tlci/1/en?format=image"));
// City's Blessing // City's Blessing
// https://scryfall.com/search?q=type%3Atoken+include%3Aextras+unique%3Aprints+City%27s+Blessing+&unique=cards&as=grid&order=name // https://scryfall.com/search?q=type%3Atoken+include%3Aextras+unique%3Aprints+City%27s+Blessing+&unique=cards&as=grid&order=name
res.add(createXmageToken("City's Blessing", 1, "https://api.scryfall.com/cards/f18/2/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_CITY_BLESSING, 1, "https://api.scryfall.com/cards/f18/2/en?format=image"));
// Day // Night // Day // Night
// https://scryfall.com/search?q=include%3Aextras+unique%3Aprints+%22Day+%2F%2F+Night%22&unique=cards&as=grid&order=name // https://scryfall.com/search?q=include%3Aextras+unique%3Aprints+%22Day+%2F%2F+Night%22&unique=cards&as=grid&order=name
res.add(createXmageToken("Day", 1, "https://api.scryfall.com/cards/tvow/21/en?format=image&face=front")); res.add(createXmageToken(XMAGE_IMAGE_NAME_DAY, 1, "https://api.scryfall.com/cards/tvow/21/en?format=image&face=front"));
res.add(createXmageToken("Night", 1, "https://api.scryfall.com/cards/tvow/21/en?format=image&face=back")); res.add(createXmageToken(XMAGE_IMAGE_NAME_NIGHT, 1, "https://api.scryfall.com/cards/tvow/21/en?format=image&face=back"));
// Manifest // Manifest
// https://scryfall.com/search?q=Manifest+include%3Aextras+unique%3Aprints&unique=cards&as=grid&order=name // https://scryfall.com/search?q=Manifest+include%3Aextras+unique%3Aprints&unique=cards&as=grid&order=name
res.add(createXmageToken("Manifest", 1, "https://api.scryfall.com/cards/tc19/28/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, 1, "https://api.scryfall.com/cards/tc19/28/en?format=image"));
res.add(createXmageToken("Manifest", 2, "https://api.scryfall.com/cards/tc18/1/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, 2, "https://api.scryfall.com/cards/tc18/1/en?format=image"));
res.add(createXmageToken("Manifest", 3, "https://api.scryfall.com/cards/tfrf/4/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, 3, "https://api.scryfall.com/cards/tfrf/4/en?format=image"));
res.add(createXmageToken("Manifest", 4, "https://api.scryfall.com/cards/tncc/3/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, 4, "https://api.scryfall.com/cards/tncc/3/en?format=image"));
// Morph // Morph
// https://scryfall.com/search?q=Morph+unique%3Aprints+otag%3Aassistant-cards&unique=cards&as=grid&order=name // https://scryfall.com/search?q=Morph+unique%3Aprints+otag%3Aassistant-cards&unique=cards&as=grid&order=name
res.add(createXmageToken("Morph", 1, "https://api.scryfall.com/cards/tktk/11/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MORPH, 1, "https://api.scryfall.com/cards/tktk/11/en?format=image"));
res.add(createXmageToken("Morph", 2, "https://api.scryfall.com/cards/ta25/15/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MORPH, 2, "https://api.scryfall.com/cards/ta25/15/en?format=image"));
res.add(createXmageToken("Morph", 3, "https://api.scryfall.com/cards/tc19/27/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MORPH, 3, "https://api.scryfall.com/cards/tc19/27/en?format=image"));
// Megamorph
// warning, mtg don't have megamorph tokens yet so use morph instead (users will see the diff by card name and face up ability text)
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH, 1, "https://api.scryfall.com/cards/tktk/11/en?format=image"));
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH, 2, "https://api.scryfall.com/cards/ta25/15/en?format=image"));
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH, 3, "https://api.scryfall.com/cards/tc19/27/en?format=image"));
// Foretell
// https://scryfall.com/search?q=Foretell+unique%3Aprints+otag%3Aassistant-cards&unique=cards&as=grid&order=name
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_FORETELL, 1, "https://api.scryfall.com/cards/tkhm/23/en?format=image"));
// The Monarch // The Monarch
// https://scryfall.com/search?q=Monarch+unique%3Aprints+otag%3Aassistant-cards&unique=cards&as=grid&order=name // https://scryfall.com/search?q=Monarch+unique%3Aprints+otag%3Aassistant-cards&unique=cards&as=grid&order=name
res.add(createXmageToken("The Monarch", 1, "https://api.scryfall.com/cards/tonc/22/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_MONARCH, 1, "https://api.scryfall.com/cards/tonc/22/en?format=image"));
res.add(createXmageToken("The Monarch", 2, "https://api.scryfall.com/cards/tcn2/1/en?format=image")); res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_MONARCH, 2, "https://api.scryfall.com/cards/tcn2/1/en?format=image"));
res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_MONARCH, 3, "https://api.scryfall.com/cards/tltc/15/en?format=image"));
return res; return res;
} }
@ -307,4 +337,33 @@ public enum TokenRepository {
public TokenInfo findPreferredTokenInfoForClass(String className, String preferredSetCode) { public TokenInfo findPreferredTokenInfoForClass(String className, String preferredSetCode) {
return findPreferredTokenInfo(TokenRepository.instance.getByClassName(className), preferredSetCode); return findPreferredTokenInfo(TokenRepository.instance.getByClassName(className), preferredSetCode);
} }
/**
* Try to find random image info by related set code (use for inner tokens like copy, morph, etc)
* <p>
* Allow to generate "random" image number from an object's UUID (workaround to keep same image after each update)
*
* @param randomFromId object's UUID for image number generation
*/
public TokenInfo findPreferredTokenInfoForXmage(String name, UUID randomFromId) {
List<TokenInfo> needList = TokenRepository.instance.getByType(TokenType.XMAGE)
.stream()
.filter(info -> info.getName().equals(name))
.collect(Collectors.toList());
if (needList.isEmpty()) {
return null;
}
if (needList.size() == 1) {
return needList.get(0);
}
// workaround to find stable image from object's id (need for face down image generation)
if (randomFromId == null) {
return RandomUtil.randomFromCollection(needList);
} else {
// warning, do not use global random here (it can break it with same seed)
int itemIndex = new Random(randomFromId.getLeastSignificantBits()).nextInt(needList.size());
return needList.get(itemIndex);
}
}
} }

View file

@ -1,12 +1,18 @@
package mage.constants; package mage.constants;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
import mage.abilities.keyword.BestowAbility; import mage.abilities.keyword.BestowAbility;
import mage.abilities.keyword.PrototypeAbility; import mage.abilities.keyword.PrototypeAbility;
import mage.cards.Card; import mage.cards.Card;
import mage.abilities.keyword.MorphAbility; import mage.game.Game;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** /**
* @author LevelX2 * @author LevelX2
*/ */
@ -16,16 +22,24 @@ public enum SpellAbilityCastMode {
FLASHBACK("Flashback"), FLASHBACK("Flashback"),
BESTOW("Bestow"), BESTOW("Bestow"),
PROTOTYPE("Prototype"), PROTOTYPE("Prototype"),
MORPH("Morph"), MORPH("Morph", false, SpellAbilityCastMode.MORPH_ADDITIONAL_RULE),
MEGAMORPH("Megamorph", false, SpellAbilityCastMode.MORPH_ADDITIONAL_RULE),
TRANSFORMED("Transformed", true), TRANSFORMED("Transformed", true),
DISTURB("Disturb", true), DISTURB("Disturb", true),
MORE_THAN_MEETS_THE_EYE("More than Meets the Eye", true); MORE_THAN_MEETS_THE_EYE("More than Meets the Eye", true);
private static final String MORPH_ADDITIONAL_RULE = "You may cast this card as a 2/2 face-down creature, with no text,"
+ " no name, no subtypes, and no mana cost by paying {3} rather than paying its mana cost.";
private final String text; private final String text;
// Should the cast mode use the second face? // should the cast mode use the second face?
private final boolean isTransformed; private final boolean isTransformed;
// use it to add additional info in stack object cause face down has nothing
// TODO: is it possible to use InfoEffect or CardHint instead that?
private final List<String> additionalRulesOnStack;
public boolean isTransformed() { public boolean isTransformed() {
return this.isTransformed; return this.isTransformed;
} }
@ -35,8 +49,17 @@ public enum SpellAbilityCastMode {
} }
SpellAbilityCastMode(String text, boolean isTransformed) { SpellAbilityCastMode(String text, boolean isTransformed) {
this(text, isTransformed, null);
}
SpellAbilityCastMode(String text, boolean isTransformed, String additionalRulesOnStack) {
this.text = text; this.text = text;
this.isTransformed = isTransformed; this.isTransformed = isTransformed;
this.additionalRulesOnStack = additionalRulesOnStack == null ? null : Collections.singletonList(additionalRulesOnStack);
}
public List<String> getAdditionalRulesOnStack() {
return additionalRulesOnStack;
} }
@Override @Override
@ -44,27 +67,46 @@ public enum SpellAbilityCastMode {
return text; return text;
} }
public Card getTypeModifiedCardObjectCopy(Card card, SpellAbility spellAbility) { public Card getTypeModifiedCardObjectCopy(Card card, SpellAbility spellAbility, Game game) {
Card cardCopy = card.copy(); Card cardCopy = card.copy();
if (this.equals(BESTOW)) {
BestowAbility.becomeAura(cardCopy);
}
if (this.isTransformed) { if (this.isTransformed) {
Card tmp = card.getSecondCardFace(); Card tmp = card.getSecondCardFace();
if (tmp != null) { if (tmp != null) {
cardCopy = tmp.copy(); cardCopy = tmp.copy();
} }
} }
if (this.equals(PROTOTYPE)) {
switch (this) {
case BESTOW:
BestowAbility.becomeAura(cardCopy);
break;
case PROTOTYPE:
cardCopy = ((PrototypeAbility) spellAbility).prototypeCardSpell(cardCopy); cardCopy = ((PrototypeAbility) spellAbility).prototypeCardSpell(cardCopy);
} break;
if (this.equals(MORPH)) { case MORPH:
case MEGAMORPH:
if (cardCopy instanceof Spell) { if (cardCopy instanceof Spell) {
//Spell doesn't support setName, so make a copy of the card (we're blowing it away anyway) //Spell doesn't support setName, so make a copy of the card (we're blowing it away anyway)
// TODO: research - is it possible to apply face down code to spell instead workaround with card
cardCopy = ((Spell) cardCopy).getCard().copy(); cardCopy = ((Spell) cardCopy).getCard().copy();
} }
MorphAbility.setCardToFaceDownCreature(cardCopy); BecomesFaceDownCreatureEffect.makeFaceDownObject(game, null, cardCopy, BecomesFaceDownCreatureEffect.FaceDownType.MORPHED, null);
break;
case NORMAL:
case MADNESS:
case FLASHBACK:
case DISTURB:
case MORE_THAN_MEETS_THE_EYE:
// it changes only cost, so keep other characteristics
// TODO: research - why TRANSFORMED here - is it used in this.isTransformed code?!
break;
case TRANSFORMED:
// TODO: research - why TRANSFORMED here - is it used in this.isTransformed code?!
break;
default:
throw new IllegalArgumentException("Un-supported ability cast mode: " + this);
} }
return cardCopy; return cardCopy;
} }
} }

View file

@ -5,12 +5,10 @@ import java.util.UUID;
/** /**
* @author magenoxx_at_gmail.com * @author magenoxx_at_gmail.com
*/ */
public interface Controllable { public interface Controllable extends ControllableOrOwnerable {
UUID getControllerId(); UUID getControllerId();
UUID getId();
default boolean isControlledBy(UUID controllerID) { default boolean isControlledBy(UUID controllerID) {
if (getControllerId() == null) { if (getControllerId() == null) {
return false; return false;

View file

@ -0,0 +1,16 @@
package mage.game;
import mage.MageItem;
import java.util.UUID;
/**
* @author JayDi85
*/
public interface ControllableOrOwnerable extends MageItem {
/**
* @return the controller if available otherwise the owner
*/
UUID getControllerOrOwnerId();
}

View file

@ -503,6 +503,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
*/ */
void applyEffects(); void applyEffects();
@Deprecated // TODO: must research usage and remove it from all non engine code (example: Bestow ability, ProcessActions must be used instead)
boolean checkStateAndTriggered(); boolean checkStateAndTriggered();
void playPriority(UUID activePlayerId, boolean resuming); void playPriority(UUID activePlayerId, boolean resuming);
@ -589,6 +590,9 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
boolean executingRollback(); boolean executingRollback();
/**
* Add counters to permanent before ETB. Use it before put real permanent to battlefield.
*/
void setEnterWithCounters(UUID sourceId, Counters counters); void setEnterWithCounters(UUID sourceId, Counters counters);
Counters getEnterWithCounters(UUID sourceId); Counters getEnterWithCounters(UUID sourceId);

View file

@ -15,6 +15,7 @@ import mage.abilities.effects.Effect;
import mage.abilities.effects.PreventionEffectData; import mage.abilities.effects.PreventionEffectData;
import mage.abilities.effects.common.CopyEffect; import mage.abilities.effects.common.CopyEffect;
import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
import mage.abilities.effects.keyword.FinalityCounterEffect; import mage.abilities.effects.keyword.FinalityCounterEffect;
import mage.abilities.effects.keyword.ShieldCounterEffect; import mage.abilities.effects.keyword.ShieldCounterEffect;
import mage.abilities.effects.keyword.StunCounterEffect; import mage.abilities.effects.keyword.StunCounterEffect;
@ -1996,10 +1997,9 @@ public abstract class GameImpl implements Game {
// workaround to find real copyable characteristics of transformed/facedown/etc permanents // workaround to find real copyable characteristics of transformed/facedown/etc permanents
if (copyFromPermanent.isMorphed() BecomesFaceDownCreatureEffect.FaceDownType faceDownType = BecomesFaceDownCreatureEffect.findFaceDownType(this, copyFromPermanent);
|| copyFromPermanent.isManifested() if (faceDownType != null) {
|| copyFromPermanent.isFaceDown(this)) { BecomesFaceDownCreatureEffect.makeFaceDownObject(this, null, newBluePrint, faceDownType, null);
MorphAbility.setPermanentToFaceDownCreature(newBluePrint, copyFromPermanent, this);
} }
newBluePrint.assignNewId(); newBluePrint.assignNewId();
if (copyFromPermanent.isTransformed()) { if (copyFromPermanent.isTransformed()) {

View file

@ -0,0 +1,10 @@
package mage.game;
import java.util.UUID;
/**
* @author JayDi85
*/
public interface Ownerable extends ControllableOrOwnerable {
UUID getOwnerId();
}

View file

@ -1,7 +1,6 @@
package mage.game; package mage.game;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.keyword.TransformAbility; import mage.abilities.keyword.TransformAbility;
import mage.cards.*; import mage.cards.*;
import mage.constants.Outcome; import mage.constants.Outcome;
@ -325,33 +324,50 @@ public final class ZonesHandler {
// Handle all normal cases // Handle all normal cases
Card card = getTargetCard(game, event.getTargetId()); Card card = getTargetCard(game, event.getTargetId());
if (card == null) { if (card == null) {
// If we can't find the card we can't remove it. // if we can't find the card we can't remove it.
return false; return false;
} }
boolean isGoodToMove = false;
if (info.faceDown) {
// any card can be moved as face down (doubled faced cards also support face down)
isGoodToMove = true;
} else if (event.getToZone().equals(Zone.BATTLEFIELD)) {
// non-permanents can't move to battlefield
// "return to battlefield transformed" abilities uses game state value instead "info.transformed", so check it too
// TODO: possible bug with non permanent on second side like Life // Death, see https://github.com/magefree/mage/issues/11573
// need to check second side here, not status only
// TODO: possible bug with Nightbound, search all usage of getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED and insert additional check Ability.checkCard
boolean wantToPutTransformed = card.isTransformable()
&& Boolean.TRUE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId()));
isGoodToMove = card.isPermanent(game) || wantToPutTransformed;
} else {
// other zones allows to move
isGoodToMove = true;
}
if (!isGoodToMove) {
return false;
}
// TODO: is it buggy? Card characteristics are global - if you change face down then it will be
// changed in original card too, not in blueprint only
card.setFaceDown(info.faceDown, game);
boolean success = false; boolean success = false;
if (info.faceDown) {
card.setFaceDown(true, game);
} else if (event.getToZone().equals(Zone.BATTLEFIELD)) {
if (!card.isPermanent(game)
&& (!card.isTransformable() || Boolean.FALSE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId())))) {
// Non permanents (Instants, Sorceries, ... stay in the zone they are if an abilty/effect tries to move it to the battlefield
return false;
}
}
if (!game.replaceEvent(event)) { if (!game.replaceEvent(event)) {
Zone fromZone = event.getFromZone(); Zone fromZone = event.getFromZone();
if (event.getToZone() == Zone.BATTLEFIELD) { if (event.getToZone() == Zone.BATTLEFIELD) {
// prepare card and permanent // PUT TO BATTLEFIELD AS PERMANENT
// If needed take attributes from the spell (e.g. color of spell was changed) // prepare card and permanent (card must contain full data, even for face down)
card = takeAttributesFromSpell(card, event, game); // if needed to take attributes from the spell (e.g. color of spell was changed)
card = prepareBlueprintCardFromSpell(card, event, game);
// controlling player can be replaced so use event player now // controlling player can be replaced so use event player now
Permanent permanent; Permanent permanent;
if (card instanceof MeldCard) { if (card instanceof MeldCard) {
permanent = new PermanentMeld(card, event.getPlayerId(), game); permanent = new PermanentMeld(card, event.getPlayerId(), game);
} else if (card instanceof ModalDoubleFacedCard) { } else if (card instanceof ModalDoubleFacedCard) {
// main mdf card must be processed before that call (e.g. only halfes can be moved to battlefield) // main mdf card must be processed before that call (e.g. only halves can be moved to battlefield)
throw new IllegalStateException("Unexpected trying of move mdf card to battlefield instead half"); throw new IllegalStateException("Unexpected trying of move mdf card to battlefield instead half");
} else if (card instanceof Permanent) { } else if (card instanceof Permanent) {
throw new IllegalStateException("Unexpected trying of move permanent to battlefield instead card"); throw new IllegalStateException("Unexpected trying of move permanent to battlefield instead card");
@ -361,11 +377,12 @@ public final class ZonesHandler {
// put onto battlefield with possible counters // put onto battlefield with possible counters
game.getPermanentsEntering().put(permanent.getId(), permanent); game.getPermanentsEntering().put(permanent.getId(), permanent);
card.checkForCountersToAdd(permanent, source, game); card.applyEnterWithCounters(permanent, source, game);
permanent.setTapped(info instanceof ZoneChangeInfo.Battlefield permanent.setTapped(info instanceof ZoneChangeInfo.Battlefield
&& ((ZoneChangeInfo.Battlefield) info).tapped); && ((ZoneChangeInfo.Battlefield) info).tapped);
// if need prototyped version
if (Zone.STACK == event.getFromZone()) { if (Zone.STACK == event.getFromZone()) {
Spell spell = game.getStack().getSpell(event.getTargetId()); Spell spell = game.getStack().getSpell(event.getTargetId());
if (spell != null) { if (spell != null) {
@ -375,11 +392,14 @@ public final class ZonesHandler {
permanent.setFaceDown(info.faceDown, game); permanent.setFaceDown(info.faceDown, game);
if (info.faceDown) { if (info.faceDown) {
card.setFaceDown(false, game); // TODO: need research cards with "setFaceDown(false"
// TODO: delete after new release and new face down bugs (old code remove face down status from a card for unknown reason), 2024-02-20
//card.setFaceDown(false, game);
} }
// make sure the controller of all continuous effects of this card are switched to the current controller // make sure the controller of all continuous effects of this card are switched to the current controller
game.setScopeRelevant(true); game.setScopeRelevant(true);
try {
game.getContinuousEffects().setController(permanent.getId(), permanent.getControllerId()); game.getContinuousEffects().setController(permanent.getId(), permanent.getControllerId());
if (permanent.entersBattlefield(source, game, fromZone, true) if (permanent.entersBattlefield(source, game, fromZone, true)
&& card.removeFromZone(game, fromZone, source)) { && card.removeFromZone(game, fromZone, source)) {
@ -390,14 +410,16 @@ public final class ZonesHandler {
game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId()); game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId());
game.getPermanentsEntering().remove(permanent.getId()); game.getPermanentsEntering().remove(permanent.getId());
} }
} finally {
game.setScopeRelevant(false); game.setScopeRelevant(false);
}
} else if (event.getTarget() != null) { } else if (event.getTarget() != null) {
card.setFaceDown(info.faceDown, game); // PUT PERMANENT TO OTHER ZONE (e.g. remove only)
Permanent target = event.getTarget(); Permanent target = event.getTarget();
success = target.removeFromZone(game, fromZone, source) success = target.removeFromZone(game, fromZone, source)
&& game.getPlayer(target.getControllerId()).removeFromBattlefield(target, source, game); && game.getPlayer(target.getControllerId()).removeFromBattlefield(target, source, game);
} else { } else {
card.setFaceDown(info.faceDown, game); // PUT CARD TO OTHER ZONE
success = card.removeFromZone(game, fromZone, source); success = card.removeFromZone(game, fromZone, source);
} }
} }
@ -434,17 +456,30 @@ public final class ZonesHandler {
return order; return order;
} }
private static Card takeAttributesFromSpell(Card card, ZoneChangeEvent event, Game game) { private static Card prepareBlueprintCardFromSpell(Card card, ZoneChangeEvent event, Game game) {
card = card.copy(); card = card.copy();
if (Zone.STACK == event.getFromZone()) { if (Zone.STACK == event.getFromZone()) {
// TODO: wtf, why only colors!? Must research and remove colors workaround or add all other data like types too
Spell spell = game.getStack().getSpell(event.getTargetId()); Spell spell = game.getStack().getSpell(event.getTargetId());
if (spell != null && !spell.isFaceDown(game)) {
// TODO: wtf, why only colors!? Must research and remove colors workaround // old version
if (false && spell != null && !spell.isFaceDown(game)) {
if (!card.getColor(game).equals(spell.getColor(game))) { if (!card.getColor(game).equals(spell.getColor(game))) {
// the card that is referenced to in the permanent is copied and the spell attributes are set to this copied card // the card that is referenced to in the permanent is copied and the spell attributes are set to this copied card
card.getColor(game).setColor(spell.getColor(game)); card.getColor(game).setColor(spell.getColor(game));
} }
} }
// new version
if (true && spell != null && spell.getSpellAbility() != null) {
Card characteristics = spell.getSpellAbility().getCharacteristics(game);
if (!characteristics.isFaceDown(game)) {
if (!card.getColor(game).equals(characteristics.getColor(game))) {
// TODO: don't work with prototyped spells (setColor can't set colorless color)
card.getColor(game).setColor(characteristics.getColor(game));
}
}
}
} }
return card; return card;
} }

View file

@ -13,8 +13,9 @@ public abstract class CommandObjectImpl implements CommandObject {
private UUID id; private UUID id;
private String name = ""; private String name = "";
private String expansionSetCode; private String expansionSetCode = "";
private String cardNumber; private String cardNumber = "";
private String imageFileName = "";
private int imageNumber; private int imageNumber;
public CommandObjectImpl(String name) { public CommandObjectImpl(String name) {
@ -27,6 +28,7 @@ public abstract class CommandObjectImpl implements CommandObject {
this.name = object.name; this.name = object.name;
this.expansionSetCode = object.expansionSetCode; this.expansionSetCode = object.expansionSetCode;
this.cardNumber = object.cardNumber; this.cardNumber = object.cardNumber;
this.imageFileName = object.imageFileName;
this.imageNumber = object.imageNumber; this.imageNumber = object.imageNumber;
} }
@ -55,6 +57,16 @@ public abstract class CommandObjectImpl implements CommandObject {
this.cardNumber = cardNumber; this.cardNumber = cardNumber;
} }
@Override
public String getImageFileName() {
return imageFileName;
}
@Override
public void setImageFileName(String imageFileName) {
this.imageFileName = imageFileName;
}
@Override @Override
public Integer getImageNumber() { public Integer getImageNumber() {
return imageNumber; return imageNumber;

View file

@ -127,6 +127,11 @@ public class Commander extends CommandObjectImpl {
return sourceObject.getOwnerId(); return sourceObject.getOwnerId();
} }
@Override
public UUID getControllerOrOwnerId() {
return getControllerId();
}
@Override @Override
public CommandObject copy() { public CommandObject copy() {
return new Commander(this); return new Commander(this);

View file

@ -184,6 +184,11 @@ public class Dungeon extends CommandObjectImpl {
this.abilites.setControllerId(controllerId); this.abilites.setControllerId(controllerId);
} }
@Override
public UUID getControllerOrOwnerId() {
return getControllerId();
}
@Override @Override
public void setCopy(boolean isCopy, MageObject copyFrom) { public void setCopy(boolean isCopy, MageObject copyFrom) {
this.copy = isCopy; this.copy = isCopy;

View file

@ -69,6 +69,7 @@ public abstract class Emblem extends CommandObjectImpl {
if (foundInfo != null) { if (foundInfo != null) {
this.setExpansionSetCode(foundInfo.getSetCode()); this.setExpansionSetCode(foundInfo.getSetCode());
this.setCardNumber(""); this.setCardNumber("");
this.setImageFileName(""); // use default
this.setImageNumber(foundInfo.getImageNumber()); this.setImageNumber(foundInfo.getImageNumber());
} else { } else {
// how-to fix: add emblem to the tokens-database // how-to fix: add emblem to the tokens-database
@ -99,6 +100,11 @@ public abstract class Emblem extends CommandObjectImpl {
this.abilites.setControllerId(controllerId); this.abilites.setControllerId(controllerId);
} }
@Override
public UUID getControllerOrOwnerId() {
return getControllerId();
}
@Override @Override
abstract public Emblem copy(); abstract public Emblem copy();

View file

@ -73,6 +73,7 @@ public abstract class Plane extends CommandObjectImpl {
if (foundInfo != null) { if (foundInfo != null) {
this.setExpansionSetCode(foundInfo.getSetCode()); this.setExpansionSetCode(foundInfo.getSetCode());
this.setCardNumber(""); this.setCardNumber("");
this.setImageFileName(""); // use default
this.setImageNumber(foundInfo.getImageNumber()); this.setImageNumber(foundInfo.getImageNumber());
} else { } else {
// how-to fix: add plane to the tokens-database // how-to fix: add plane to the tokens-database
@ -103,6 +104,11 @@ public abstract class Plane extends CommandObjectImpl {
this.abilites.setControllerId(controllerId); this.abilites.setControllerId(controllerId);
} }
@Override
public UUID getControllerOrOwnerId() {
return getControllerId();
}
@Override @Override
abstract public Plane copy(); abstract public Plane copy();

View file

@ -23,8 +23,8 @@ import java.util.stream.Collectors;
* mana burn with Yurlok of Scorch Thrash, and anything else players might think of. * mana burn with Yurlok of Scorch Thrash, and anything else players might think of.
*/ */
public final class EmblemOfCard extends Emblem { public final class EmblemOfCard extends Emblem {
private final boolean usesVariousArt; private final boolean usesVariousArt;
private static final Logger logger = Logger.getLogger(EmblemOfCard.class);
public static Card lookupCard( public static Card lookupCard(
String cardName, String cardName,
@ -75,8 +75,10 @@ public final class EmblemOfCard extends Emblem {
return ability; return ability;
}).collect(Collectors.toList())); }).collect(Collectors.toList()));
this.getAbilities().setSourceId(this.getId()); this.getAbilities().setSourceId(this.getId());
this.setExpansionSetCode(card.getExpansionSetCode()); this.setExpansionSetCode(card.getExpansionSetCode());
this.setCardNumber(card.getCardNumber()); this.setCardNumber(card.getCardNumber());
this.setImageFileName(card.getImageFileName());
this.setImageNumber(card.getImageNumber()); this.setImageNumber(card.getImageNumber());
this.usesVariousArt = card.getUsesVariousArt(); this.usesVariousArt = card.getUsesVariousArt();
} }

View file

@ -103,4 +103,8 @@ public class ZoneChangeEvent extends GameEvent {
return this.source; return this.source;
} }
@Override
public String toString() {
return super.toString() + ", from " + getFromZone() + " to " + getToZone();
}
} }

View file

@ -4,7 +4,6 @@ import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.Rarity;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Controllable; import mage.game.Controllable;
import mage.game.Game; import mage.game.Game;
@ -113,8 +112,6 @@ public interface Permanent extends Card, Controllable {
void setExpansionSetCode(String expansionSetCode); void setExpansionSetCode(String expansionSetCode);
void setRarity(Rarity rarity);
void setFlipCard(boolean flipCard); void setFlipCard(boolean flipCard);
void setFlipCardName(String flipCardName); void setFlipCardName(String flipCardName);
@ -136,7 +133,7 @@ public interface Permanent extends Card, Controllable {
boolean hasProtectionFrom(MageObject source, Game game); boolean hasProtectionFrom(MageObject source, Game game);
/** /**
* @param attachment * @param attachment can be any object: card, permanent, token
* @param source can be null for default checks like state base * @param source can be null for default checks like state base
* @param game * @param game
* @param silentMode - use it to ignore warning message for users (e.g. for * @param silentMode - use it to ignore warning message for users (e.g. for
@ -194,7 +191,14 @@ public interface Permanent extends Card, Controllable {
void reset(Game game); void reset(Game game);
MageObject getBasicMageObject(Game game); /**
* Return original/blueprint/printable object (token or card)
* <p>
* Original object used on each game cycle for permanent reset and apply all active effects
* <p>
* Warning, all changes to the original object will be applied forever
*/
MageObject getBasicMageObject();
boolean destroy(Ability source, Game game); boolean destroy(Ability source, Game game);
@ -221,7 +225,7 @@ public interface Permanent extends Card, Controllable {
* Add abilities to the permanent, can be used in effects * Add abilities to the permanent, can be used in effects
* *
* @param ability * @param ability
* @param sourceId * @param sourceId can be null
* @param game * @param game
* @return can be null for exists abilities * @return can be null for exists abilities
*/ */

View file

@ -14,28 +14,30 @@ import mage.cards.SplitCard;
import mage.constants.SpellAbilityType; import mage.constants.SpellAbilityType;
import mage.game.Game; import mage.game.Game;
import mage.game.events.ZoneChangeEvent; import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.token.Token;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import java.util.UUID; import java.util.UUID;
/** /**
* Static permanent on the battlefield. There are possible multiple permanents per one card, * Static permanent on the battlefield. There are possible multiple permanents per one card,
* so be carefull for targets (ids are different) and ZCC (zcc is static for permanent). * so be carefully for targets (ids are different) and ZCC (zcc is static for permanent).
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class PermanentCard extends PermanentImpl { public class PermanentCard extends PermanentImpl {
// blueprint e.g. a copy of the original card that was cast
// (this is not the original card, so it's possible to change some attribute before it enters the battlefield)
// TODO: wtf, it modified on getCard/getBasicMageObject/getMainCard() and other places, e.g. on bestow -- must be fixed!
protected Card card;
protected int maxLevelCounters; protected int maxLevelCounters;
// A copy of the original card that was cast (this is not the original card, so it's possible to change some attribute to this blueprint to change attributes to the permanent if it enters the battlefield with e.g. a subtype)
protected Card card; // TODO: wtf, it modified on getCard and other places, e.g. on bestow -- must be fixed!
// the number this permanent instance had
protected int zoneChangeCounter; protected int zoneChangeCounter;
public PermanentCard(Card card, UUID controllerId, Game game) { public PermanentCard(Card card, UUID controllerId, Game game) {
super(card.getId(), card.getOwnerId(), controllerId, card.getName()); super(card.getId(), card.getOwnerId(), controllerId, card.getName()); // card id
// TODO: wtf, must research - is it possible to have diff ids for same card id?!
// ETB with counters depends on card id, not permanent id
// TODO: ETB with counters works with tokens?! Must research
// runtime check: must use real card only inside // runtime check: must use real card only inside
if (card instanceof PermanentCard) { if (card instanceof PermanentCard) {
@ -124,7 +126,7 @@ public class PermanentCard extends PermanentImpl {
this.abilities.setSourceId(objectId); this.abilities.setSourceId(objectId);
this.cardType.clear(); this.cardType.clear();
this.cardType.addAll(card.getCardType()); this.cardType.addAll(card.getCardType());
this.color = card.getColor(null).copy(); this.color = card.getColor(null).copy(); // TODO: need research - why it null
this.frameColor = card.getFrameColor(game).copy(); this.frameColor = card.getFrameColor(game).copy();
this.frameStyle = card.getFrameStyle(); this.frameStyle = card.getFrameStyle();
this.manaCost = card.getManaCost().copy(); this.manaCost = card.getManaCost().copy();
@ -134,10 +136,12 @@ public class PermanentCard extends PermanentImpl {
this.subtype.copyFrom(card.getSubtype()); this.subtype.copyFrom(card.getSubtype());
this.supertype.clear(); this.supertype.clear();
this.supertype.addAll(card.getSuperType()); this.supertype.addAll(card.getSuperType());
this.rarity = card.getRarity();
this.setExpansionSetCode(card.getExpansionSetCode()); this.setExpansionSetCode(card.getExpansionSetCode());
this.setCardNumber(card.getCardNumber()); this.setCardNumber(card.getCardNumber());
this.rarity = card.getRarity(); this.setImageFileName(card.getImageFileName());
this.setImageNumber(card.getImageNumber());
this.usesVariousArt = card.getUsesVariousArt(); this.usesVariousArt = card.getUsesVariousArt();
if (card.getSecondCardFace() != null) { if (card.getSecondCardFace() != null) {
@ -152,7 +156,7 @@ public class PermanentCard extends PermanentImpl {
} }
@Override @Override
public MageObject getBasicMageObject(Game game) { public MageObject getBasicMageObject() {
return card; return card;
} }
@ -214,12 +218,15 @@ public class PermanentCard extends PermanentImpl {
@Override @Override
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) { public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
// TODO: wtf, permanent must not change ZCC at all, is it buggy here?!
card.updateZoneChangeCounter(game, event); card.updateZoneChangeCounter(game, event);
zoneChangeCounter = card.getZoneChangeCounter(game); zoneChangeCounter = card.getZoneChangeCounter(game);
} }
@Override @Override
public void setZoneChangeCounter(int value, Game game) { public void setZoneChangeCounter(int value, Game game) {
// TODO: wtf, why it sync card only without permanent zcc, is it buggy here?!
// TODO: miss zoneChangeCounter = card.getZoneChangeCounter(game); ?
card.setZoneChangeCounter(value, game); card.setZoneChangeCounter(value, game);
} }
@ -227,13 +234,4 @@ public class PermanentCard extends PermanentImpl {
public Card getMainCard() { public Card getMainCard() {
return card.getMainCard(); return card.getMainCard();
} }
@Override
public String toString() {
return card.toString()
+ ", " + ((this instanceof Token) ? "T" : "C")
+ (this.isCopy() ? ", copy" : "")
+ ", " + this.getPower() + "/" + this.getToughness()
+ (this.isTapped() ? ", tapped" : "");
}
} }

View file

@ -12,6 +12,7 @@ import mage.abilities.effects.Effect;
import mage.abilities.effects.RequirementEffect; import mage.abilities.effects.RequirementEffect;
import mage.abilities.effects.RestrictionEffect; import mage.abilities.effects.RestrictionEffect;
import mage.abilities.effects.common.RegenerateSourceEffect; import mage.abilities.effects.common.RegenerateSourceEffect;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
import mage.abilities.hint.HintUtils; import mage.abilities.hint.HintUtils;
import mage.abilities.keyword.*; import mage.abilities.keyword.*;
import mage.cards.Card; import mage.cards.Card;
@ -30,6 +31,7 @@ import mage.game.command.CommandObject;
import mage.game.events.*; import mage.game.events.*;
import mage.game.events.GameEvent.EventType; import mage.game.events.GameEvent.EventType;
import mage.game.permanent.token.SquirrelToken; import mage.game.permanent.token.SquirrelToken;
import mage.game.permanent.token.Token;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject; import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
@ -118,6 +120,12 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected PermanentImpl(UUID ownerId, UUID controllerId, String name) { protected PermanentImpl(UUID ownerId, UUID controllerId, String name) {
super(ownerId, name); super(ownerId, name);
// runtime check: need controller (if you catch it in non-game then use random uuid)
if (controllerId == null) {
throw new IllegalArgumentException("Wrong code usage: controllerId can't be null - " + name, new Throwable());
}
this.originalControllerId = controllerId; this.originalControllerId = controllerId;
this.controllerId = controllerId; this.controllerId = controllerId;
this.counters = new Counters(); this.counters = new Counters();
@ -186,12 +194,20 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = threadLocalBuilder.get(); String name = getName().isEmpty()
sb.append(this.getName()).append('-').append(this.getExpansionSetCode()); ? "face down" + " [" + getId().toString().substring(0, 3) + "]"
if (copy) { : getIdName();
sb.append(" [Copy]"); String imageInfo = getExpansionSetCode()
} + ":" + getCardNumber()
return sb.toString(); + ":" + getImageFileName()
+ ":" + getImageNumber();
return name
+ ", " + (getBasicMageObject() instanceof Token ? "T" : "C")
+ ", " + getBasicMageObject().getClass().getSimpleName()
+ ", " + imageInfo
+ ", " + this.getPower() + "/" + this.getToughness()
+ (this.isCopy() ? ", copy" : "")
+ (this.isTapped() ? ", tapped" : "");
} }
@Override @Override
@ -483,7 +499,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
} }
@Override @Override
protected UUID getControllerOrOwner() { public UUID getControllerOrOwnerId() {
return controllerId; return controllerId;
} }
@ -1222,18 +1238,24 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override @Override
public boolean entersBattlefield(Ability source, Game game, Zone fromZone, boolean fireEvent) { public boolean entersBattlefield(Ability source, Game game, Zone fromZone, boolean fireEvent) {
controlledFromStartOfControllerTurn = false; controlledFromStartOfControllerTurn = false;
if (this.isFaceDown(game)) { // ?? add morphed/manifested here ???
BecomesFaceDownCreatureEffect.FaceDownType faceDownType = BecomesFaceDownCreatureEffect.findFaceDownType(game, this);
if (faceDownType != null) {
// remove some attributes here, because first apply effects comes later otherwise abilities (e.g. color related) will unintended trigger // remove some attributes here, because first apply effects comes later otherwise abilities (e.g. color related) will unintended trigger
MorphAbility.setPermanentToFaceDownCreature(this, this, game); BecomesFaceDownCreatureEffect.makeFaceDownObject(game, null, this, faceDownType, null);
} }
// own etb event
if (game.replaceEvent(new EntersTheBattlefieldEvent(this, source, getControllerId(), fromZone, EnterEventType.SELF))) { if (game.replaceEvent(new EntersTheBattlefieldEvent(this, source, getControllerId(), fromZone, EnterEventType.SELF))) {
return false; return false;
} }
// normal etb event
EntersTheBattlefieldEvent event = new EntersTheBattlefieldEvent(this, source, getControllerId(), fromZone); EntersTheBattlefieldEvent event = new EntersTheBattlefieldEvent(this, source, getControllerId(), fromZone);
if (game.replaceEvent(event)) { if (game.replaceEvent(event)) {
return false; return false;
} }
if (this.isPlaneswalker(game)) { if (this.isPlaneswalker(game)) {
int loyalty; int loyalty;
if (this.getStartingLoyalty() == -2) { if (this.getStartingLoyalty() == -2) {

View file

@ -22,10 +22,11 @@ public class PermanentToken extends PermanentImpl {
// non-modifyable container with token characteristics // non-modifyable container with token characteristics
// this PermanentToken resets to it on each game cycle // this PermanentToken resets to it on each game cycle
// TODO: see PermanentCard.card for usage research and fixes
protected Token token; protected Token token;
public PermanentToken(Token token, UUID controllerId, Game game) { public PermanentToken(Token token, UUID controllerId, Game game) {
super(controllerId, controllerId, token.getName()); super(controllerId, controllerId, token.getName()); // random id
this.token = token.copy(); this.token = token.copy();
this.token.getAbilities().newOriginalId(); // neccessary if token has ability like DevourAbility() this.token.getAbilities().newOriginalId(); // neccessary if token has ability like DevourAbility()
this.token.getAbilities().setSourceId(objectId); this.token.getAbilities().setSourceId(objectId);
@ -76,11 +77,6 @@ public class PermanentToken extends PermanentImpl {
} }
} }
@Override
public String toString() {
return String.format("%s - %s", getExpansionSetCode(), getName());
}
private void copyFromToken(Token token, Game game, boolean reset) { private void copyFromToken(Token token, Game game, boolean reset) {
// modify all attributes permanently (without game usage) // modify all attributes permanently (without game usage)
this.name = token.getName(); this.name = token.getName();
@ -119,7 +115,7 @@ public class PermanentToken extends PermanentImpl {
} }
@Override @Override
public MageObject getBasicMageObject(Game game) { public MageObject getBasicMageObject() {
return token; return token;
} }

View file

@ -21,6 +21,7 @@ import mage.game.permanent.PermanentToken;
import mage.game.permanent.token.custom.CreatureToken; import mage.game.permanent.token.custom.CreatureToken;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import org.apache.log4j.Logger;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.*; import java.util.*;
@ -30,6 +31,8 @@ import java.util.*;
*/ */
public abstract class TokenImpl extends MageObjectImpl implements Token { public abstract class TokenImpl extends MageObjectImpl implements Token {
private static final Logger logger = Logger.getLogger(MageObjectImpl.class);
protected String description; protected String description;
private final ArrayList<UUID> lastAddedTokenIds = new ArrayList<>(); private final ArrayList<UUID> lastAddedTokenIds = new ArrayList<>();
@ -142,6 +145,14 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
return putOntoBattlefield(amount, game, source, controllerId, tapped, attacking, null); return putOntoBattlefield(amount, game, source, controllerId, tapped, attacking, null);
} }
/**
* Find random token image from a database
*
* @param token
* @param game
* @param sourceId
* @return
*/
public static TokenInfo generateTokenInfo(TokenImpl token, Game game, UUID sourceId) { public static TokenInfo generateTokenInfo(TokenImpl token, Game game, UUID sourceId) {
// Choose a token image by priority: // Choose a token image by priority:
// - use source's set code // - use source's set code
@ -190,11 +201,10 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
// TODO: return default creature token image // TODO: return default creature token image
} }
// TODO: implement Copy image // unknown tokens:
// TODO: implement Manifest image // - without official token sets;
// TODO: implement Morph image // - un-implemented token set (must add missing images to tokens database);
// - another use cases with unknown tokens
// unknown tokens
return new TokenInfo(TokenType.TOKEN, "Unknown", TokenRepository.XMAGE_TOKENS_SET_CODE, 0); return new TokenInfo(TokenType.TOKEN, "Unknown", TokenRepository.XMAGE_TOKENS_SET_CODE, 0);
} }

View file

@ -89,7 +89,7 @@ public class Spell extends StackObjectImpl implements Card {
} }
this.card = affectedCard; this.card = affectedCard;
this.manaCost = this.card.getManaCost().copy(); this.manaCost = affectedCard.getManaCost().copy();
this.color = affectedCard.getColor(null).copy(); this.color = affectedCard.getColor(null).copy();
this.frameColor = affectedCard.getFrameColor(null).copy(); this.frameColor = affectedCard.getFrameColor(null).copy();
this.frameStyle = affectedCard.getFrameStyle(); this.frameStyle = affectedCard.getFrameStyle();
@ -100,7 +100,11 @@ public class Spell extends StackObjectImpl implements Card {
this.ability = ability; this.ability = ability;
this.ability.setControllerId(controllerId); this.ability.setControllerId(controllerId);
if (ability.getSpellAbilityCastMode() == SpellAbilityCastMode.MORPH){ if (ability.getSpellAbilityCastMode() == SpellAbilityCastMode.MORPH
|| ability.getSpellAbilityCastMode() == SpellAbilityCastMode.MEGAMORPH){
// TODO: need research:
// - why it use game param for color and subtype (possible bug?)
// - is it possible to use BecomesFaceDownCreatureEffect.makeFaceDownObject or like that?
this.faceDown = true; this.faceDown = true;
this.getColor(game).setColor(null); this.getColor(game).setColor(null);
game.getState().getCreateMageObjectAttribute(this.getCard(), game).getSubtype().clear(); game.getState().getCreateMageObjectAttribute(this.getCard(), game).getSubtype().clear();
@ -238,6 +242,16 @@ public class Spell extends StackObjectImpl implements Card {
throw new IllegalStateException("Wrong code usage: you can't change card number for the spell"); throw new IllegalStateException("Wrong code usage: you can't change card number for the spell");
} }
@Override
public String getImageFileName() {
return card.getImageFileName();
}
@Override
public void setImageFileName(String imageFile) {
throw new IllegalStateException("Wrong code usage: you can't change image file name for the spell");
}
@Override @Override
public Integer getImageNumber() { public Integer getImageNumber() {
return card.getImageNumber(); return card.getImageNumber();
@ -509,6 +523,11 @@ public class Spell extends StackObjectImpl implements Card {
return this.controllerId; return this.controllerId;
} }
@Override
public UUID getControllerOrOwnerId() {
return getControllerId();
}
@Override @Override
public String getName() { public String getName() {
return card.getName(); return card.getName();
@ -546,6 +565,11 @@ public class Spell extends StackObjectImpl implements Card {
return card.getRarity(); return card.getRarity();
} }
@Override
public void setRarity(Rarity rarity) {
throw new IllegalArgumentException("Un-supported operation: " + this, new Throwable());
}
@Override @Override
public List<CardType> getCardType(Game game) { public List<CardType> getCardType(Game game) {
if (faceDown) { if (faceDown) {
@ -933,6 +957,11 @@ public class Spell extends StackObjectImpl implements Card {
return card.getUsesVariousArt(); return card.getUsesVariousArt();
} }
@Override
public void setUsesVariousArt(boolean usesVariousArt) {
card.setUsesVariousArt(usesVariousArt);
}
@Override @Override
public List<Mana> getMana() { public List<Mana> getMana() {
return card.getMana(); return card.getMana();
@ -1104,8 +1133,8 @@ public class Spell extends StackObjectImpl implements Card {
} }
@Override @Override
public void checkForCountersToAdd(Permanent permanent, Ability source, Game game) { public void applyEnterWithCounters(Permanent permanent, Ability source, Game game) {
card.checkForCountersToAdd(permanent, source, game); card.applyEnterWithCounters(permanent, source, game);
} }
@Override @Override

View file

@ -147,6 +147,16 @@ public class StackAbility extends StackObjectImpl implements Ability {
throw new IllegalStateException("Wrong code usage: you can't change card number for the stack ability"); throw new IllegalStateException("Wrong code usage: you can't change card number for the stack ability");
} }
@Override
public String getImageFileName() {
return "";
}
@Override
public void setImageFileName(String imageFile) {
throw new IllegalStateException("Wrong code usage: you can't change image file name for the stack ability");
}
@Override @Override
public Integer getImageNumber() { public Integer getImageNumber() {
return 0; return 0;
@ -301,6 +311,11 @@ public class StackAbility extends StackObjectImpl implements Ability {
return this.controllerId; return this.controllerId;
} }
@Override
public UUID getControllerOrOwnerId() {
return getControllerId();
}
@Override @Override
public Costs<Cost> getCosts() { public Costs<Cost> getCosts() {
return emptyCosts; return emptyCosts;

View file

@ -842,13 +842,13 @@ public abstract class PlayerImpl implements Player, Serializable {
private boolean doDiscard(Card card, Ability source, Game game, boolean payForCost, boolean fireFinalEvent) { private boolean doDiscard(Card card, Ability source, Game game, boolean payForCost, boolean fireFinalEvent) {
//20100716 - 701.7 //20100716 - 701.7
/* 701.7. Discard # /* 701.7. Discard #
701.7a To discard a card, move it from its owner’s hand to that player’s graveyard. 701.7a To discard a card, move it from its owner's hand to that player's graveyard.
701.7b By default, effects that cause a player to discard a card allow the affected 701.7b By default, effects that cause a player to discard a card allow the affected
player to choose which card to discard. Some effects, however, require a random player to choose which card to discard. Some effects, however, require a random
discard or allow another player to choose which card is discarded. discard or allow another player to choose which card is discarded.
701.7c If a card is discarded, but an effect causes it to be put into a hidden zone 701.7c If a card is discarded, but an effect causes it to be put into a hidden zone
instead of into its owner’s graveyard without being revealed, all values of that instead of into its owner's graveyard without being revealed, all values of that
card’s characteristics are considered to be undefined. card's characteristics are considered to be undefined.
TODO: TODO:
If a card is discarded this way to pay a cost that specifies a characteristic If a card is discarded this way to pay a cost that specifies a characteristic
about the discarded card, that cost payment is illegal; the game returns to about the discarded card, that cost payment is illegal; the game returns to

Some files were not shown because too many files have changed in this diff Show more