Merge branch 'magefree:master' into master

This commit is contained in:
Grath 2023-03-12 22:28:27 -04:00 committed by GitHub
commit 4f3d99d5d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 428 additions and 303 deletions

View file

@ -215,6 +215,10 @@
</descriptors>
</configuration>
</plugin>
<plugin>
<groupId>com.ruleoftech</groupId>
<artifactId>markdown-page-generator-plugin</artifactId>
</plugin>
</plugins>
<finalName>mage-client</finalName>

View file

@ -1,5 +0,0 @@
use .exe or one of the scripts:
startClient.bat - for Windows
startClientWin7.bat - for Windows 7
startClient.sh - for Linux
startClient.command - for MacOS

View file

@ -1,60 +0,0 @@
XMage - is an acronym for Extended - Magic, Another Game Engine
XMage is a client/server implementation of a popular CCG without the collecting part.
The server hosts games and enforces the rules. The client creates or joins games,
displays the current state of a game in progress and sends user events to the server.
You will need to have the Java Runtime Environment Version 7 or greater.
You can download this from: http://java.com/
-----------------------------------------------------------------------------------
Installing and running XMage
You will need to download both the client and the server applications. These can be
obtained from HTTP://XMage.de.
Extact the client and the server to separate folders.
To play a game you can either connect to a server or start your own server. To
connect to a server you will need to know the server name or IP address and the port.
To start a server run the startServer.bat command. If you want to use a different
port or change the timeout setting then modify the config.xml file in the
config folder.
To launch the client run the startClient.bat command. Click on the connect button on
the toolbar and enter the server name/IP address and port. Then click on the tables
button. This will bring up a list of active and completed games. Click on join to
join an existing game that hasn't started yet or you can create a new table by
clicking the New button.
-----------------------------------------------------------------------------------
Playing a game
Playing a game should be fairly self evident. Your hand is displayed at the bottom
of the screen. The battlefield is the central area. Click on cards in your hand to
play them. Click on cards in the battlefield to activate abilities. A popup menu
will be presented if you have more than one choice. To pass priority for the turn
hold down the ctrl key while clicking done. You will still receive priority if
your opponent casts a spell or activates an ability. Target cards by clicking on
them. To target a player click on the player name. You can see the cards in any
graveyard by clicking on the graveyard count.
-----------------------------------------------------------------------------------
Deck editor
A simple deck editor is available by clicking on the Deck Editor button on the
toolbar. All the available cards are displayed in the top section. Your deck
and sideboard are displayed at the bottom. To add a card to your deck double
click on the card in the top section. To remove it from your deck double click
on the card in the deck area. The sideboard section is not ready yet (don't
worry it's coming soon).
You can save or load a deck using the Save or Load buttons.
-----------------------------------------------------------------------------------
Notes
XMage is still in the testing phase so there can be bugs and/or
missing functionality. Please be patient. If you notice anything or want to
make suggestions please visit our board at
http://www.slightlymagic.net/forum/viewforum.php?f=70
and give us some feedback.

View file

@ -33,6 +33,23 @@
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<filtered>true</filtered>
<directory>../</directory>
<includes>
<include>LICENSE.txt</include>
<include>readme.md</include>
</includes>
<outputDirectory>/</outputDirectory>
</fileSet>
<fileSet>
<filtered>true</filtered>
<directory>${project.build.directory}/docs/</directory>
<includes>
<include>readme.html</include>
</includes>
<outputDirectory>/</outputDirectory>
</fileSet>
<fileSet>
<filtered>false</filtered>
<directory>release/</directory>

View file

@ -33,6 +33,7 @@ import mage.game.mulligan.Mulligan;
import mage.game.mulligan.MulliganType;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.PermanentMeld;
import mage.players.Player;
import mage.players.StubPlayer;
import mage.util.CardUtil;
@ -111,7 +112,7 @@ public class TestCardRenderDialog extends MageDialog {
}
private PermanentView createPermanentCard(Game game, UUID controllerId, String code, String cardNumber, int powerBoosted, int toughnessBoosted, int damage, boolean tapped, boolean transform, List<Ability> extraAbilities) {
CardInfo cardInfo = CardRepository.instance.findCard(code, cardNumber);
CardInfo cardInfo = CardRepository.instance.findCard(code, cardNumber, false);
ExpansionInfo setInfo = ExpansionRepository.instance.getSetByCode(code);
CardSetInfo testSet = new CardSetInfo(cardInfo.getName(), setInfo.getCode(), cardNumber, cardInfo.getRarity(),
new CardGraphicInfo(cardInfo.getFrameStyle(), cardInfo.usesVariousArt()));
@ -126,24 +127,31 @@ public class TestCardRenderDialog extends MageDialog {
extraAbilities.forEach(ability -> permCard.addAbility(ability));
}
PermanentCard perm = new PermanentCard(permCard, controllerId, game);
if (transform) {
// need direct transform call to keep other side info (original)
TransformAbility.transformPermanent(perm, permCard.getSecondCardFace(), game, null);
// meld card must be a special class, see CardUtils.putCardOntoBattlefieldWithEffects
PermanentCard permanent;
if (permCard instanceof MeldCard) {
permanent = new PermanentMeld(permCard, controllerId, game);
} else {
permanent = new PermanentCard(permCard, controllerId, game);
}
if (damage > 0) perm.damage(damage, controllerId, null, game);
if (powerBoosted > 0) perm.getPower().setBoostedValue(powerBoosted);
if (toughnessBoosted > 0) perm.getToughness().setBoostedValue(toughnessBoosted);
perm.removeSummoningSickness();
perm.setTapped(tapped);
PermanentView cardView = new PermanentView(perm, permCard, controllerId, game);
if (transform) {
// need direct transform call to keep other side info (original)
TransformAbility.transformPermanent(permanent, permCard.getSecondCardFace(), game, null);
}
if (damage > 0) permanent.damage(damage, controllerId, null, game);
if (powerBoosted > 0) permanent.getPower().setBoostedValue(powerBoosted);
if (toughnessBoosted > 0) permanent.getToughness().setBoostedValue(toughnessBoosted);
permanent.removeSummoningSickness();
permanent.setTapped(tapped);
PermanentView cardView = new PermanentView(permanent, permCard, controllerId, game);
return cardView;
}
private CardView createFaceDownCard(Game game, UUID controllerId, String code, String cardNumber, boolean isMorphed, boolean isManifested, boolean tapped) {
CardInfo cardInfo = CardRepository.instance.findCard(code, cardNumber);
CardInfo cardInfo = CardRepository.instance.findCard(code, cardNumber, false);
ExpansionInfo setInfo = ExpansionRepository.instance.getSetByCode(code);
CardSetInfo testSet = new CardSetInfo(cardInfo.getName(), setInfo.getCode(), cardNumber, cardInfo.getRarity(),
new CardGraphicInfo(cardInfo.getFrameStyle(), cardInfo.usesVariousArt()));
@ -170,7 +178,7 @@ public class TestCardRenderDialog extends MageDialog {
}
private CardView createHandCard(Game game, UUID controllerId, String code, String cardNumber) {
CardInfo cardInfo = CardRepository.instance.findCard(code, cardNumber);
CardInfo cardInfo = CardRepository.instance.findCard(code, cardNumber, false);
ExpansionInfo setInfo = ExpansionRepository.instance.getSetByCode(code);
CardSetInfo testSet = new CardSetInfo(cardInfo.getName(), setInfo.getCode(), cardNumber, cardInfo.getRarity(),
new CardGraphicInfo(cardInfo.getFrameStyle(), cardInfo.usesVariousArt()));
@ -303,12 +311,31 @@ public class TestCardRenderDialog extends MageDialog {
//test split, transform and mdf in hands
cardViews.add(createHandCard(game, playerYou.getId(), "SOI", "97")); // Accursed Witch
cardViews.add(createHandCard(game, playerYou.getId(), "UMA", "225")); // Fire // Ice
cardViews.add(createHandCard(game, playerYou.getId(), "ELD", "14")); // Giant Killer
cardViews.add(createHandCard(game, playerYou.getId(), "ZNR", "134")); // Akoum Warrior
//cardViews.add(createHandCard(game, playerYou.getId(), "UMA", "225")); // Fire // Ice
//cardViews.add(createHandCard(game, playerYou.getId(), "ELD", "14")); // Giant Killer
//cardViews.add(createHandCard(game, playerYou.getId(), "ZNR", "134")); // Akoum Warrior
//*/
//* //test card icons
/*// test meld cards in hands and battlefield
cardViews.add(createHandCard(game, playerYou.getId(), "EMN", "204")); // Hanweir Battlements
cardViews.add(createHandCard(game, playerYou.getId(), "EMN", "130a")); // Hanweir Garrison
cardViews.add(createHandCard(game, playerYou.getId(), "EMN", "130b")); // Hanweir, the Writhing Township
cardViews.add(createPermanentCard(game, playerYou.getId(), "EMN", "204", 1, 1, 0, false, false, null)); // Hanweir Battlements
cardViews.add(createPermanentCard(game, playerYou.getId(), "EMN", "130a", 1, 1, 0, false, false, null)); // Hanweir Garrison
cardViews.add(createPermanentCard(game, playerYou.getId(), "EMN", "130b", 1, 1, 0, false, false, null)); // Hanweir, the Writhing Township
//*/
// test variant double faced cards (main and second sides must be same pair)
// Jacob Hauken, Inspector -> Hauken's Insight
cardViews.add(createHandCard(game, playerYou.getId(), "VOW", "65"));
cardViews.add(createHandCard(game, playerYou.getId(), "VOW", "320"));
cardViews.add(createHandCard(game, playerYou.getId(), "VOW", "332"));
cardViews.add(createPermanentCard(game, playerYou.getId(), "VOW", "65", 1, 1, 0, false, false, null));
cardViews.add(createPermanentCard(game, playerYou.getId(), "VOW", "320", 1, 1, 0, false, false, null));
cardViews.add(createPermanentCard(game, playerYou.getId(), "VOW", "332", 1, 1, 0, false, false, null));
//*/
/*//test card icons
cardViews.add(createHandCard(game, playerYou.getId(), "POR", "169")); // Grizzly Bears
cardViews.add(createHandCard(game, playerYou.getId(), "DKA", "140")); // Huntmaster of the Fells, transforms
cardViews.add(createPermanentCard(game, playerYou.getId(), "DKA", "140", 3, 3, 1, false, true, additionalIcons)); // Huntmaster of the Fells, transforms

View file

@ -672,7 +672,7 @@ public class MageActionCallback implements ActionCallback {
popupContainer.setVisible(true);
// popup hint mode
Image image = null;
Image image = cardPanel.getImage();
CardView displayCard = cardPanel.getOriginal();
switch (enlargeMode) {
case COPY:
@ -696,12 +696,9 @@ public class MageActionCallback implements ActionCallback {
default:
break;
}
if (image == null) {
image = cardPanel.getImage();
}
// shows the card in the popup Container
displayCardInfo(displayCard, image, (BigCard) cardPreviewPane);
} else {
logger.warn("No Card preview Pane in Mage Frame defined. Card: " + cardView.getName());
}

View file

@ -3,6 +3,7 @@ package mage.client.themes;
import mage.abilities.hint.HintUtils;
import mage.abilities.icon.CardIconColor;
import org.mage.card.arcane.SvgUtils;
import org.mage.plugins.card.images.ImageCache;
import java.awt.*;
@ -347,5 +348,8 @@ public enum ThemeType {
for (CardIconColor cardIconColor : CardIconColor.values()) {
SvgUtils.prepareCss(this.getCardIconsCssFile(cardIconColor), this.getCardIconsCssSettings(cardIconColor), true);
}
// reload card icons and other rendering things from cache - it can depend on current theme
ImageCache.clearCache();
}
}

View file

@ -856,7 +856,7 @@ public abstract class CardPanel extends MagePermanent implements ComponentListen
if (this.guiTransformed) {
// main side -> alternative side
if (this.cardSideOther == null) {
logger.error("no second side for card to transform!");
logger.error("can't find second side to toggle transform from main to second: " + this.getCard().getName());
return;
}
copySelections(this.cardSideMain, this.cardSideOther);
@ -864,6 +864,10 @@ public abstract class CardPanel extends MagePermanent implements ComponentListen
this.getGameCard().setAlternateName(this.cardSideMain.getName());
} else {
// alternative side -> main side
if (this.cardSideOther == null) {
logger.error("can't find second side to toggle transform from second side to main: " + this.getCard().getName());
return;
}
copySelections(this.cardSideOther, this.cardSideMain);
update(this.cardSideMain);
this.getGameCard().setAlternateName(this.cardSideOther.getName());
@ -945,10 +949,16 @@ public abstract class CardPanel extends MagePermanent implements ComponentListen
}
}
// fix other side: if it's a night side permanent then the main side info must be extracted
// fix other side: if it's a night side permanent then the main side info can be extracted from original
if (this.cardSideOther == null || this.cardSideOther.getName().equals(this.cardSideMain.getName())) {
if (this.cardSideMain instanceof PermanentView) {
this.cardSideOther = ((PermanentView) this.cardSideMain).getOriginal();
if ((this.cardSideMain instanceof PermanentView)) {
// some "transformed" cards don't have info about main side
// (example: melded card have two main sides/cards),
// so it must be ignored until multiple hints implement like mtga
CardView original = ((PermanentView) this.cardSideMain).getOriginal();
if (original != null && !original.getName().equals(this.getName())) {
this.cardSideOther = original;
}
}
}
}

View file

@ -449,18 +449,24 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
// main side
allCardsUrls.add(url);
// second side (xmage's set doesn't have info about it, so generate it here)
// second side
// xmage doesn't search night cards by default, so add it and other types manually
if (card.isDoubleFaced()) {
if (card.getSecondSideName() == null || card.getSecondSideName().trim().isEmpty()) {
throw new IllegalStateException("Second side card can't have empty name.");
}
CardInfo secondSideCard = CardRepository.instance.findCardWPreferredSet(card.getSecondSideName(), card.getSetCode());
CardInfo secondSideCard = CardRepository.instance.findCardWithPreferredSetAndNumber(card.getSecondSideName(), card.getSetCode(), card.getCardNumber());
if (secondSideCard == null) {
throw new IllegalStateException("Can''t find second side card in database: " + card.getSecondSideName());
}
url = new CardDownloadData(card.getSecondSideName(), card.getSetCode(), secondSideCard.getCardNumber(), card.usesVariousArt(), 0, "", "", false, card.isDoubleFaced(), true);
url = new CardDownloadData(
card.getSecondSideName(),
card.getSetCode(),
secondSideCard.getCardNumber(),
card.usesVariousArt(),
0, "", "", false, card.isDoubleFaced(), true);
url.setType2(isType2);
allCardsUrls.add(url);
}
@ -479,6 +485,26 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
cardDownloadData.setType2(isType2);
allCardsUrls.add(cardDownloadData);
}
if (card.getMeldsToCardName() != null) {
if (card.getMeldsToCardName().trim().isEmpty()) {
throw new IllegalStateException("MeldsToCardName can't be empty in " + card.getName());
}
CardInfo meldsToCard = CardRepository.instance.findCardWithPreferredSetAndNumber(card.getMeldsToCardName(), card.getSetCode(), card.getCardNumber());
if (meldsToCard == null) {
throw new IllegalStateException("Can''t find meldsToCard in database: " + card.getMeldsToCardName());
}
// meld cards are normal cards from the set, so no needs to set two faces/sides here
url = new CardDownloadData(
card.getMeldsToCardName(),
card.getSetCode(),
meldsToCard.getCardNumber(),
card.usesVariousArt(),
0, "", "", false, false, false);
url.setType2(isType2);
allCardsUrls.add(url);
}
if (card.isModalDoubleFacesCard()) {
if (card.getModalDoubleFacesSecondSideName() == null || card.getModalDoubleFacesSecondSideName().trim().isEmpty()) {
throw new IllegalStateException("MDF card can't have empty name.");

View file

@ -519,6 +519,13 @@ public class CardView extends SimpleCardView {
this.alternateName = mdfCard.getRightHalfCard().getName();
}
Card meldsToCard = card.getMeldsToCard();
if (meldsToCard != null) {
this.transformable = true; // enable GUI day/night button
this.secondCardFace = new CardView(meldsToCard, game);
this.alternateName = meldsToCard.getName();
}
if (card instanceof Spell) {
this.mageObjectType = MageObjectType.SPELL;
Spell spell = (Spell) card;

View file

@ -62,6 +62,10 @@
</descriptors>
</configuration>
</plugin>
<plugin>
<groupId>com.ruleoftech</groupId>
<artifactId>markdown-page-generator-plugin</artifactId>
</plugin>
</plugins>
<finalName>mage-serverconsole</finalName>

View file

@ -24,6 +24,23 @@
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<filtered>true</filtered>
<directory>../</directory>
<includes>
<include>LICENSE.txt</include>
<include>readme.md</include>
</includes>
<outputDirectory>/</outputDirectory>
</fileSet>
<fileSet>
<filtered>true</filtered>
<directory>${project.build.directory}/docs/</directory>
<includes>
<include>readme.html</include>
</includes>
<outputDirectory>/</outputDirectory>
</fileSet>
<fileSet>
<filtered>true</filtered>
<directory>release/</directory>

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog" prefer="system">
<system systemId="file:/C:/Projects/Mage/Mage.Server/src/mage/server/util/config.xml" uri="xml-resources/jaxb/ServerConfig/config.xml"/>
<system systemId="file:/C:/Projects/Mage/Mage.Server/src/mage/server/util/ConfigSettings.xsd" uri="xml-resources/jaxb/Config/ConfigSettings.xsd"/>
<system systemId="file:/C:/Projects/Mage/Mage.Server/src/mage/server/util/Config.xsd" uri="xml-resources/jaxb/Config/Config.xsd"/>
</catalog>

View file

@ -1,25 +0,0 @@
Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of the
authors and should not be interpreted as representing official policies, either expressed
or implied, of BetaSteward_at_googlemail.com.

View file

@ -352,6 +352,10 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.ruleoftech</groupId>
<artifactId>markdown-page-generator-plugin</artifactId>
</plugin>
</plugins>
<finalName>mage-server</finalName>

View file

@ -1,4 +0,0 @@
use .exe or one of the scripts:
startServer.bat - for Windows
startServer.sh - for Linux
startServer.command - for MacOS

View file

@ -1,25 +0,0 @@
Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of the
authors and should not be interpreted as representing official policies, either expressed
or implied, of BetaSteward_at_googlemail.com.

View file

@ -1,60 +0,0 @@
XMage - is an acronym for Extended - Magic, Another Game Engine
XMage is a client/server implementation of a popular CCG without the collecting part.
The server hosts games and enforces the rules. The client creates or joins games,
displays the current state of a game in progress and sends user events to the server.
You will need to have the Java Runtime Environment Version 7 or greater.
You can download this from: http://java.com/
-----------------------------------------------------------------------------------
Installing and running XMage
You will need to download both the client and the server applications. These can be
obtained from HTTP://XMage.de.
Extact the client and the server to separate folders.
To play a game you can either connect to a server or start your own server. To
connect to a server you will need to know the server name or IP address and the port.
To start a server run the startServer.bat command. If you want to use a different
port or change the timeout setting then modify the config.xml file in the
config folder.
To launch the client run the startClient.bat command. Click on the connect button on
the toolbar and enter the server name/IP address and port. Then click on the tables
button. This will bring up a list of active and completed games. Click on join to
join an existing game that hasn't started yet or you can create a new table by
clicking the New button.
-----------------------------------------------------------------------------------
Playing a game
Playing a game should be fairly self evident. Your hand is displayed at the bottom
of the screen. The battlefield is the central area. Click on cards in your hand to
play them. Click on cards in the battlefield to activate abilities. A popup menu
will be presented if you have more than one choice. To pass priority for the turn
hold down the ctrl key while clicking done. You will still receive priority if
your opponent casts a spell or activates an ability. Target cards by clicking on
them. To target a player click on the player name. You can see the cards in any
graveyard by clicking on the graveyard count.
-----------------------------------------------------------------------------------
Deck editor
A simple deck editor is available by clicking on the Deck Editor button on the
toolbar. All the available cards are displayed in the top section. Your deck
and sideboard are displayed at the bottom. To add a card to your deck double
click on the card in the top section. To remove it from your deck double click
on the card in the deck area. The sideboard section is not ready yet (don't
worry it's coming soon).
You can save or load a deck using the Save or Load buttons.
-----------------------------------------------------------------------------------
Notes
XMage is still in the testing phase so there can be bugs and/or
missing functionality. Please be patient. If you notice anything or want to
make suggestions please visit our board at
http://www.slightlymagic.net/forum/viewforum.php?f=70
and give us some feedback.

View file

@ -1,6 +0,0 @@
HotKeys: Alt+E - Enlarge card image
Wheel zoom in/out - Enlarge card image
F4 - end current turn, response to stack
F9 - skip all opponents' turns, no response to stack
Welcome! You are playing Mage version 1.4.9
Contact us on www.slightlymagic.net

View file

@ -50,6 +50,23 @@
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<filtered>true</filtered>
<directory>../</directory>
<includes>
<include>LICENSE.txt</include>
<include>readme.md</include>
</includes>
<outputDirectory>/</outputDirectory>
</fileSet>
<fileSet>
<filtered>true</filtered>
<directory>${project.build.directory}/docs/</directory>
<includes>
<include>readme.html</include>
</includes>
<outputDirectory>/</outputDirectory>
</fileSet>
<fileSet>
<filtered>true</filtered>
<directory>release/</directory>

View file

@ -47,6 +47,7 @@ public final class ArgothSanctumOfNature extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.LAND}, "");
this.meldsWithClazz = mage.cards.t.TitaniaVoiceOfGaea.class;
this.meldsToClazz = mage.cards.h.HanweirTheWrithingTownship.class;
// Argoth, Sanctum of Nature enters the battlefield tapped unless you control a legendary green creature.
this.addAbility(new EntersBattlefieldAbility(

View file

@ -89,8 +89,7 @@ class AtraxaGrandUnifierEffect extends OneShotEffect {
TargetCard target = new AtraxaGrandUnifierTarget();
player.choose(outcome, cards, target, game);
Cards toHand = new CardsImpl(target.getTargets());
player.revealCards(source, toHand, game);
player.moveCards(toHand, Zone.HAND, source, game);
player.moveCardsToHandWithInfo(toHand, source, game, true);
cards.retainZone(Zone.LIBRARY, game);
player.putCardsOnBottomOfLibrary(cards, game, source, false);
return true;

View file

@ -84,7 +84,7 @@ class BountyOfSkemfarEffect extends OneShotEffect {
player.choose(outcome, cards, target, game);
Card elf = cards.get(target.getFirstTarget(), game);
if (elf != null) {
player.moveCards(elf, Zone.HAND, source, game);
player.moveCardToHandWithInfo(elf, source, game, true);
}
cards.removeIf(uuid -> game.getState().getZone(uuid) != Zone.LIBRARY);
player.putCardsOnBottomOfLibrary(cards, game, source, false);

View file

@ -42,7 +42,9 @@ public final class BrunaTheFadingLight extends CardImpl {
this.subtype.add(SubType.ANGEL, SubType.HORROR);
this.power = new MageInt(5);
this.toughness = new MageInt(7);
this.meldsWithClazz = mage.cards.g.GiselaTheBrokenBlade.class;
this.meldsToClazz = mage.cards.b.BriselaVoiceOfNightmares.class;
// When you cast Bruna, the Fading Light, you may return target Angel or Human creature card from your graveyard to the battlefield.
Effect effect = new ReturnFromGraveyardToBattlefieldTargetEffect();

View file

@ -30,7 +30,7 @@ public final class GarrukRelentless extends CardImpl {
this.addSuperType(SuperType.LEGENDARY);
this.subtype.add(SubType.GARRUK);
this.secondSideCardClazz = GarrukTheVeilCursed.class;
this.secondSideCardClazz = mage.cards.g.GarrukTheVeilCursed.class;
this.setStartingLoyalty(3);

View file

@ -22,7 +22,7 @@ public final class GatstafArsonists extends CardImpl {
this.power = new MageInt(5);
this.toughness = new MageInt(4);
this.secondSideCardClazz = GatstafRavagers.class;
this.secondSideCardClazz = mage.cards.g.GatstafRavagers.class;
// At the beginning of each upkeep, if no spells were cast last turn, transform Gatstaf Arsonists.
this.addAbility(new TransformAbility());

View file

@ -20,7 +20,7 @@ public final class GatstafShepherd extends CardImpl {
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.WEREWOLF);
this.secondSideCardClazz = GatstafHowler.class;
this.secondSideCardClazz = mage.cards.g.GatstafHowler.class;
this.power = new MageInt(2);
this.toughness = new MageInt(2);

View file

@ -31,7 +31,9 @@ public final class GiselaTheBrokenBlade extends CardImpl {
this.subtype.add(SubType.HORROR);
this.power = new MageInt(4);
this.toughness = new MageInt(3);
this.meldsWithClazz = mage.cards.b.BrunaTheFadingLight.class;
this.meldsToClazz = mage.cards.b.BriselaVoiceOfNightmares.class;
// Flying
this.addAbility(FlyingAbility.getInstance());

View file

@ -26,7 +26,9 @@ public final class GrafRats extends CardImpl {
this.subtype.add(SubType.RAT);
this.power = new MageInt(2);
this.toughness = new MageInt(1);
this.meldsWithClazz = mage.cards.m.MidnightScavengers.class;
this.meldsToClazz = mage.cards.c.ChitteringHost.class;
// At the beginning of combat on your turn, if you both own and control Graf Rats and a creature named Midnight Scavengers, exile them, then meld them into Chittering Host.
this.addAbility(new ConditionalInterveningIfTriggeredAbility(

View file

@ -47,7 +47,7 @@ public final class GrimlockDinobotLeader extends CardImpl{
this.power = new MageInt(4);
this.toughness = new MageInt(4);
this.secondSideCardClazz = GrimlockFerociousKing.class;
this.secondSideCardClazz = mage.cards.g.GrimlockFerociousKing.class;
// Dinosaurs, Vehicles and other Transformers creatures you control get +2/+0.
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostControlledEffect(2, 0, Duration.WhileOnBattlefield, filter, false)));

View file

@ -32,7 +32,7 @@ public final class GrizzledAngler extends CardImpl {
this.power = new MageInt(2);
this.toughness = new MageInt(3);
this.secondSideCardClazz = GrislyAnglerfish.class;
this.secondSideCardClazz = mage.cards.g.GrislyAnglerfish.class;
// {T}: Put the top two cards of your library into your graveyard. Then if there is a colorless creature card in your graveyard, transform Grizzled Angler.
this.addAbility(new TransformAbility());

View file

@ -29,7 +29,9 @@ public final class HanweirBattlements extends CardImpl {
public HanweirBattlements(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.LAND}, "");
this.meldsWithClazz = mage.cards.h.HanweirGarrison.class;
this.meldsToClazz = mage.cards.h.HanweirTheWrithingTownship.class;
// {T}: Add {C}.
this.addAbility(new ColorlessManaAbility());

View file

@ -26,7 +26,9 @@ public final class HanweirGarrison extends CardImpl {
this.subtype.add(SubType.SOLDIER);
this.power = new MageInt(2);
this.toughness = new MageInt(3);
this.meldsWithClazz = mage.cards.h.HanweirBattlements.class;
this.meldsToClazz = mage.cards.h.HanweirTheWrithingTownship.class;
// Whenever Hanweir Garrison attacks, create two 1/1 red Human creature tokens tapped and attacking.
this.addAbility(new AttacksTriggeredAbility(new CreateTokenEffect(new RedHumanToken(), 2, true, true), false));

View file

@ -20,7 +20,7 @@ public final class HinterlandHermit extends CardImpl {
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.WEREWOLF);
this.secondSideCardClazz = HinterlandScourge.class;
this.secondSideCardClazz = mage.cards.h.HinterlandScourge.class;
this.power = new MageInt(2);
this.toughness = new MageInt(1);

View file

@ -94,7 +94,7 @@ class HurkylMasterWizardEffect extends OneShotEffect {
TargetCard target = new HurkylMasterWizardTarget(source, game);
player.choose(outcome, cards, target, game);
Cards toHand = new CardsImpl(target.getTargets());
player.moveCards(toHand, Zone.HAND, source, game);
player.moveCardsToHandWithInfo(toHand, source, game, true);
cards.retainZone(Zone.LIBRARY, game);
player.putCardsOnBottomOfLibrary(cards, game, source, false);
return true;

View file

@ -78,7 +78,7 @@ class InscribedTabletEffect extends OneShotEffect {
Card land = game.getCard(target.getFirstTarget());
if (land != null) {
cards.remove(land);
landToHand = controller.moveCards(land, Zone.HAND, source, game);
landToHand = controller.moveCardToHandWithInfo(land, source, game, true);
}
}
controller.putCardsOnBottomOfLibrary(cards, game, source, false);

View file

@ -37,7 +37,9 @@ public final class MidnightScavengers extends CardImpl {
this.subtype.add(SubType.ROGUE);
this.power = new MageInt(3);
this.toughness = new MageInt(3);
this.meldsWithClazz = mage.cards.g.GrafRats.class;
this.meldsToClazz = mage.cards.c.ChitteringHost.class;
// When Midnight Scavengers enters the battlefield, you may return target creature card with converted mana cost 3 or less from your graveyard to your hand.
Ability ability = new EntersBattlefieldTriggeredAbility(new ReturnFromGraveyardToHandTargetEffect(), true);

View file

@ -38,7 +38,9 @@ public final class MishraClaimedByGix extends CardImpl {
this.subtype.add(SubType.ARTIFICER);
this.power = new MageInt(3);
this.toughness = new MageInt(5);
this.meldsWithClazz = mage.cards.p.PhyrexianDragonEngine.class;
this.meldsToClazz = mage.cards.m.MishraLostToPhyrexia.class;
// Whenever you attack, each opponent loses X life and you gain X life, where X is the number of attacking creatures. If Mishra, Claimed by Gix and a creature named Phyrexian Dragon Engine are attacking, and you both own and control them, exile them, then meld them into Mishra, Lost to Phyrexia. It enters the battlefield tapped and attacking.
Ability ability = new AttacksWithCreaturesTriggeredAbility(

View file

@ -86,10 +86,7 @@ class NivMizzetRebornEffect extends OneShotEffect {
TargetCard target = new NivMizzetRebornTarget();
player.choose(outcome, cards, target, game);
Cards toHand = new CardsImpl(target.getTargets());
player.moveCards(toHand, Zone.HAND, source, game);
game.informPlayers(player.getLogName() + " moves " + CardUtil.concatWithAnd(
toHand.getCards(game).stream().map(MageObject::getName).collect(Collectors.toList())
) + " to hand");
player.moveCardsToHandWithInfo(toHand, source, game, true);
cards.retainZone(Zone.LIBRARY, game);
player.putCardsOnBottomOfLibrary(cards, game, source, false);
return true;

View file

@ -31,7 +31,9 @@ public final class PhyrexianDragonEngine extends CardImpl {
this.subtype.add(SubType.DRAGON);
this.power = new MageInt(2);
this.toughness = new MageInt(2);
this.meldsWithClazz = mage.cards.m.MishraClaimedByGix.class;
this.meldsToClazz = mage.cards.m.MishraLostToPhyrexia.class;
// Double strike
this.addAbility(DoubleStrikeAbility.getInstance());

View file

@ -1,15 +1,12 @@
package mage.cards.s;
import java.util.UUID;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.target.common.TargetCreaturePermanent;
import mage.target.common.TargetPlayerOrPlaneswalker;
import mage.target.targetpointer.SecondTargetPointer;
/**
*
@ -19,15 +16,10 @@ public final class ShowerOfSparks extends CardImpl {
public ShowerOfSparks(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}");
// Shower of Sparks deals 1 damage to target creature and 1 damage to target player.
this.getSpellAbility().addEffect(new DamageTargetEffect(1));
// Shower of sparks deals 1 damage to target creature and 1 damage to target player.
this.getSpellAbility().addEffect(new DamageTargetEffect(1, true, "target creature and 1 damage to target player or planeswalker"));
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
Effect effect = new DamageTargetEffect(1);
effect.setTargetPointer(new SecondTargetPointer());
effect.setText("and 1 damage to target player");
this.getSpellAbility().addEffect(effect);
this.getSpellAbility().addTarget(new TargetPlayerOrPlaneswalker());
}

View file

@ -95,7 +95,7 @@ class TajuruParagonEffect extends OneShotEffect {
player.choose(outcome, cards, target, game);
Card card = game.getCard(target.getFirstTarget());
if (card != null) {
player.moveCards(card, Zone.HAND, source, game);
player.moveCardToHandWithInfo(card, source, game, true);
cards.remove(card);
}
}

View file

@ -31,6 +31,7 @@ public final class TheMightstoneAndWeakstone extends CardImpl {
this.subtype.add(SubType.POWERSTONE);
this.meldsWithClazz = mage.cards.u.UrzaLordProtector.class;
this.meldsToClazz = mage.cards.u.UrzaPlaneswalker.class;
// When The Mightstone and Weakstone enters the battlefield, choose one --
// * Draw two cards.

View file

@ -39,7 +39,9 @@ public final class TitaniaVoiceOfGaea extends CardImpl {
this.subtype.add(SubType.ELEMENTAL);
this.power = new MageInt(3);
this.toughness = new MageInt(4);
this.meldsWithClazz = mage.cards.a.ArgothSanctumOfNature.class;
this.meldsToClazz = mage.cards.t.TitaniaGaeaIncarnate.class;
// Reach
this.addAbility(ReachAbility.getInstance());

View file

@ -46,7 +46,9 @@ public final class UrzaLordProtector extends CardImpl {
this.subtype.add(SubType.ARTIFICER);
this.power = new MageInt(2);
this.toughness = new MageInt(4);
this.meldsWithClazz = mage.cards.t.TheMightstoneAndWeakstone.class;
this.meldsToClazz = mage.cards.u.UrzaPlaneswalker.class;
// Artifact, instant, and sorcery spells you cast cost {1} less to cast.
this.addAbility(new SimpleStaticAbility(new SpellsCostReductionControllerEffect(filter, 1)));

View file

@ -358,7 +358,7 @@ public final class Battlebond extends ExpansionSet {
//Check if the pack already contains a partner pair
if (partnerAllowed) {
//Added card always replaces an uncommon card
Card card = CardRepository.instance.findCardWPreferredSet(partnerName, sourceCard.getExpansionSetCode()).getCard();
Card card = CardRepository.instance.findCardWithPreferredSetAndNumber(partnerName, sourceCard.getExpansionSetCode(), null).getCard();
if (i < max) {
booster.add(card);
} else {

View file

@ -3482,7 +3482,7 @@ public class MysteryBooster extends ExpansionSet {
private void populateSlot(int slotNumber, List<String> cardNames) {
final List<CardInfo> cardInfoList = this.possibleCardsPerBoosterSlot.get(slotNumber);
for (String name : cardNames) {
final CardInfo cardWithGivenName = CardRepository.instance.findCardWPreferredSet(name, this.code);
final CardInfo cardWithGivenName = CardRepository.instance.findCardWithPreferredSetAndNumber(name, this.code, null);
cardInfoList.add(cardWithGivenName);
}
}

View file

@ -3813,6 +3813,11 @@ public class TestPlayer implements Player {
return computerPlayer.moveCardToHandWithInfo(card, source, game, withName);
}
@Override
public boolean moveCardsToHandWithInfo(Cards cards, Ability source, Game game, boolean withName) {
return computerPlayer.moveCardsToHandWithInfo(cards, source, game, withName);
}
@Override
public boolean moveCardsToExile(Card card, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) {
return computerPlayer.moveCardsToExile(card, source, game, withName, exileId, exileZoneName);

View file

@ -1204,6 +1204,11 @@ public class PlayerStub implements Player {
return false;
}
@Override
public boolean moveCardsToHandWithInfo(Cards cards, Ability source, Game game, boolean withName) {
return false;
}
@Override
public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, Ability source, Game game, Zone fromZone, boolean withName) {
return false;

View file

@ -2,7 +2,9 @@ package org.mage.test.utils;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.cards.repository.CardScanner;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
@ -10,17 +12,22 @@ import java.util.List;
/**
* Testing of CardRepository functionality.
*
* @author Alex-Vasile
* @author Alex-Vasile, JayDi85
*/
public class CardRepositoryTest {
@Before
public void setUp() {
CardScanner.scan();
}
/**
* Test CardRepository.findCards for the difficult cases when provided with the full card name.
*
* <p>
* CardRepository.findCards is used only for testing purposes.
*/
@Test
public void testFindSplitCardsByFullName() {
public void test_FindSplitCardsByFullName() {
// Modal double-faced
assertFindCard("Malakir Rebirth // Malakir Mire");
// Transform double-faced
@ -35,11 +42,11 @@ public class CardRepositoryTest {
/**
* Test CardRepository.findCards for the difficult cases.
*
* <p>
* CardRepository.findCards is used only for testing purposes.
*/
@Test
public void testFindSplitCardsByMainName() {
public void test_FindSplitCardsByMainName() {
// Modal double-faced
assertFindCard("Malakir Rebirth");
// Transform double-faced
@ -54,11 +61,11 @@ public class CardRepositoryTest {
/**
* Test CardRepository.findCards for the difficult cases.
*
* <p>
* CardRepository.findCards is used only for testing purposes.
*/
@Test
public void testFindSplitCardsBySecondName() {
public void test_FindSplitCardsBySecondName() {
// Modal double-faced
assertFindCard("Malakir Mire");
// Transform double-faced
@ -73,11 +80,11 @@ public class CardRepositoryTest {
/**
* Test CardRepository.findCardsCaseInsensitive for the difficult cases.
*
* <p>
* CardRepository.findCardsCaseInsensitive is used for actual game
*/
@Test
public void testFindSplitCardsByFullNameCaseInsensitive() {
public void test_FindSplitCardsByFullNameCaseInsensitive() {
// Modal double-faced
assertFindCard("malakIR rebirTH // maLAkir mIRe");
// Transform double-faced
@ -92,11 +99,11 @@ public class CardRepositoryTest {
/**
* Test CardRepository.findCards for the difficult cases.
*
* <p>
* CardRepository.findCards is used only for testing purposes.
*/
@Test
public void testFindSplitCardsByMainNameCaseInsensitive() {
public void test_FindSplitCardsByMainNameCaseInsensitive() {
// Modal double-faced
assertFindCard("malakIR rebirTH");
// Transform double-faced
@ -111,11 +118,11 @@ public class CardRepositoryTest {
/**
* Test CardRepository.findCards for the difficult cases.
*
* <p>
* CardRepository.findCards is used only for testing purposes.
*/
@Test
public void testFindSplitCardsBySecondNameCaseInsensitive() {
public void test_FindSplitCardsBySecondNameCaseInsensitive() {
// Modal double-faced
assertFindCard("maLAkir mIRe");
// Transform double-faced
@ -130,13 +137,13 @@ public class CardRepositoryTest {
/**
* Reported bug: https://github.com/magefree/mage/issues/9533
*
* <p>
* Each half of a split card displays the combined information of both halves in the deck editor.
*
* <p>
* `findCards`'s `returnSplitCardHalf` parameter should handle this issue
*/
@Test
public void splitCardInfoIsntDoubled() {
public void test_splitCardInfoIsntDoubled() {
// Consecrate // Consume
// {1}{W/B} // {2}{W}{B}
List<CardInfo> fullCard1 = CardRepository.instance.findCards("Consecrate", 1, false);
@ -146,7 +153,7 @@ public class CardRepositoryTest {
Assert.assertTrue(fullCard2.get(0).isSplitCard());
Assert.assertEquals("Consecrate // Consume", fullCard2.get(0).getName());
List<CardInfo> splitHalfCardLeft = CardRepository.instance.findCards("Consecrate", 1, true);
List<CardInfo> splitHalfCardLeft = CardRepository.instance.findCards("Consecrate", 1, true);
Assert.assertTrue(splitHalfCardLeft.get(0).isSplitCardHalf());
Assert.assertEquals("Consecrate", splitHalfCardLeft.get(0).getName());
List<CardInfo> splitHalfCardRight = CardRepository.instance.findCards("Consume", 1, true);
@ -167,4 +174,35 @@ public class CardRepositoryTest {
foundCards.isEmpty()
);
}
/**
* Some set can contain both main and second side cards with same card numbers,
* so search result must return main side first
*/
@Test
public void test_SearchSecondSides_FindCard() {
// XLN - Ixalan - Arguel's Blood Fast -> Temple of Aclazotz - 90
Assert.assertEquals("Arguel's Blood Fast", CardRepository.instance.findCard("XLN", "90").getName());
Assert.assertEquals("Arguel's Blood Fast", CardRepository.instance.findCard("XLN", "90", true).getName());
Assert.assertEquals("Arguel's Blood Fast", CardRepository.instance.findCard("XLN", "90", false).getName());
// VOW - Innistrad: Crimson Vow - Jacob Hauken, Inspector -> Hauken's Insight - 320
Assert.assertEquals("Jacob Hauken, Inspector", CardRepository.instance.findCard("VOW", "320").getName());
Assert.assertEquals("Jacob Hauken, Inspector", CardRepository.instance.findCard("VOW", "320", true).getName());
Assert.assertEquals("Jacob Hauken, Inspector", CardRepository.instance.findCard("VOW", "320", false).getName());
}
@Test
public void test_SearchSecondSides_FindCardWithPreferredSetAndNumber() {
// VOW - Innistrad: Crimson Vow - Jacob Hauken, Inspector -> Hauken's Insight - 65
// VOW - Innistrad: Crimson Vow - Jacob Hauken, Inspector -> Hauken's Insight - 320
// VOW - Innistrad: Crimson Vow - Jacob Hauken, Inspector -> Hauken's Insight - 332
Assert.assertEquals("65", CardRepository.instance.findCardWithPreferredSetAndNumber("Jacob Hauken, Inspector", "VOW", "65").getCardNumber());
Assert.assertEquals("320", CardRepository.instance.findCardWithPreferredSetAndNumber("Jacob Hauken, Inspector", "VOW", "320").getCardNumber());
Assert.assertEquals("332", CardRepository.instance.findCardWithPreferredSetAndNumber("Jacob Hauken, Inspector", "VOW", "332").getCardNumber());
Assert.assertEquals("65", CardRepository.instance.findCardWithPreferredSetAndNumber("Hauken's Insight", "VOW", "65").getCardNumber());
Assert.assertEquals("320", CardRepository.instance.findCardWithPreferredSetAndNumber("Hauken's Insight", "VOW", "320").getCardNumber());
Assert.assertEquals("332", CardRepository.instance.findCardWithPreferredSetAndNumber("Hauken's Insight", "VOW", "332").getCardNumber());
}
}

View file

@ -942,6 +942,7 @@ public class VerifyCardDataTest {
}
boolean containsDoubleSideCards = false;
Map<String, String> cardNumbers = new HashMap<>();
for (ExpansionSet.SetCardInfo cardInfo : set.getSetCardInfo()) {
Card card = CardImpl.createCard(cardInfo.getCardClass(), new CardSetInfo(cardInfo.getName(), set.getCode(),
cardInfo.getCardNumber(), cardInfo.getRarity(), cardInfo.getGraphicInfo()));
@ -973,7 +974,20 @@ public class VerifyCardDataTest {
+ " - " + card.getName() + " - " + card.getCardNumber()
+ " - " + card.getSecondCardFace().getName() + " - " + card.getSecondCardFace().getCardNumber());
}
*/
//*/
// CHECK: set contains both card sides
// related to second side cards usage
/*
String existedCardName = cardNumbers.getOrDefault(card.getCardNumber(), null);
if (existedCardName != null && !existedCardName.equals(card.getName())) {
String info = card.isNightCard() ? existedCardName + " -> " + card.getName() : card.getName() + " -> " + existedCardName;
errorsList.add("Error: set contains both card sides instead main only: "
+ set.getCode() + " - " + set.getName() + " - " + info + " - " + card.getCardNumber());
} else {
cardNumbers.put(card.getCardNumber(), card.getName());
}
//*/
}
// CHECK: double side cards must be in boosters
@ -1954,7 +1968,7 @@ public class VerifyCardDataTest {
// same find code as original cube
CardInfo cardInfo;
if (!cardId.getExtension().isEmpty()) {
cardInfo = CardRepository.instance.findCardWPreferredSet(cardId.getName(), cardId.getExtension());
cardInfo = CardRepository.instance.findCardWithPreferredSetAndNumber(cardId.getName(), cardId.getExtension(), null);
} else {
cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardId.getName());
}

View file

@ -80,6 +80,14 @@ public interface Card extends MageObject {
return false;
}
default Class<? extends Card> getMeldsToClazz() {
return null;
}
default Card getMeldsToCard() {
return null;
}
void assignNewId();
void addInfo(String key, String value, Game game);

View file

@ -47,6 +47,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
protected Rarity rarity;
protected Class<? extends Card> secondSideCardClazz;
protected Class<? extends Card> meldsWithClazz;
protected Class<? extends Card> meldsToClazz;
protected Card meldsToCard;
protected Card secondSideCard;
protected boolean nightCard;
protected SpellAbility spellAbility;
@ -127,6 +129,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
secondSideCard = null; // will be set on first getSecondCardFace call if card has one
nightCard = card.nightCard;
meldsWithClazz = card.meldsWithClazz;
meldsToClazz = card.meldsToClazz;
meldsToCard = null; // will be set on first getMeldsToCard call if card has one
spellAbility = null; // will be set on first getSpellAbility call if card has one
flipCard = card.flipCard;
@ -614,29 +618,36 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
@Override
public boolean isTransformable() {
// warning, not all multifaces cards can be transformable (meld, mdfc)
// mtg rules method: here
// GUI related method: search "transformable = true" in CardView
// TODO: check and fix method usage in game engine, it's must be mtg rules logic, not GUI
return this.secondSideCardClazz != null || this.nightCard;
}
@Override
public final Card getSecondCardFace() {
// init second side card on first call
// init card side on first call
if (secondSideCardClazz == null && secondSideCard == null) {
return null;
}
if (secondSideCard != null) {
return secondSideCard;
if (secondSideCard == null) {
secondSideCard = initSecondSideCard(secondSideCardClazz);
}
return secondSideCard;
}
private Card initSecondSideCard(Class<? extends Card> cardClazz) {
// must be non strict search in any sets, not one set
// example: if set contains only one card side
// method used in cards database creating, so can't use repository here
ExpansionSet.SetCardInfo info = Sets.findCardByClass(secondSideCardClazz, expansionSetCode);
ExpansionSet.SetCardInfo info = Sets.findCardByClass(cardClazz, expansionSetCode, cardNumber);
if (info == null) {
return null;
}
secondSideCard = createCard(secondSideCardClazz, new CardSetInfo(info.getName(), expansionSetCode, info.getCardNumber(), info.getRarity(), info.getGraphicInfo()));
return secondSideCard;
return createCard(cardClazz, new CardSetInfo(info.getName(), expansionSetCode, info.getCardNumber(), info.getRarity(), info.getGraphicInfo()));
}
@Override
@ -653,6 +664,25 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
return this.meldsWithClazz != null && this.meldsWithClazz.isInstance(card.getMainCard());
}
@Override
public Class<? extends Card> getMeldsToClazz() {
return this.meldsToClazz;
}
@Override
public Card getMeldsToCard() {
// init card on first call
if (meldsToClazz == null && meldsToCard == null) {
return null;
}
if (meldsToCard == null) {
meldsToCard = initSecondSideCard(meldsToClazz);
}
return meldsToCard;
}
@Override
public boolean isNightCard() {
return this.nightCard;

View file

@ -109,6 +109,8 @@ public abstract class MeldCard extends CardImpl {
@Override
public boolean isTransformable() {
// there are multiple day cards for one meld card, so can't show it as second side
// TODO: can be fixed after mutiple sides implement, e.g. with Mutate support
return false;
}

View file

@ -199,15 +199,23 @@ public class Sets extends HashMap<String, ExpansionSet> {
return null;
}
public static ExpansionSet.SetCardInfo findCardByClass(Class<?> clazz, String preferredSetCode) {
public static ExpansionSet.SetCardInfo findCardByClass(Class<?> clazz, String preferredSetCode, String preferredCardNumber) {
ExpansionSet.SetCardInfo info = null;
if (instance.containsKey(preferredSetCode)) {
info = instance.get(preferredSetCode).findCardInfoByClass(clazz).stream().findFirst().orElse(null);
info = instance.get(preferredSetCode).findCardInfoByClass(clazz)
.stream()
.filter(card -> preferredCardNumber == null || card.getCardNumber().equals(preferredCardNumber))
.findFirst()
.orElse(null);
}
if (info == null) {
for (Map.Entry<String, ExpansionSet> entry : instance.entrySet()) {
info = entry.getValue().findCardInfoByClass(clazz).stream().findFirst().orElse(null);
info = entry.getValue().findCardInfoByClass(clazz)
.stream()
.filter(card -> preferredCardNumber == null || card.getCardNumber().equals(preferredCardNumber))
.findFirst()
.orElse(null);
if (info != null) {
break;
}

View file

@ -65,7 +65,7 @@ public class MockCard extends CardImpl {
this.nightCard = card.isNightCard();
if (card.getSecondSideName() != null && !card.getSecondSideName().isEmpty()) {
this.secondSideCard = new MockCard(CardRepository.instance.findCardWPreferredSet(card.getSecondSideName(), card.getSetCode()));
this.secondSideCard = new MockCard(CardRepository.instance.findCardWithPreferredSetAndNumber(card.getSecondSideName(), card.getSetCode(), card.getCardNumber()));
}
if (card.isAdventureCard()) {

View file

@ -40,7 +40,7 @@ public class MockSplitCard extends SplitCard {
this.nightCard = card.isNightCard();
if (card.getSecondSideName() != null && !card.getSecondSideName().isEmpty()) {
this.secondSideCard = new MockCard(CardRepository.instance.findCardWPreferredSet(card.getSecondSideName(), card.getSetCode()));
this.secondSideCard = new MockCard(CardRepository.instance.findCardWithPreferredSetAndNumber(card.getSecondSideName(), card.getSetCode(), card.getCardNumber()));
}
this.flipCardName = card.getFlipCardName();
@ -49,13 +49,13 @@ public class MockSplitCard extends SplitCard {
this.addAbility(textAbilityFromString(ruleText));
}
CardInfo leftHalf = CardRepository.instance.findCardWPreferredSet(getLeftHalfName(card), card.getSetCode(), true);
CardInfo leftHalf = CardRepository.instance.findCardWithPreferredSetAndNumber(getLeftHalfName(card), card.getSetCode(), card.getCardNumber(), true);
if (leftHalf != null) {
this.leftHalfCard = new MockSplitCardHalf(leftHalf);
((SplitCardHalf) this.leftHalfCard).setParentCard(this);
}
CardInfo rightHalf = CardRepository.instance.findCardWPreferredSet(getRightHalfName(card), card.getSetCode(), true);
CardInfo rightHalf = CardRepository.instance.findCardWithPreferredSetAndNumber(getRightHalfName(card), card.getSetCode(), card.getCardNumber(), true);
if (rightHalf != null) {
this.rightHalfCard = new MockSplitCardHalf(rightHalf);
((SplitCardHalf) this.rightHalfCard).setParentCard(this);

View file

@ -93,7 +93,7 @@ public class CardInfo {
protected boolean flipCard;
@DatabaseField
protected boolean doubleFaced;
@DatabaseField(indexName = "name_index")
@DatabaseField(indexName = "nightCard_index")
protected boolean nightCard;
@DatabaseField
protected String flipCardName;
@ -107,6 +107,8 @@ public class CardInfo {
protected boolean modalDoubleFacesCard;
@DatabaseField
protected String modalDoubleFacesSecondSideName;
@DatabaseField
protected String meldsToCardName;
// if you add new field with card side name then update CardRepository.addNewNames too
@ -134,6 +136,11 @@ public class CardInfo {
this.flipCard = card.isFlipCard();
this.flipCardName = card.getFlipCardName();
Card meldToCard = card.getMeldsToCard();
if (meldToCard != null) {
this.meldsToCardName = meldToCard.getName();
}
this.doubleFaced = card.isTransformable() && card.getSecondCardFace() != null;
this.nightCard = card.isNightCard();
Card secondSide = card.getSecondCardFace();
@ -437,6 +444,10 @@ public class CardInfo {
return flipCardName;
}
public String getMeldsToCardName() {
return meldsToCardName;
}
public boolean isDoubleFaced() {
return doubleFaced;
}

View file

@ -117,6 +117,9 @@ public enum CardRepository {
if (card.getFlipCardName() != null && !card.getFlipCardName().isEmpty()) {
namesList.add(card.getFlipCardName());
}
if (card.getMeldsToCardName() != null && !card.getMeldsToCardName().isEmpty()) {
namesList.add(card.getMeldsToCardName());
}
}
public static Boolean haveSnowLands(String setCode) {
@ -284,6 +287,10 @@ public enum CardRepository {
queryBuilder.limit(1L).where()
.eq("setCode", new SelectArg(setCode))
.and().eq("cardNumber", new SelectArg(cardNumber));
// some double faced cards can use second side card with same number as main side
// (example: vow - 65 - Jacob Hauken, Inspector), so make priority for main side first
queryBuilder.orderBy("nightCard", true);
}
List<CardInfo> result = cardDao.query(queryBuilder.prepare());
if (!result.isEmpty()) {
@ -387,32 +394,36 @@ public enum CardRepository {
/**
* Function to find a card by name from a specific set.
* Used for building cubes, packs, and for ensuring that dual faces and split cards have sides/halves from the same set.
* Used for building cubes, packs, and for ensuring that dual faces and split cards have sides/halves from
* the same set and variant art.
*
* @param name name of the card, or side of the card, to find
* @param expansion the set name from which to find the card
* @param cardNumber the card number for variant arts in one set
* @param returnSplitCardHalf whether to return a half of a split card or the corresponding full card.
* Want this `false` when user is searching by either names in a split card so that
* the full card can be found by either name.
* @return
*/
public CardInfo findCardWPreferredSet(String name, String expansion, boolean returnSplitCardHalf) {
public CardInfo findCardWithPreferredSetAndNumber(String name, String expansion, String cardNumber, boolean returnSplitCardHalf) {
List<CardInfo> cards;
cards = findCards(name, 0, returnSplitCardHalf);
CardInfo bestCard = cards.stream()
.filter(card -> expansion == null || expansion.equalsIgnoreCase(card.getSetCode()))
.filter(card -> cardNumber == null || cardNumber.equals(card.getCardNumber()))
.findFirst()
.orElse(null);
if (!cards.isEmpty()) {
for (CardInfo cardinfo : cards) {
if (cardinfo.getSetCode() != null && expansion != null && expansion.equalsIgnoreCase(cardinfo.getSetCode())) {
return cardinfo;
}
}
if (bestCard != null) {
return bestCard;
} else {
return findPreferredCoreExpansionCard(name);
}
return findPreferredCoreExpansionCard(name);
}
public CardInfo findCardWPreferredSet(String name, String expansion) {
return findCardWPreferredSet(name, expansion, false);
public CardInfo findCardWithPreferredSetAndNumber(String name, String expansion, String cardNumber) {
return findCardWithPreferredSetAndNumber(name, expansion, cardNumber, false);
}
public List<CardInfo> findCards(String name) {
@ -528,6 +539,8 @@ public enum CardRepository {
* Warning, don't use db functions in card's code - it generates heavy db loading in AI simulations. If you
* need that feature then check for simulation mode. See https://github.com/magefree/mage/issues/7014
*
* Ignoring night cards by default
*
* @param criteria
* @return
*/

View file

@ -84,7 +84,7 @@ public abstract class DraftCube {
if (!cardId.getName().isEmpty()) {
CardInfo cardInfo = null;
if (!cardId.getExtension().isEmpty()) {
cardInfo = CardRepository.instance.findCardWPreferredSet(cardId.getName(), cardId.getExtension());
cardInfo = CardRepository.instance.findCardWithPreferredSetAndNumber(cardId.getName(), cardId.getExtension(), null);
} else {
cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardId.getName());
}

View file

@ -71,7 +71,7 @@ public class PermanentCard extends PermanentImpl {
if (card instanceof LevelerCard) {
maxLevelCounters = ((LevelerCard) card).getMaxLevelCounters();
}
if (isTransformable()) {
if (card.isTransformable()) {
if (game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getId()) != null
|| NightboundAbility.checkCard(this, game)) {
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getId(), null);
@ -133,6 +133,9 @@ public class PermanentCard extends PermanentImpl {
if (card.getSecondCardFace() != null) {
this.secondSideCardClazz = card.getSecondCardFace().getClass();
}
if (card.getMeldsToCard() != null) {
this.meldsToClazz = card.getMeldsToCard().getClass();
}
this.nightCard = card.isNightCard();
this.flipCard = card.isFlipCard();
this.flipCardName = card.getFlipCardName();

View file

@ -908,6 +908,17 @@ public interface Player extends MageItem, Copyable<Player> {
*/
boolean moveCardToHandWithInfo(Card card, Ability source, Game game, boolean withName);
/**
* Iterates through a set of cards and runs moveCardToHandWithInfo on each item
*
* @param cards
* @param source
* @param game
* @param withName show the card names in the log
* @return
*/
boolean moveCardsToHandWithInfo(Cards cards, Ability source, Game game, boolean withName);
/**
* Uses card.moveToExile and posts a inform message about moving the card to
* exile into the game log. Don't use this in replacement effects, because

View file

@ -4642,6 +4642,15 @@ public abstract class PlayerImpl implements Player, Serializable {
return result;
}
@Override
public boolean moveCardsToHandWithInfo(Cards cards, Ability source, Game game, boolean withName) {
Player player = this;
for (Card card : cards.getCards(game)) {
player.moveCardToHandWithInfo(card, source, game, withName);
}
return true;
}
@Override
public boolean moveCardToHandWithInfo(Card card, Ability source, Game game, boolean withName) {
boolean result = false;

23
pom.xml
View file

@ -116,11 +116,34 @@
<version>2.7</version>
</plugin>
<!-- devs only: allows to run apps from command line by exec:java command -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<!-- generate readable readme file for releases -->
<!-- results uses in distribution.xml -->
<!-- https://github.com/walokra/markdown-page-generator-plugin -->
<plugin>
<groupId>com.ruleoftech</groupId>
<artifactId>markdown-page-generator-plugin</artifactId>
<version>2.4.0</version>
<executions>
<execution>
<phase>process-resources</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<inputDirectory>../</inputDirectory>
<inputFileExtensions>md</inputFileExtensions>
<outputDirectory>${project.build.directory}/docs</outputDirectory>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>