Merge branch 'External-master'
All checks were successful
/ build_release (push) Successful in 10m38s

This commit is contained in:
Failure 2025-07-19 22:36:15 -07:00
commit 3062dd066d
838 changed files with 24448 additions and 4173 deletions

1
.gitignore vendored
View file

@ -12,6 +12,7 @@ syntax: glob
*.log.*
*/gamelogs
*/gamelogsJson
*/gamesHistory
# Mage.Client
Mage.Client/plugins/images

View file

@ -8,7 +8,7 @@ git:
before_install:
- echo "MAVEN_OPTS='-Xmx2g'" > ~/.mavenrc
script:
- mvn test -B -Dlog4j.configuration=file:${TRAVIS_BUILD_DIR}/.travis/log4j.properties
- mvn test -B -Dxmage.dataCollectors.printGameLogs=false -Dlog4j.configuration=file:${TRAVIS_BUILD_DIR}/.travis/log4j.properties
cache:
directories:
- $HOME/.m2

View file

@ -87,14 +87,6 @@
<artifactId>jetlang</artifactId>
<version>0.2.23</version>
</dependency>
<dependency>
<!-- amazon s3 cloud lib to upload game logs from experimental client -->
<!-- TODO: feature must be removed as unused or implemented for all -->
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.78</version>
</dependency>
<dependency>
<!-- GUI lib TODO: unused and can be deleted? -->
<groupId>com.jgoodies</groupId>

View file

@ -122,6 +122,9 @@ public final class Constants {
public static final String RESOURCE_SYMBOL_FOLDER_SVG = "svg";
public static final String RESOURCE_SYMBOL_FOLDER_PNG = "png";
// download rarity icons to large folder by default
public static final String RESOURCE_PATH_SYMBOLS_RARITY_DEFAULT_PATH = RESOURCE_PATH_SYMBOLS + File.separator + RESOURCE_SYMBOL_FOLDER_LARGE;
public enum ResourceSymbolSize {
SMALL, // TODO: delete SMALL, MEDIUM and LARGE as outdated (svg or generated png works fine)
MEDIUM,
@ -133,12 +136,12 @@ public final class Constants {
// resources - sets
public static final String RESOURCE_PATH_SETS = File.separator + "sets";
public static final String RESOURCE_SET_FOLDER_SMALL = "small";
public static final String RESOURCE_SET_FOLDER_MEDIUM = ""; // empty, medium images laydown in "sets" folder, TODO: delete that and auto gen, use png for html, not gif
public static final String RESOURCE_SET_FOLDER_LARGE = "large";
public static final String RESOURCE_SET_FOLDER_SVG = "svg";
public enum ResourceSetSize {
SMALL,
MEDIUM,
LARGE,
SVG
}

View file

@ -44,7 +44,7 @@ public class DeckGeneratorPool {
// List of cards so far in the deck
private final List<Card> deckCards = new ArrayList<>();
// List of reserve cards found to fix up undersized decks
private final List<Card> reserveSpells = new ArrayList<>();
private final Map<String, Card> reserveSpells = new HashMap<>();
private final Deck deck;
/**
@ -170,12 +170,13 @@ public class DeckGeneratorPool {
* @param card the card to add.
*/
public void addCard(Card card) {
Object cnt = cardCounts.get((card.getName()));
if (cnt == null)
cardCounts.put(card.getName(), 0);
int existingCount = cardCounts.get((card.getName()));
cardCounts.put(card.getName(), existingCount + 1);
int count = cardCounts.getOrDefault(card.getName(), 0);
cardCounts.put(card.getName(), count + 1);
deckCards.add(card);
if (deckCards.stream().distinct().collect(Collectors.toList()).size() != deckCards.size()) {
System.out.println("wtf " + card.getName());
}
}
public void clearCards(boolean isClearReserve) {
@ -198,8 +199,8 @@ public class DeckGeneratorPool {
// Only cards with CMC < 7 and don't already exist in the deck
// can be added to our reserve pool as not to overwhelm the curve
// with high CMC cards and duplicates.
if (cardCMC < 7 && getCardCount(card.getName()) == 0) {
this.reserveSpells.add(card);
if (cardCMC < 7 && getCardCount(card.getName()) == 0 && !this.reserveSpells.containsKey(card.getName())) {
this.reserveSpells.put(card.getName(), card);
return true;
}
return false;
@ -416,48 +417,38 @@ public class DeckGeneratorPool {
* @return a fixed list of cards for this deck.
*/
private List<Card> getFixedSpells() {
int spellSize = deckCards.size();
int spellsSize = deckCards.size();
int nonLandSize = (deckSize - landCount);
// Less spells than needed
if (spellSize < nonLandSize) {
int spellsNeeded = nonLandSize - spellSize;
// If we haven't got enough spells in reserve to fulfil the amount we need, skip adding any.
if (reserveSpells.size() >= spellsNeeded) {
List<Card> spellsToAdd = new ArrayList<>(spellsNeeded);
// Initial reservoir
for (int i = 0; i < spellsNeeded; i++)
spellsToAdd.add(reserveSpells.get(i));
for (int i = spellsNeeded + 1; i < reserveSpells.size() - 1; i++) {
int j = RandomUtil.nextInt(i);
Card randomCard = reserveSpells.get(j);
if (isValidSpellCard(randomCard) && j < spellsToAdd.size()) {
spellsToAdd.set(j, randomCard);
}
// fewer spells than needed - add
if (spellsSize < nonLandSize) {
int needExtraSpells = nonLandSize - spellsSize;
List<Card> possibleSpells = new ArrayList<>(reserveSpells.values());
while (needExtraSpells > 0) {
Card card = RandomUtil.randomFromCollection(possibleSpells);
if (card == null) {
break;
}
// Add randomly selected spells needed
deckCards.addAll(spellsToAdd);
if (isValidSpellCard(card)) {
needExtraSpells--;
deckCards.add(card);
}
possibleSpells.remove(card);
}
}
// More spells than needed
else if (spellSize > (deckSize - landCount)) {
int spellsRemoved = (spellSize) - (deckSize - landCount);
for (int i = 0; i < spellsRemoved; ++i) {
deckCards.remove(RandomUtil.nextInt(deckCards.size()));
// more spells than needed - remove
if (spellsSize > nonLandSize) {
int removeCount = spellsSize - nonLandSize;
for (int i = 0; i < removeCount; ++i) {
deckCards.remove(RandomUtil.randomFromCollection(deckCards));
}
}
// Check we have exactly the right amount of cards for a deck.
if (deckCards.size() != nonLandSize) {
logger.info("Can't generate full deck for selected settings - try again or choose more sets and less colors");
logger.info("Can't generate full deck for selected settings - try again or choose more sets and less colors (wrong non land cards amount)");
}
// Return the fixed amount
return deckCards;
}
@ -619,48 +610,75 @@ public class DeckGeneratorPool {
if (needCommandersCount > 0 && !genPool.cardCounts.isEmpty()) {
throw new IllegalArgumentException("Wrong code usage: generateSpells with creatures and commanders must be called as first");
}
List<CardInfo> cardPool = CardRepository.instance.findCards(criteria);
List<Card> cardsPool = CardRepository.instance.findCards(criteria).stream()
.map(CardInfo::createMockCard)
.filter(genPool::isValidSpellCard)
.collect(Collectors.toList());
List<Card> commandersPool = cardsPool.stream()
.filter(genPool::isValidCommander)
.collect(Collectors.toList());
List<DeckGeneratorCMC.CMC> deckCMCs = genPool.getCMCsForSpellCount(needCardsCount);
int count = 0;
int usedCardsCount = 0;
int validCommanders = 0;
int reservesAdded = 0;
if (cardPool.size() > 0 && cardPool.size() >= needCardsCount) {
if (cardsPool.size() > 0 && cardsPool.size() >= needCardsCount) {
int tries = 0;
List<Card> possibleCards = new ArrayList<>(cardsPool);
List<Card> possibleCommanders = new ArrayList<>(commandersPool);
while (true) {
tries++;
// can't finish deck, stop and use reserved cards later
if (tries > DeckGenerator.MAX_TRIES) {
logger.info("Can't generate full deck for selected settings - try again or choose more sets and less colors");
logger.info("Can't generate full deck for selected settings - try again or choose more sets and less colors (max tries exceeded)");
break;
}
// can finish deck - but make sure it has commander
if (count >= needCardsCount) {
if (usedCardsCount >= needCardsCount) {
if (validCommanders < needCommandersCount) {
// reset deck search from scratch (except reserved cards)
count = 0;
usedCardsCount = 0;
validCommanders = 0;
deckCMCs = genPool.getCMCsForSpellCount(needCardsCount);
genPool.clearCards(false);
genPool.clearCards(true);
possibleCards = new ArrayList<>(cardsPool);
possibleCommanders = new ArrayList<>(commandersPool);
continue;
}
break;
}
Card card = cardPool.get(RandomUtil.nextInt(cardPool.size())).createMockCard();
if (possibleCards.isEmpty()) {
throw new IllegalStateException("Not enough cards to generate deck (possible cards is empty)");
}
// choose commander first
Card card = null;
if (validCommanders < needCommandersCount && !possibleCommanders.isEmpty()) {
card = RandomUtil.randomFromCollection(possibleCommanders);
}
// choose other cards after commander
if (card == null) {
card = RandomUtil.randomFromCollection(possibleCards);
}
if (!genPool.isValidSpellCard(card)) {
possibleCards.remove(card);
possibleCommanders.remove(card);
continue;
}
int cardCMC = card.getManaValue();
for (DeckGeneratorCMC.CMC deckCMC : deckCMCs) {
if (cardCMC >= deckCMC.min && cardCMC <= deckCMC.max) {
int currentAmount = deckCMC.getAmount();
if (currentAmount > 0) {
deckCMC.setAmount(currentAmount - 1);
int needAmount = deckCMC.getAmount();
if (needAmount > 0) {
deckCMC.setAmount(needAmount - 1);
genPool.addCard(card.copy());
count++;
usedCardsCount++;
// make sure it has compatible commanders
if (genPool.isValidCommander(card)) {
validCommanders++;
@ -674,7 +692,7 @@ public class DeckGeneratorPool {
}
}
} else {
throw new IllegalStateException("Not enough cards to generate deck.");
throw new IllegalStateException("Not enough cards to generate deck (cards pool too small)");
}
}

View file

@ -19,7 +19,6 @@ import mage.client.dialog.AddLandDialog;
import mage.client.dialog.PreferencesDialog;
import mage.client.plugins.impl.Plugins;
import mage.client.util.Event;
import mage.client.util.GUISizeHelper;
import mage.client.util.Listener;
import mage.client.util.audio.AudioManager;
import mage.components.CardInfoPane;
@ -31,7 +30,6 @@ import mage.util.XmageThreadFactory;
import mage.view.CardView;
import mage.view.SimpleCardView;
import org.apache.log4j.Logger;
import org.mage.card.arcane.ManaSymbols;
import javax.swing.*;
import javax.swing.border.Border;
@ -534,7 +532,8 @@ public class DeckEditorPanel extends javax.swing.JPanel {
}
}
});
refreshDeck(true);
refreshDeck(true, false);
// auto-import dropped files from OS
if (mode == DeckEditorMode.FREE_BUILDING) {
@ -673,20 +672,28 @@ public class DeckEditorPanel extends javax.swing.JPanel {
private void refreshDeck() {
if (this.isVisible()) { // TODO: test auto-close deck with active lands dialog, e.g. on timeout
refreshDeck(false);
refreshDeck(false, false);
}
}
private void refreshDeck(boolean useLayout) {
private void refreshDeck(boolean useLayout, boolean useDeckValidation) {
try {
setCursor(new Cursor(Cursor.WAIT_CURSOR));
MageFrame.getDesktop().setCursor(new Cursor(Cursor.WAIT_CURSOR));
this.txtDeckName.setText(deck.getName());
deckArea.loadDeck(deck, useLayout, bigCard);
if (useDeckValidation) {
validateDeck();
}
} finally {
setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
MageFrame.getDesktop().setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
}
private void validateDeck() {
this.deckLegalityDisplay.setVisible(true);
this.deckLegalityDisplay.validateDeck(deck);
}
private void setTimeout(int s) {
int minute = s / 60;
int second = s - (minute * 60);
@ -762,7 +769,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
if (newDeck != null) {
deck = newDeck;
refreshDeck();
refreshDeck(false, true);
}
// save last deck import folder
@ -818,7 +825,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
Deck deckToAppend = Deck.load(DeckImporter.importDeckFromFile(tempDeckPath, errorMessages, false), true, true);
processAndShowImportErrors(errorMessages);
this.deck = Deck.append(deckToAppend, this.deck);
refreshDeck();
refreshDeck(false, true);
} catch (GameException e1) {
JOptionPane.showMessageDialog(MageFrame.getDesktop(), e1.getMessage(), "Error loading deck", JOptionPane.ERROR_MESSAGE);
} finally {
@ -922,7 +929,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
if (newDeck != null) {
deck = newDeck;
refreshDeck();
refreshDeck(false, true);
return true;
}
@ -1441,7 +1448,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
if (newDeck != null) {
deck = newDeck;
refreshDeck(true);
refreshDeck(true, true);
}
// save last deck history
@ -1474,7 +1481,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
// in deck editor mode - clear all cards
deck = new Deck();
}
refreshDeck();
refreshDeck(false, true);
}//GEN-LAST:event_btnNewActionPerformed
private void btnExitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnExitActionPerformed
@ -1483,7 +1490,9 @@ public class DeckEditorPanel extends javax.swing.JPanel {
private void btnAddLandActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnAddLandActionPerformed
AddLandDialog dialog = new AddLandDialog();
dialog.showDialog(deck, mode, this::refreshDeck);
dialog.showDialog(deck, mode, () -> {
this.refreshDeck(false, true);
});
}//GEN-LAST:event_btnAddLandActionPerformed
private void btnGenDeckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnGenDeckActionPerformed
@ -1501,7 +1510,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
} finally {
MageFrame.getDesktop().setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
refreshDeck();
refreshDeck(false, true);
}//GEN-LAST:event_btnGenDeckActionPerformed
private void btnSubmitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSubmitActionPerformed
@ -1548,8 +1557,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
}//GEN-LAST:event_btnExportActionPerformed
private void btnLegalityActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnLegalityActionPerformed
this.deckLegalityDisplay.setVisible(true);
this.deckLegalityDisplay.validateDeck(deck);
validateDeck();
}//GEN-LAST:event_btnLegalityActionPerformed
// Variables declaration - do not modify//GEN-BEGIN:variables

View file

@ -75,7 +75,7 @@ public class DraftPickLogger {
private void appendToDraftLog(String data) {
if (logging) {
try {
Files.write(logPath, data.getBytes(), StandardOpenOption.APPEND);
Files.write(logPath, data.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException ex) {
LOGGER.error(null, ex);
}

View file

@ -257,10 +257,7 @@ public class CallbackClientImpl implements CallbackClient {
if (panel != null) {
Session session = SessionHandler.getSession();
if (session.isJsonLogActive()) {
UUID gameId = callback.getObjectId();
appendJsonEvent("GAME_OVER", callback.getObjectId(), message);
String logFileName = "game-" + gameId + ".json";
S3Uploader.upload(logFileName, gameId.toString());
}
panel.endMessage(callback.getMessageId(), message.getGameView(), message.getOptions(), message.getMessage());
}

View file

@ -1,46 +0,0 @@
package mage.client.remote;
import com.amazonaws.AmazonClientException;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.Upload;
import org.apache.log4j.Logger;
import java.io.File;
public class S3Uploader {
private static final Logger logger = Logger.getLogger(S3Uploader.class);
public static Boolean upload(String filePath, String keyName) throws Exception {
String existingBucketName = System.getenv("S3_BUCKET") != null ? System.getenv("S3_BUCKET")
: "xmage-game-logs-dev";
String accessKeyId = System.getenv("AWS_ACCESS_ID");
String secretKeyId = System.getenv("AWS_SECRET_KEY");
if (accessKeyId == null || accessKeyId.isEmpty()
|| secretKeyId == null || secretKeyId.isEmpty()
|| existingBucketName.isEmpty()) {
logger.info("Aborting json log sync.");
return false;
}
String path = new File("./" + filePath).getCanonicalPath();
logger.info("Syncing " + path + " to bucket: " + existingBucketName + " with AWS Access Id: " + accessKeyId);
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKeyId, secretKeyId);
TransferManager tm = new TransferManager(awsCreds);
Upload upload = tm.upload(existingBucketName, "/game/" + keyName + ".json", new File(path));
try {
upload.waitForUploadResult();
logger.info("Sync Complete For " + path + " to bucket: " + existingBucketName + " with AWS Access Id: " + accessKeyId);
new File(path);
return true;
} catch (AmazonClientException amazonClientException) {
logger.fatal("Unable to upload file, upload was aborted.", amazonClientException);
return false;
}
}
}

View file

@ -896,7 +896,7 @@ public class TablesPanel extends javax.swing.JPanel {
formatFilterList.add(RowFilter.regexFilter("^Limited", TablesTableModel.COLUMN_DECK_TYPE));
}
if (btnFormatOther.isSelected()) {
formatFilterList.add(RowFilter.regexFilter("^Momir Basic|^Constructed - Pauper|^Constructed - Frontier|^Constructed - Extended|^Constructed - Eternal|^Constructed - Historical|^Constructed - Super|^Constructed - Freeform|^Australian Highlander|^European Highlander|^Canadian Highlander|^Constructed - Old|^Constructed - Historic", TablesTableModel.COLUMN_DECK_TYPE));
formatFilterList.add(RowFilter.regexFilter("^Momir Basic|^Constructed - Pauper|^Constructed - Frontier|^Constructed - Extended|^Constructed - Eternal|^Constructed - Historical|^Constructed - Super|^Constructed - Freeform|^Constructed - Freeform Unlimited|^Australian Highlander|^European Highlander|^Canadian Highlander|^Constructed - Old|^Constructed - Historic", TablesTableModel.COLUMN_DECK_TYPE));
}
// skill

View file

@ -89,11 +89,6 @@ public final class ManaSymbols {
public static void loadImages() {
logger.info("Symbols: loading...");
// TODO: delete files rename jpg->gif (it was for backward compatibility for one of the old version?)
renameSymbols(getResourceSymbolsPath(ResourceSymbolSize.SMALL));
renameSymbols(getResourceSymbolsPath(ResourceSymbolSize.MEDIUM));
renameSymbols(getResourceSymbolsPath(ResourceSymbolSize.LARGE));
//renameSymbols(getSymbolsPath(ResourceSymbolSize.SVG)); // not need
// TODO: remove medium sets files to "medium" folder like symbols above?
// prepare svg's css settings
@ -145,9 +140,9 @@ public final class ManaSymbols {
Map<Rarity, Image> rarityImages = new EnumMap<>(Rarity.class);
setImages.put(set, rarityImages);
// load medium size
// load large size
for (Rarity rarityCode : codes) {
File file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM) + set + '-' + rarityCode.getCode() + ".jpg");
File file = new File(getResourceSetsPath(ResourceSetSize.LARGE) + set + '-' + rarityCode.getCode() + ".png");
try {
Image image = UI.getImageIcon(file.getAbsolutePath()).getImage();
int width = image.getWidth(null);
@ -167,7 +162,7 @@ public final class ManaSymbols {
// generate small size
try {
File file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM));
File file = new File(getResourceSetsPath(ResourceSetSize.LARGE));
if (!file.exists()) {
file.mkdirs();
}
@ -175,11 +170,11 @@ public final class ManaSymbols {
for (Rarity code : codes) {
File newFile = new File(pathRoot + '-' + code + ".png");
if (!(MageFrame.isSkipSmallSymbolGenerationForExisting() && newFile.exists())) {// skip if option enabled and file already exists
file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM) + set + '-' + code + ".png");
file = new File(getResourceSetsPath(ResourceSetSize.LARGE) + set + '-' + code + ".png");
if (file.exists()) {
continue;
}
file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM) + set + '-' + code + ".jpg");
file = new File(getResourceSetsPath(ResourceSetSize.LARGE) + set + '-' + code + ".png");
Image image = UI.getImageIcon(file.getAbsolutePath()).getImage();
try {
int width = image.getWidth(null);
@ -239,7 +234,7 @@ public final class ManaSymbols {
}
}
private static File getSymbolFileNameAsGIF(String symbol, int size) {
private static File getSymbolFileNameAsPNG(String symbol, int size) {
ResourceSymbolSize needSize = null;
if (size <= 15) {
@ -250,15 +245,15 @@ public final class ManaSymbols {
needSize = ResourceSymbolSize.LARGE;
}
return new File(getResourceSymbolsPath(needSize) + symbol + ".gif");
return new File(getResourceSymbolsPath(needSize) + symbol + ".png");
}
private static BufferedImage loadSymbolAsGIF(String symbol, int resizeToWidth, int resizeToHeight) {
File file = getSymbolFileNameAsGIF(symbol, resizeToWidth);
return loadSymbolAsGIF(file, resizeToWidth, resizeToHeight);
private static BufferedImage loadSymbolAsPNG(String symbol, int resizeToWidth, int resizeToHeight) {
File file = getSymbolFileNameAsPNG(symbol, resizeToWidth);
return loadSymbolAsPNG(file, resizeToWidth, resizeToHeight);
}
private static BufferedImage loadSymbolAsGIF(File sourceFile, int resizeToWidth, int resizeToHeight) {
private static BufferedImage loadSymbolAsPNG(File sourceFile, int resizeToWidth, int resizeToHeight) {
BufferedImage image = null;
@ -315,9 +310,9 @@ public final class ManaSymbols {
// gif (if svg fails)
if (image == null) {
file = getSymbolFileNameAsGIF(symbol, size);
file = getSymbolFileNameAsPNG(symbol, size);
if (file.exists()) {
image = loadSymbolAsGIF(file, size, size);
image = loadSymbolAsPNG(file, size, size);
}
}
if (image == null) {
@ -352,29 +347,6 @@ public final class ManaSymbols {
return errorInfo.isEmpty();
}
private static void renameSymbols(String path) {
File file = new File(path);
if (!file.exists()) {
return;
}
final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.jpg");
try {
Files.walkFileTree(Paths.get(path), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (matcher.matches(file)) {
Path gifPath = file.resolveSibling(file.getFileName().toString().replaceAll("\\.jpg$", ".gif"));
Files.move(file, gifPath, StandardCopyOption.REPLACE_EXISTING);
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
logger.error("Couldn't rename mana symbols on " + path, e);
}
}
private static String getResourceSymbolsPath(ResourceSymbolSize needSize) {
// return real path to symbols (default or user defined)
@ -420,8 +392,8 @@ public final class ManaSymbols {
case SMALL:
path = path + Constants.RESOURCE_SET_FOLDER_SMALL;
break;
case MEDIUM:
path = path + Constants.RESOURCE_SET_FOLDER_MEDIUM;
case LARGE:
path = path + Constants.RESOURCE_SET_FOLDER_LARGE;
break;
case SVG:
path = path + Constants.RESOURCE_SET_FOLDER_SVG;

View file

@ -663,7 +663,9 @@ public class CardPluginImpl implements CardPlugin {
// mana symbols (low quality)
jobs = new GathererSymbols();
for (DownloadJob job : jobs) {
downloader.add(job);
// TODO: gatherer removed mana symbols icons after 2025, see https://github.com/magefree/mage/issues/13797
// remove GathererSymbols code after few releases as unused (2025.06.28)
// downloader.add(job);
}
// set code symbols (low quality)

View file

@ -46,8 +46,10 @@ public class Downloader extends AbstractLaternaBean {
public Downloader() {
// prepare 10 threads and start to waiting new download jobs from queue
// TODO: gatherer website has download rate limits, so limit max threads as temporary solution
int maxThreads = 3;
PoolFiberFactory f = new PoolFiberFactory(pool);
for (int i = 0, numThreads = 10; i < numThreads; i++) {
for (int i = 0, numThreads = maxThreads; i < numThreads; i++) {
Fiber fiber = f.create();
fiber.start();
fibers.add(fiber);

View file

@ -16,7 +16,9 @@ import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir;
/**
* Download: set code symbols download from wizards web size
* <p>
* Warning, it's outdated source with low quality images. TODO: must migrate to scryfall like mana icons
* Warning, it's outdated source with low quality images.
* TODO: must migrate to scryfall like mana icons,
* see https://github.com/magefree/mage/issues/13261
*/
public class GathererSets implements Iterable<DownloadJob> {
@ -41,9 +43,10 @@ public class GathererSets implements Iterable<DownloadJob> {
private static File outDir;
private static final int DAYS_BEFORE_RELEASE_TO_DOWNLOAD = +14; // Try to load the symbolsBasic eralies 14 days before release date
private static final int DAYS_BEFORE_RELEASE_TO_DOWNLOAD = +14; // Try to load the symbolsBasic 14 days before release date
private static final Logger logger = Logger.getLogger(GathererSets.class);
// TODO: find all possible sets from ExpansionRepository instead custom
private static final String[] symbolsBasic = {"10E", "9ED", "8ED", "7ED", "6ED", "5ED", "4ED", "3ED", "2ED", "LEB", "LEA",
"HOP",
"ARN", "ATQ", "LEG", "DRK", "FEM", "HML",
@ -61,14 +64,16 @@ public class GathererSets implements Iterable<DownloadJob> {
"TSP", "TSB", "PLC", "FUT",
"LRW", "MOR",
"SHM", "EVE",
"MED", "ME2", "ME3", "ME4",
"ME2", "ME3", "ME4",
"POR", "P02", "PTK",
"ARC", "DD3EVG",
"W16", "W17",
"W16", "W17",
// "PALP" -- Gatherer does not have the set Asia Pacific Land Program
// "ATH" -- has cards from many sets, symbol does not exist on gatherer
// "CP", "DPA", "PELP", "PGPX", "PGRU", "H17", "JR", "SWS", // need to fix
"H09", "PD2", "PD3", "UNH", "CM1", "V11", "A25", "UST", "IMA", "DD2", "EVG", "DDC", "DDE", "DDD", "CHR", "G18", "GVL", "S00", "S99", "UGL" // ok
"H09", "PD2", "PD3", "UNH", "CM1", "V11", "A25", "UST", "IMA", "DD2",
"EVG", "DDC", "DDE", "DDD", "CHR", "G18", "GVL", "S00", "S99", "UGL",
"BTD" // ok
// current testing
};
@ -98,21 +103,25 @@ public class GathererSets implements Iterable<DownloadJob> {
"GNT", "UMA", "GRN",
"RNA", "WAR", "MH1",
"M20",
"C19", "ELD", "MB1", "GN2", "J20", "THB", "UND", "C20", "IKO", "M21",
"C19", "ELD", "MB1", "GN2", "THB", "UND", "C20", "IKO", "M21",
"JMP", "2XM", "ZNR", "KLR", "CMR", "KHC", "KHM", "TSR", "STX", "STA",
"C21", "MH2", "AFR", "AFC", "J21", "MID", "MIC", "VOW", "VOC", "YMID",
"NEC", "YNEO", "NEO", "SNC", "NCC", "CLB", "2X2", "DMU", "DMC", "YDMU", "40K", "GN3",
"UNF", "BRO", "BRC", "BOT", "30A", "J22", "SCD", "DMR", "ONE", "ONC",
"NEC", "YNEO", "NEO", "SNC", "NCC", "CLB", "2X2", "DMU", "DMC", "40K", "GN3",
"UNF", "BRO", "BRC", "BOT", "J22", "DMR", "ONE", "ONC",
"MOM", "MOC", "MUL", "MAT", "LTR", "CMM", "WOE", "WHO", "RVR", "WOT",
"WOC", "SPG", "LCI", "LCC", "REX", "PIP", "MKM", "MKC", "CLU", "OTJ",
"OTC", "OTP", "BIG", "MH3", "M3C", "ACR", "BLB"
"OTC", "OTP", "BIG", "MH3", "M3C", "ACR", "BLB", "BLC", "DSK", "DSC",
"MB2", "FDN", "INR", "J25", "DRC", "DFT", "TDC", "TDM", "FCA", "FIC",
"FIN", "SIS", "SIR", "SLD", "AKR", "MD1", "ANB", "LTC", "BRR", "HA1",
"HA2", "HA3", "HA4", "HA5", "ZNC", "EOE", "EOC", "SPE", "TLA", "EOS"
// "HHO", "ANA" -- do not exist on gatherer
};
private static final String[] symbolsOnlyMyth = {
"DRB", "V09", "V10", "V12", "V13", "V14", "V15", "V16", "V17", "EXP", "MED"
"DRB", "V09", "V10", "V12", "V13", "V14", "V15", "V16", "V17", "EXP", "MED", "ZNE"
// "HTR16" does not exist
};
private static final String[] symbolsOnlySpecial = {
"MPS", "MP2"
};
@ -130,6 +139,7 @@ public class GathererSets implements Iterable<DownloadJob> {
codeReplacements.put("APC", "AP");
codeReplacements.put("ARN", "AN");
codeReplacements.put("ATQ", "AQ");
codeReplacements.put("BTD", "BD");
codeReplacements.put("CMA", "CM1");
codeReplacements.put("CHR", "CH");
codeReplacements.put("DVD", "DD3_DVD");
@ -165,14 +175,16 @@ public class GathererSets implements Iterable<DownloadJob> {
codeReplacements.put("UGIN", "FRF_UGIN");
codeReplacements.put("UGL", "UG");
codeReplacements.put("ULG", "GU");
codeReplacements.put("UNF", "UNFS");
codeReplacements.put("USG", "UZ");
codeReplacements.put("VIS", "VI");
codeReplacements.put("WTH", "WL");
codeReplacements.put("YMID", "Y22");
codeReplacements.put("YNEO", "Y22NEO");
}
public GathererSets() {
outDir = new File(getImagesDir() + Constants.RESOURCE_PATH_SYMBOLS);
outDir = new File(getImagesDir() + Constants.RESOURCE_PATH_SYMBOLS_RARITY_DEFAULT_PATH);
if (!outDir.exists()) {
outDir.mkdirs();
@ -287,9 +299,9 @@ public class GathererSets implements Iterable<DownloadJob> {
canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true;
jobs.add(generateDownloadJob(symbol, "C", "C"));
jobs.add(generateDownloadJob(symbol, "U", "U"));
jobs.add(generateDownloadJob(symbol, "R", "R"));
jobs.add(generateDownloadJob(symbol, "C", "common"));
jobs.add(generateDownloadJob(symbol, "U", "uncommon"));
jobs.add(generateDownloadJob(symbol, "R", "rare"));
}
CheckSearchResult(symbol, exp, canDownload, true, true, true, false);
}
@ -299,10 +311,10 @@ public class GathererSets implements Iterable<DownloadJob> {
canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true;
jobs.add(generateDownloadJob(symbol, "C", "C"));
jobs.add(generateDownloadJob(symbol, "U", "U"));
jobs.add(generateDownloadJob(symbol, "R", "R"));
jobs.add(generateDownloadJob(symbol, "M", "M"));
jobs.add(generateDownloadJob(symbol, "C", "common"));
jobs.add(generateDownloadJob(symbol, "U", "uncommon"));
jobs.add(generateDownloadJob(symbol, "R", "rare"));
jobs.add(generateDownloadJob(symbol, "M", "mythic"));
}
CheckSearchResult(symbol, exp, canDownload, true, true, true, true);
}
@ -312,7 +324,7 @@ public class GathererSets implements Iterable<DownloadJob> {
canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true;
jobs.add(generateDownloadJob(symbol, "M", "M"));
jobs.add(generateDownloadJob(symbol, "M", "mythic"));
}
CheckSearchResult(symbol, exp, canDownload, false, false, false, true);
}
@ -322,7 +334,7 @@ public class GathererSets implements Iterable<DownloadJob> {
canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true;
jobs.add(generateDownloadJob(symbol, "M", "S"));
jobs.add(generateDownloadJob(symbol, "M", "special"));
}
CheckSearchResult(symbol, exp, canDownload, false, false, false, true);
}
@ -334,11 +346,22 @@ public class GathererSets implements Iterable<DownloadJob> {
}
private DownloadJob generateDownloadJob(String set, String rarity, String urlRarity) {
File dst = new File(outDir, set + '-' + rarity + ".jpg");
File dst = new File(outDir, set + '-' + rarity + ".png");
if (codeReplacements.containsKey(set)) {
set = codeReplacements.get(set);
}
String url = "https://gatherer.wizards.com/Handlers/Image.ashx?type=symbol&set=" + set + "&size=small&rarity=" + urlRarity;
// example:
// - small: https://gatherer-static.wizards.com/set_symbols/FIN/small-common-FIN.png
// - big: https://gatherer-static.wizards.com/set_symbols/FIN/large-rare-FIN.png
String useSet = set.toUpperCase(Locale.ENGLISH);
String useSize = "large"; // allow: small, large
String url = String.format("https://gatherer-static.wizards.com/set_symbols/%s/%s-%s-%s.png",
useSet,
useSize,
urlRarity,
useSet
);
return new DownloadJob(set + '-' + rarity, url, toFile(dst), false);
}
}

View file

@ -75,7 +75,7 @@ public class GathererSymbols implements Iterable<DownloadJob> {
continue;
}
String symbol = sym.replaceAll("/", "");
File dst = new File(dir, symbol + ".gif");
File dst = new File(dir, symbol + ".png");
// workaround for miss icons on Gatherer (no cards with it, so no icons)
// TODO: comment and try download without workaround, keep fix for symbols with "Resource not found" error

View file

@ -478,8 +478,8 @@ public enum GrabbagImageSource implements CardImageSource {
// Emblems
singleLinks.put("SWS/Emblem Obi-Wan Kenobi", "Qyc10aT.png");
singleLinks.put("SWS/Aurra Sing", "BLWbVJC.png");
singleLinks.put("SWS/Yoda", "zH0sYxg.png");
singleLinks.put("SWS/Emblem Aurra Sing", "BLWbVJC.png");
singleLinks.put("SWS/Emblem Yoda", "zH0sYxg.png");
singleLinks.put("SWS/Emblem Luke Skywalker", "kHELZDJ.jpeg");
// Tokens

View file

@ -590,6 +590,7 @@ public class ScryfallImageSupportCards {
add("FCA"); // Final Fantasy: Through the Ages
add("EOE"); // Edge of Eternities
add("EOC"); // Edge of Eternities Commander
add("EOS"); // Edge of Eternities: Stellar Sights
add("SPE"); // Marvel's Spider-Man Eternal
add("TLA"); // Avatar: The Last Airbender

View file

@ -832,12 +832,18 @@ public class ScryfallImageSupportTokens {
put("SLD/Food/3", "https://api.scryfall.com/cards/sld/2011?format=image");
put("SLD/Food/4", "https://api.scryfall.com/cards/sld/2012?format=image");
put("SLD/Food/5", "https://api.scryfall.com/cards/sld/2013?format=image");
put("SLD/Food/6", "https://api.scryfall.com/cards/sld/2064?format=image");
put("SLD/Goblin", "https://api.scryfall.com/cards/sld/219?format=image");
put("SLD/Hydra", "https://api.scryfall.com/cards/sld/1334?format=image");
put("SLD/Icingdeath, Frost Tongue", "https://api.scryfall.com/cards/sld/1018?format=image");
put("SLD/Marit Lage", "https://api.scryfall.com/cards/sld/1681?format=image");
put("SLD/Mechtitan", "https://api.scryfall.com/cards/sld/1969?format=image");
put("SLD/Myr", "https://api.scryfall.com/cards/sld/2101?format=image");
put("SLD/Saproling", "https://api.scryfall.com/cards/sld/1139?format=image");
put("SLD/Shapeshifter/1", "https://api.scryfall.com/cards/sld/1906?format=image");
put("SLD/Shapeshifter/2", "https://api.scryfall.com/cards/sld/1907?format=image");
put("SLD/Shapeshifter/3", "https://api.scryfall.com/cards/sld/1908?format=image");
put("SLD/Shapeshifter/4", "https://api.scryfall.com/cards/sld/1909?format=image");
put("SLD/Shrine", "https://api.scryfall.com/cards/sld/1835?format=image");
put("SLD/Spirit/1", "https://api.scryfall.com/cards/sld/1341?format=image");
put("SLD/Spirit/2", "https://api.scryfall.com/cards/sld/1852?format=image");
@ -846,6 +852,8 @@ public class ScryfallImageSupportTokens {
put("SLD/Treasure/2", "https://api.scryfall.com/cards/sld/1736/en?format=image");
put("SLD/Treasure/3", "https://api.scryfall.com/cards/sld/1507/en?format=image");
put("SLD/Treasure/4", "https://api.scryfall.com/cards/sld/153/en?format=image");
put("SLD/Treasure/5", "https://api.scryfall.com/cards/sld/2065/en?format=image");
put("SLD/Treasure/6", "https://api.scryfall.com/cards/sld/2094/en?format=image");
put("SLD/Walker/1", "https://api.scryfall.com/cards/sld/148/en?format=image");
put("SLD/Walker/2", "https://api.scryfall.com/cards/sld/149/en?format=image");
put("SLD/Walker/3", "https://api.scryfall.com/cards/sld/150/en?format=image");
@ -2172,23 +2180,16 @@ public class ScryfallImageSupportTokens {
put("WOE/Young Hero", "https://api.scryfall.com/cards/twoe/16/en?format=image");
// WOC
put("WOC/Cat/1", "https://api.scryfall.com/cards/twoc/6/en?format=image");
put("WOC/Cat/2", "https://api.scryfall.com/cards/twoc/5/en?format=image");
put("WOC/Elephant", "https://api.scryfall.com/cards/twoc/13/en?format=image");
put("WOC/Faerie", "https://api.scryfall.com/cards/twoc/10/en?format=image");
put("WOC/Faerie Rogue/1", "https://api.scryfall.com/cards/twoc/11/en?format=image");
put("WOC/Faerie Rogue/2", "https://api.scryfall.com/cards/twoc/16/en?format=image");
put("WOC/Human Monk", "https://api.scryfall.com/cards/twoc/14/en?format=image");
put("WOC/Human Soldier", "https://api.scryfall.com/cards/twoc/7/en?format=image");
put("WOC/Monster", "https://api.scryfall.com/cards/twoc/1/en?format=image");
put("WOC/Ox", "https://api.scryfall.com/cards/twoc/8/en?format=image");
put("WOC/Pegasus", "https://api.scryfall.com/cards/twoc/9/en?format=image");
put("WOC/Pirate", "https://api.scryfall.com/cards/twoc/12/en?format=image");
put("WOC/Royal", "https://api.scryfall.com/cards/twoc/2/en?format=image");
put("WOC/Saproling", "https://api.scryfall.com/cards/twoc/15/en?format=image");
put("WOC/Sorcerer", "https://api.scryfall.com/cards/twoc/3/en?format=image");
put("WOC/Spirit", "https://api.scryfall.com/cards/twoc/17/en?format=image");
put("WOC/Virtuous", "https://api.scryfall.com/cards/twoc/3/en?format=image");
put("WOC/Virtuous", "https://api.scryfall.com/cards/twoc/2/en?format=image");
// WHO
put("WHO/Alien", "https://api.scryfall.com/cards/twho/2?format=image");
@ -2553,7 +2554,8 @@ public class ScryfallImageSupportTokens {
put("DSK/Primo, the Indivisible", "https://api.scryfall.com/cards/tdsk/14?format=image");
put("DSK/Shard", "https://api.scryfall.com/cards/tdsk/2?format=image");
put("DSK/Spider", "https://api.scryfall.com/cards/tdsk/12?format=image");
put("DSK/Spirit", "https://api.scryfall.com/cards/tdsk/8?format=image");
put("DSK/Spirit/1", "https://api.scryfall.com/cards/tdsk/6?format=image");
put("DSK/Spirit/2", "https://api.scryfall.com/cards/tdsk/8?format=image");
put("DSK/Treasure", "https://api.scryfall.com/cards/tdsk/15?format=image");
// DSC
@ -2783,6 +2785,36 @@ public class ScryfallImageSupportTokens {
put("FIC/The Blackjack", "https://api.scryfall.com/cards/tfic/8/en?format=image");
put("FIC/Clue", "https://api.scryfall.com/cards/tfic/9/en?format=image");
// EOE
put("EOE/Drone", "https://api.scryfall.com/cards/teoe/3?format=image");
put("EOE/Human Soldier", "https://api.scryfall.com/cards/teoe/2?format=image");
put("EOE/Lander/1", "https://api.scryfall.com/cards/teoe/4?format=image");
put("EOE/Lander/2", "https://api.scryfall.com/cards/teoe/5?format=image");
put("EOE/Lander/3", "https://api.scryfall.com/cards/teoe/6?format=image");
put("EOE/Lander/4", "https://api.scryfall.com/cards/teoe/7?format=image");
put("EOE/Lander/5", "https://api.scryfall.com/cards/teoe/8?format=image");
put("EOE/Munitions", "https://api.scryfall.com/cards/teoe/9?format=image");
put("EOE/Robot", "https://api.scryfall.com/cards/teoe/10?format=image");
put("EOE/Sliver", "https://api.scryfall.com/cards/teoe/1?format=image");
// EOC
put("EOC/Beast/1", "https://api.scryfall.com/cards/teoc/5/en?format=image");
put("EOC/Beast/2", "https://api.scryfall.com/cards/teoc/6/en?format=image");
put("EOC/Bird", "https://api.scryfall.com/cards/teoc/3/en?format=image");
put("EOC/Clue", "https://api.scryfall.com/cards/teoc/10/en?format=image");
put("EOC/Elemental/1", "https://api.scryfall.com/cards/teoc/7/en?format=image");
put("EOC/Elemental/2", "https://api.scryfall.com/cards/teoc/8/en?format=image");
put("EOC/Gnome", "https://api.scryfall.com/cards/teoc/11/en?format=image");
put("EOC/Golem/1", "https://api.scryfall.com/cards/teoc/12/en?format=image");
put("EOC/Golem/2", "https://api.scryfall.com/cards/teoc/13/en?format=image");
put("EOC/Golem/3", "https://api.scryfall.com/cards/teoc/14/en?format=image");
put("EOC/Incubator", "https://api.scryfall.com/cards/teoc/15/en?format=image&face=front");
put("EOC/Insect", "https://api.scryfall.com/cards/teoc/4/en?format=image");
put("EOC/Pest", "https://api.scryfall.com/cards/teoc/9/en?format=image");
put("EOC/Phyrexian", "https://api.scryfall.com/cards/teoc/15/en?format=image&face=back");
put("EOC/Shapeshifter", "https://api.scryfall.com/cards/teoc/2/en?format=image");
put("EOC/Thopter", "https://api.scryfall.com/cards/teoc/16/en?format=image");
// JVC
put("JVC/Elemental Shaman", "https://api.scryfall.com/cards/tjvc/4?format=image");

View file

@ -157,7 +157,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
@Override
public boolean isNeedCancel() {
return this.needCancel || (this.errorCount > MAX_ERRORS_COUNT_BEFORE_CANCEL) || Thread.interrupted();
return this.needCancel || (this.errorCount > MAX_ERRORS_COUNT_BEFORE_CANCEL) || Thread.currentThread().isInterrupted();
}
private void setNeedCancel(boolean needCancel) {

View file

@ -66,7 +66,6 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View file

@ -1,63 +0,0 @@
package mage.cards.action.impl;
import mage.cards.action.ActionCallback;
import mage.cards.action.TransferData;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
/**
* Callback that does nothing on any action
*
* @author nantuko84
*/
public class EmptyCallback implements ActionCallback {
@Override
public void mouseMoved(MouseEvent e, TransferData data) {
}
@Override
public void mouseDragged(MouseEvent e, TransferData data) {
}
@Override
public void mouseEntered(MouseEvent e, TransferData data) {
}
@Override
public void mouseExited(MouseEvent e, TransferData data) {
}
@Override
public void mouseWheelMoved(int mouseWheelRotation, TransferData data) {
}
@Override
public void hideOpenComponents() {
}
@Override
public void mouseClicked(MouseEvent e, TransferData data, boolean doubleClick) {
}
@Override
public void mousePressed(MouseEvent e, TransferData data) {
}
@Override
public void mouseReleased(MouseEvent e, TransferData data) {
}
@Override
public void popupMenuCard(MouseEvent e, TransferData data) {
}
@Override
public void popupMenuPanel(MouseEvent e, Component sourceComponent) {
}
}

View file

@ -1,72 +0,0 @@
package mage.filters;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
/**
* Mage abstract class that implements single-input/single-output
* operations performed on {@link java.awt.image.BufferedImage}.
*
* @author nantuko
*/
public abstract class MageBufferedImageOp implements BufferedImageOp {
/**
* Creates compatible image for @param src image.
*/
@Override
public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dest) {
if (dest == null) {
dest = src.getColorModel();
}
return new BufferedImage(dest, dest.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dest.isAlphaPremultiplied(), null);
}
@Override
public RenderingHints getRenderingHints() {
return null;
}
@Override
public Rectangle2D getBounds2D(BufferedImage src) {
return new Rectangle(0, 0, src.getWidth(), src.getHeight());
}
@Override
public Point2D getPoint2D(Point2D srcPt, Point2D destPt) {
if (destPt == null) {
destPt = new Point2D.Double();
}
destPt.setLocation(srcPt.getX(), srcPt.getY());
return destPt;
}
/**
* Gets ARGB pixels from image. Solves the performance
* issue of BufferedImage.getRGB method.
*/
public int[] getRGB(BufferedImage image, int x, int y, int width, int height, int[] pixels) {
int type = image.getType();
if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB) {
return (int[]) image.getRaster().getDataElements(x, y, width, height, pixels);
}
return image.getRGB(x, y, width, height, pixels, 0, width);
}
/**
* Sets ARGB pixels in image. Solves the performance
* issue of BufferedImage.setRGB method.
*/
public void setRGB(BufferedImage image, int x, int y, int width, int height, int[] pixels) {
int type = image.getType();
if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB) {
image.getRaster().setDataElements(x, y, width, height, pixels);
} else {
image.setRGB(x, y, width, height, pixels, 0, width);
}
}
}

View file

@ -723,7 +723,7 @@ public class SessionImpl implements Session {
public Optional<UUID> getRoomChatId(UUID roomId) {
try {
if (isConnected()) {
return Optional.of(server.chatFindByRoom(roomId));
return Optional.ofNullable(server.chatFindByRoom(roomId));
}
} catch (MageException ex) {
handleMageException(ex);
@ -735,7 +735,7 @@ public class SessionImpl implements Session {
public Optional<UUID> getTableChatId(UUID tableId) {
try {
if (isConnected()) {
return Optional.of(server.chatFindByTable(tableId));
return Optional.ofNullable(server.chatFindByTable(tableId));
}
} catch (MageException ex) {
handleMageException(ex);
@ -747,7 +747,7 @@ public class SessionImpl implements Session {
public Optional<UUID> getGameChatId(UUID gameId) {
try {
if (isConnected()) {
return Optional.of(server.chatFindByGame(gameId));
return Optional.ofNullable(server.chatFindByGame(gameId));
}
} catch (MageException ex) {
handleMageException(ex);
@ -761,7 +761,7 @@ public class SessionImpl implements Session {
public Optional<TableView> getTable(UUID roomId, UUID tableId) {
try {
if (isConnected()) {
return Optional.of(server.roomGetTableById(roomId, tableId));
return Optional.ofNullable(server.roomGetTableById(roomId, tableId));
}
} catch (MageException ex) {
handleMageException(ex);
@ -905,7 +905,7 @@ public class SessionImpl implements Session {
public Optional<UUID> getTournamentChatId(UUID tournamentId) {
try {
if (isConnected()) {
return Optional.of(server.chatFindByTournament(tournamentId));
return Optional.ofNullable(server.chatFindByTournament(tournamentId));
}
} catch (MageException ex) {
handleMageException(ex);

View file

@ -1,11 +0,0 @@
package mage.utils;
import mage.view.TableView;
/**
* Used to write less code for ActionWithResult anonymous classes with UUID return type.
*
* @author noxx
*/
public abstract class ActionWithUUIDResult extends ActionWithNullNegativeResult<TableView> {
}

View file

@ -9,7 +9,11 @@ import java.util.List;
*/
public class AmountTestableResult extends BaseTestableResult {
int amount = 0;
Integer amount = null;
boolean aiAssertEnabled = false;
int aiAssertMinAmount = 0;
int aiAssertMaxAmount = 0;
public void onFinish(String resDebugSource, boolean status, List<String> info, int amount) {
this.onFinish(resDebugSource, status, info);
@ -18,12 +22,38 @@ public class AmountTestableResult extends BaseTestableResult {
@Override
public String getResAssert() {
return null; // TODO: implement
if (!this.aiAssertEnabled) {
return null;
}
// not finished
if (this.amount == null) {
return null;
}
if (!this.getResStatus()) {
return String.format("Wrong status: need %s, but get %s",
true, // res must be true all the time
this.getResStatus()
);
}
// wrong amount
if (this.amount < this.aiAssertMinAmount || this.amount > this.aiAssertMaxAmount) {
return String.format("Wrong amount: need [%d, %d], but get %d",
this.aiAssertMinAmount,
this.aiAssertMaxAmount,
this.amount
);
}
// all fine
return "";
}
@Override
public void onClear() {
super.onClear();
this.amount = 0;
this.amount = null;
}
}

View file

@ -34,6 +34,15 @@ class AnnounceXTestableDialog extends BaseTestableDialog {
this.max = max;
}
private AnnounceXTestableDialog aiMustChoose(int minAmount, int maxAmount) {
// require min/max cause AI logic uses random choices
AmountTestableResult res = ((AmountTestableResult) this.getResult());
res.aiAssertEnabled = true;
res.aiAssertMinAmount = minAmount;
res.aiAssertMaxAmount = maxAmount;
return this;
}
@Override
public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent;
@ -51,17 +60,17 @@ class AnnounceXTestableDialog extends BaseTestableDialog {
List<Boolean> isManas = Arrays.asList(false, true);
for (boolean isYou : isYous) {
for (boolean isMana : isManas) {
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 0));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 1));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 3));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 50));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 500));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 1));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 3));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 50));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 3));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 10));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 10, 10));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 0).aiMustChoose(0, 0));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 1).aiMustChoose(0, 1));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 3).aiMustChoose(0, 3));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 50).aiMustChoose(0, 50));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 500).aiMustChoose(0, 500));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 1).aiMustChoose(1, 1));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 3).aiMustChoose(1, 3));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 50).aiMustChoose(1, 50));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 3).aiMustChoose(3, 3));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 10).aiMustChoose(3, 10));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 10, 10).aiMustChoose(10, 10));
}
}
}

View file

@ -36,6 +36,15 @@ class GetAmountTestableDialog extends BaseTestableDialog {
this.max = max;
}
private GetAmountTestableDialog aiMustChoose(int minAmount, int maxAmount) {
// require min/max cause AI logic uses random choices
AmountTestableResult res = ((AmountTestableResult) this.getResult());
res.aiAssertEnabled = true;
res.aiAssertMinAmount = minAmount;
res.aiAssertMaxAmount = maxAmount;
return this;
}
@Override
public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent;
@ -51,17 +60,20 @@ class GetAmountTestableDialog extends BaseTestableDialog {
static public void register(TestableDialogsRunner runner) {
List<Boolean> isYous = Arrays.asList(false, true);
for (boolean isYou : isYous) {
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 0));
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 1));
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 3));
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 50));
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 500));
runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 1));
runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 3));
runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 50));
runner.registerDialog(new GetAmountTestableDialog(isYou, 3, 3));
runner.registerDialog(new GetAmountTestableDialog(isYou, 3, 10));
runner.registerDialog(new GetAmountTestableDialog(isYou, 10, 10));
// TODO: add good and bad effects:
// - on good: choose random big value
// - on bad: choose lower value
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 0).aiMustChoose(0, 0));
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 1).aiMustChoose(0, 1));
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 3).aiMustChoose(0, 3));
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 50).aiMustChoose(0, 50));
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 500).aiMustChoose(0, 500));
runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 1).aiMustChoose(1, 1));
runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 3).aiMustChoose(1, 3));
runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 50).aiMustChoose(1, 50));
runner.registerDialog(new GetAmountTestableDialog(isYou, 3, 3).aiMustChoose(3, 3));
runner.registerDialog(new GetAmountTestableDialog(isYou, 3, 10).aiMustChoose(3, 10));
runner.registerDialog(new GetAmountTestableDialog(isYou, 10, 10).aiMustChoose(10, 10));
}
}
}

View file

@ -53,7 +53,9 @@ class GetMultiAmountTestableDialog extends BaseTestableDialog {
}
private GetMultiAmountTestableDialog aiMustChoose(Integer... needValues) {
// TODO: AI use default distribution (min possible values), improve someday
// TODO: AI use default distribution:
// - bad effect: min possible values
// - good effect: max possible and distributed values
MultiAmountTestableResult res = ((MultiAmountTestableResult) this.getResult());
res.aiAssertEnabled = true;
res.aiAssertValues = Arrays.stream(needValues).collect(Collectors.toList());

View file

@ -25,7 +25,12 @@ public class ChatMessage implements Serializable {
}
public enum MessageType {
USER_INFO, STATUS, GAME, TALK, WHISPER_FROM, WHISPER_TO
USER_INFO, // system messages
STATUS, // system messages
GAME, // game logs
TALK, // public chat
WHISPER_FROM, // private chat income
WHISPER_TO // private chat outcome
}
public enum SoundToPlay {

View file

@ -5,11 +5,13 @@ import mage.abilities.Ability;
import mage.abilities.common.CanBeYourCommanderAbility;
import mage.abilities.common.CommanderChooseColorAbility;
import mage.abilities.keyword.CompanionAbility;
import mage.abilities.keyword.StationLevelAbility;
import mage.cards.Card;
import mage.cards.decks.Constructed;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckValidatorErrorType;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.FilterMana;
import mage.util.CardUtil;
import mage.util.ManaUtil;
@ -51,9 +53,19 @@ public abstract class AbstractCommander extends Constructed {
protected abstract boolean checkBanned(Map<String, Integer> counts);
protected boolean checkCommander(Card commander, Set<Card> commanders) {
return commander.hasCardTypeForDeckbuilding(CardType.CREATURE) && commander.isLegendary()
|| commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance())
|| (validators.stream().anyMatch(validator -> validator.specialCheck(commander)) && commanders.size() == 2);
if (commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance())) {
return true;
}
if (commander.isLegendary()
&& (commander.hasCardTypeForDeckbuilding(CardType.CREATURE)
|| commander.hasSubTypeForDeckbuilding(SubType.VEHICLE)
|| commander.hasSubTypeForDeckbuilding(SubType.SPACECRAFT)
&& CardUtil
.castStream(commander.getAbilities(), StationLevelAbility.class)
.anyMatch(StationLevelAbility::hasPT))) {
return true;
}
return commanders.size() == 2 && validators.stream().anyMatch(validator -> validator.specialCheck(commander));
}
protected boolean checkPartners(Set<Card> commanders) {

View file

@ -19,6 +19,7 @@ public class CanadianHighlander extends Constructed {
static {
pointMap.put("Ancestral Recall", 8);
pointMap.put("Ancient Tomb", 1);
pointMap.put("Balance", 1);
pointMap.put("Black Lotus", 7);
pointMap.put("Demonic Tutor", 3);
pointMap.put("Dig Through Time", 1);
@ -30,19 +31,22 @@ public class CanadianHighlander extends Constructed {
pointMap.put("Mana Crypt", 5);
pointMap.put("Mana Drain", 1);
pointMap.put("Mana Vault", 1);
pointMap.put("Merchant Scroll", 1);
pointMap.put("Minsc & Boo, Timeless Heroes", 1);
pointMap.put("Mox Emerald", 3);
pointMap.put("Mox Jet", 3);
pointMap.put("Mox Pearl", 3);
pointMap.put("Mox Ruby", 3);
pointMap.put("Mox Sapphire", 3);
pointMap.put("Mystical Tutor", 1);
pointMap.put("Nadu, Winged Wisdom", 1);
pointMap.put("Natural Order", 1);
pointMap.put("Sol Ring", 4);
pointMap.put("Spellseeker", 1);
pointMap.put("Strip Mine", 2);
pointMap.put("Survival of the Fittest", 1);
pointMap.put("Psychic Frog", 1);
pointMap.put("Reanimate", 1);
pointMap.put("Sol Ring", 3);
pointMap.put("Strip Mine", 1);
pointMap.put("Tainted Pact", 1);
pointMap.put("Thassa's Oracle", 7);
pointMap.put("Thassa's Oracle", 6);
pointMap.put("Time Vault", 7);
pointMap.put("Time Walk", 6);
pointMap.put("Tinker", 3);
@ -50,8 +54,11 @@ public class CanadianHighlander extends Constructed {
pointMap.put("Treasure Cruise", 1);
pointMap.put("True-Name Nemesis", 1);
pointMap.put("Underworld Breach", 3);
pointMap.put("Urza's Saga", 1);
pointMap.put("Vampiric Tutor", 2);
pointMap.put("White Plume Adventurer", 1);
pointMap.put("Wishclaw Talisman", 1);
pointMap.put("Wrenn and Six", 1);
}
public CanadianHighlander() {

View file

@ -0,0 +1,34 @@
package mage.deck;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckValidator;
import mage.cards.decks.DeckValidatorErrorType;
/**
* @author resech
*/
public class FreeformUnlimited extends DeckValidator {
public FreeformUnlimited() {
this("Constructed - Freeform Unlimited", null);
}
public FreeformUnlimited(String name, String shortName) {
super(name, shortName);
}
@Override
public int getDeckMinSize() {
return 0;
}
@Override
public int getSideboardMinSize() {
return 0;
}
@Override
public boolean validate(Deck deck) {
return true;
}
}

View file

@ -32,7 +32,6 @@ public class Historic extends Constructed {
banned.add("Agent of Treachery");
banned.add("Brainstorm");
banned.add("Channel");
banned.add("Counterspell");
banned.add("Dark Ritual");
banned.add("Demonic Tutor");
banned.add("Fires of Invention");

View file

@ -18,6 +18,14 @@ public class Standard extends Constructed {
super("Constructed - Standard");
setCodes.addAll(makeLegalSets());
banned.add("Abuelo's Awakening");
banned.add("Cori-Steel Cutter");
banned.add("Heartfire Hero");
banned.add("Hopeless Nightmare");
banned.add("Monstrous Rage");
banned.add("This Town Ain't Big Enough");
banned.add("Up the Beanstalk");
}
static List<String> makeLegalSets() {

View file

@ -114,6 +114,13 @@ public class ComputerPlayer6 extends ComputerPlayer {
this.actionCache = player.actionCache;
}
/**
* Change simulation timeout - used for AI stability tests only
*/
public void setMaxThinkTimeSecs(int maxThinkTimeSecs) {
this.maxThinkTimeSecs = maxThinkTimeSecs;
}
@Override
public ComputerPlayer6 copy() {
return new ComputerPlayer6(this);
@ -206,10 +213,8 @@ public class ComputerPlayer6 extends ComputerPlayer {
logger.trace("Add Action [" + depth + "] " + node.getAbilities().toString() + " a: " + alpha + " b: " + beta);
}
Game game = node.getGame();
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
&& Thread.interrupted()) {
Thread.currentThread().interrupt();
logger.debug("interrupted");
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.currentThread().isInterrupted()) {
logger.debug("AI game sim interrupted by timeout");
return GameStateEvaluator2.evaluate(playerId, game).getTotalScore();
}
// Condition to stop deeper simulation
@ -431,6 +436,8 @@ public class ComputerPlayer6 extends ComputerPlayer {
* @return
*/
protected Integer addActionsTimed() {
// TODO: all actions added and calculated one by one,
// multithreading do not supported here
// run new game simulation in parallel thread
FutureTask<Integer> task = new FutureTask<>(() -> addActions(root, maxDepth, Integer.MIN_VALUE, Integer.MAX_VALUE));
threadPoolSimulations.execute(task);
@ -446,15 +453,23 @@ public class ComputerPlayer6 extends ComputerPlayer {
}
} catch (TimeoutException | InterruptedException e) {
// AI thinks too long
logger.info("ai simulating - timed out");
// how-to fix: look at stack info - it can contain bad ability with infinite choose dialog
logger.warn("");
logger.warn("AI player thinks too long (report it to github):");
logger.warn(" - player: " + getName());
logger.warn(" - battlefield size: " + root.game.getBattlefield().getAllPermanents().size());
logger.warn(" - stack: " + root.game.getStack());
logger.warn(" - game: " + root.game);
printFreezeNode(root);
logger.warn("");
task.cancel(true);
} catch (ExecutionException e) {
// game error
logger.error("AI simulation catch game error: " + e, e);
logger.error("AI player catch game error in simulation - " + getName() + " - " + root.game + ": " + e, e);
task.cancel(true);
// real games: must catch and log
// unit tests: must raise again for fast fail
if (this.isTestsMode()) {
if (this.isTestMode() && this.isFastFailInTestMode()) {
throw new IllegalStateException("One of the simulated games raise the error: " + e, e);
}
} catch (Throwable e) {
@ -466,11 +481,33 @@ public class ComputerPlayer6 extends ComputerPlayer {
return 0;
}
private void printFreezeNode(SimulationNode2 root) {
// print simple tree - there are possible multiple child nodes, but ignore it - same for abilities
List<String> chain = new ArrayList<>();
SimulationNode2 node = root;
while (node != null) {
if (node.abilities != null && !node.abilities.isEmpty()) {
Ability ability = node.abilities.get(0);
String sourceInfo = CardUtil.getSourceIdName(node.game, ability);
chain.add(String.format("%s: %s",
(sourceInfo.isEmpty() ? "unknown" : sourceInfo),
ability
));
}
node = node.children == null || node.children.isEmpty() ? null : node.children.get(0);
}
logger.warn("Possible freeze chain:");
if (root != null && chain.isEmpty()) {
logger.warn(" - unknown use case"); // maybe can't finish any calc, maybe related to target options, I don't know
}
chain.forEach(s -> {
logger.warn(" - " + s);
});
}
protected int simulatePriority(SimulationNode2 node, Game game, int depth, int alpha, int beta) {
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
&& Thread.interrupted()) {
Thread.currentThread().interrupt();
logger.info("interrupted");
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.currentThread().isInterrupted()) {
logger.debug("AI game sim interrupted by timeout");
return GameStateEvaluator2.evaluate(playerId, game).getTotalScore();
}
node.setGameValue(game.getState().getValue(true).hashCode());
@ -498,9 +535,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
int bestValSubNodes = Integer.MIN_VALUE;
for (Ability action : allActions) {
actionNumber++;
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
&& Thread.interrupted()) {
Thread.currentThread().interrupt();
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.currentThread().isInterrupted()) {
logger.info("Sim Prio [" + depth + "] -- interrupted");
break;
}

View file

@ -142,7 +142,8 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
}
}
} else {
logger.info('[' + game.getPlayer(playerId).getName() + "][pre] Action: skip");
// nothing to choose or freeze/infinite game
logger.info("AI player can't find next action: " + getName());
}
} else {
logger.debug("Next Action exists!");

View file

@ -52,7 +52,7 @@ public class ComputerPlayer extends PlayerImpl {
protected static final int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available
// debug only: set TRUE to debug simulation's code/games (on false sim thread will be stopped after few secs by timeout)
protected static final boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = true; // DebugUtil.AI_ENABLE_DEBUG_MODE;
public static final boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = false; // DebugUtil.AI_ENABLE_DEBUG_MODE;
// AI agents uses game simulation thread for all calcs and it's high CPU consumption
// More AI threads - more parallel AI games can be calculate
@ -64,7 +64,7 @@ public class ComputerPlayer extends PlayerImpl {
// * use yours CPU cores for best performance
// TODO: add server config to control max AI threads (with CPU cores by default)
// TODO: rework AI implementation to use multiple sims calculation instead one by one
final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 1;//DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5;
final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 5;//DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5;
// remember picked cards for better draft choices
@ -104,7 +104,7 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean chooseMulligan(Game game) {
if (hand.size() < 6
|| isTestsMode() // ignore mulligan in tests
|| isTestMode() // ignore mulligan in tests
|| game.getClass().getName().contains("Momir") // ignore mulligan in Momir games
) {
return false;

View file

@ -45,9 +45,8 @@ public class PossibleTargetsSelector {
public void findNewTargets(Set<UUID> fromTargetsList) {
// collect new valid targets
List<MageItem> found = target.possibleTargets(abilityControllerId, source, game).stream()
List<MageItem> found = target.possibleTargets(abilityControllerId, source, game, fromTargetsList).stream()
.filter(id -> !target.contains(id))
.filter(id -> fromTargetsList == null || fromTargetsList.contains(id))
.filter(id -> target.canTarget(abilityControllerId, id, source, game))
.map(id -> {
Player player = game.getPlayer(id);

View file

@ -196,7 +196,7 @@ public class ComputerPlayerMCTS extends ComputerPlayer {
} catch (ExecutionException e) {
// real games: must catch and log
// unit tests: must raise again for fast fail
if (this.isTestsMode()) {
if (this.isTestMode() && this.isFastFailInTestMode()) {
throw new IllegalStateException("One of the simulated games raise the error: " + e, e);
}
}

View file

@ -198,6 +198,7 @@
<deckType name="Constructed - Old School 93/94 - EC Rules" jar="mage-deck-constructed.jar" className="mage.deck.OldSchool9394EC"/>
<deckType name="Constructed - Premodern" jar="mage-deck-constructed.jar" className="mage.deck.Premodern"/>
<deckType name="Constructed - Freeform" jar="mage-deck-constructed.jar" className="mage.deck.Freeform"/>
<deckType name="Constructed - Freeform Unlimited" jar="mage-deck-constructed.jar" className="mage.deck.FreeformUnlimited"/>
<deckType name="Variant Magic - Commander" jar="mage-deck-constructed.jar" className="mage.deck.Commander"/>
<deckType name="Variant Magic - Duel Commander" jar="mage-deck-constructed.jar" className="mage.deck.DuelCommander"/>
<deckType name="Variant Magic - MTGO 1v1 Commander" jar="mage-deck-constructed.jar" className="mage.deck.MTGO1v1Commander"/>

View file

@ -192,11 +192,6 @@
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
@ -213,12 +208,12 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>[1.19,)</version>
<version>1.27.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.8.0</version>
<version>1.13.0</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>

View file

@ -192,6 +192,7 @@
<deckType name="Constructed - Old School 93/94 - EC Rules" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.OldSchool9394EC"/>
<deckType name="Constructed - Premodern" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.Premodern"/>
<deckType name="Constructed - Freeform" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.Freeform"/>
<deckType name="Constructed - Freeform Unlimited" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.FreeformUnlimited"/>
<deckType name="Variant Magic - Commander" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.Commander"/>
<deckType name="Variant Magic - Duel Commander" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.DuelCommander"/>
<deckType name="Variant Magic - MTGO 1v1 Commander" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.MTGO1v1Commander"/>

View file

@ -4,6 +4,8 @@ import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.constants.Constants;
import mage.game.Game;
import mage.game.Table;
import mage.game.tournament.Tournament;
import mage.server.game.GameController;
import mage.server.managers.ChatManager;
import mage.server.managers.ManagerFactory;
@ -40,11 +42,39 @@ public class ChatManagerImpl implements ChatManager {
this.managerFactory = managerFactory;
}
@Override
public UUID createChatSession(String info) {
public UUID createRoomChatSession(UUID roomId) {
return createChatSession("Room " + roomId)
.withRoom(roomId)
.getChatId();
}
@Override
public UUID createTourneyChatSession(Tournament tournament) {
return createChatSession("Tourney " + tournament.getId())
.withTourney(tournament)
.getChatId();
}
@Override
public UUID createTableChatSession(Table table) {
return createChatSession("Table " + table.getId())
.withTable(table)
.getChatId();
}
@Override
public UUID createGameChatSession(Game game) {
return createChatSession("Game " + game.getId())
.withGame(game)
.getChatId();
}
private ChatSession createChatSession(String info) {
ChatSession chatSession = new ChatSession(managerFactory, info);
chatSessions.put(chatSession.getChatId(), chatSession);
return chatSession.getChatId();
return chatSession;
}
@Override
@ -94,7 +124,7 @@ public class ChatManagerImpl implements ChatManager {
ChatSession chatSession = chatSessions.get(chatId);
Optional<User> user = managerFactory.userManager().getUserByName(userName);
if (chatSession != null) {
// special commads
// special commands
if (message.startsWith("\\") || message.startsWith("/")) {
if (user.isPresent()) {
if (!performUserCommand(user.get(), message, chatId, false)) {

View file

@ -1,6 +1,9 @@
package mage.server;
import mage.collectors.DataCollectorServices;
import mage.game.Game;
import mage.game.Table;
import mage.game.tournament.Tournament;
import mage.interfaces.callback.ClientCallback;
import mage.interfaces.callback.ClientCallbackMethod;
import mage.server.managers.ManagerFactory;
@ -27,6 +30,13 @@ public class ChatSession {
private final ManagerFactory managerFactory;
private final ReadWriteLock lock = new ReentrantReadWriteLock(); // TODO: no needs due ConcurrentHashMap usage?
// only 1 field must be filled per chat type
// TODO: rework chat sessions to share logic (one server room/lobby + one table/subtable + one games/match)
private UUID roomId = null;
private UUID tourneyId = null;
private UUID tableId = null;
private UUID gameId = null;
private final ConcurrentMap<UUID, String> users = new ConcurrentHashMap<>(); // active users
private final Set<UUID> usersHistory = new HashSet<>(); // all users that was here (need for system messages like connection problem)
private final UUID chatId;
@ -40,6 +50,26 @@ public class ChatSession {
this.info = info;
}
public ChatSession withRoom(UUID roomId) {
this.roomId = roomId;
return this;
}
public ChatSession withTourney(Tournament tournament) {
this.tourneyId = tournament.getId();
return this;
}
public ChatSession withTable(Table table) {
this.tableId = table.getId();
return this;
}
public ChatSession withGame(Game game) {
this.gameId = game.getId();
return this;
}
public void join(UUID userId) {
managerFactory.userManager().getUser(userId).ifPresent(user -> {
if (!users.containsKey(userId)) {
@ -112,9 +142,36 @@ public class ChatSession {
// TODO: is it freeze on someone's connection fail/freeze with play multiple games/chats/lobby?
// TODO: send messages in another thread?!
if (!message.isEmpty()) {
ChatMessage chatMessage = new ChatMessage(userName, message, (withTime ? new Date() : null), game, color, messageType, soundToPlay);
switch (messageType) {
case USER_INFO:
case STATUS:
case TALK:
if (this.roomId != null) {
DataCollectorServices.getInstance().onChatRoom(this.roomId, userName, message);
} else if (this.tourneyId != null) {
DataCollectorServices.getInstance().onChatTourney(this.tourneyId, userName, message);
} else if (this.tableId != null) {
DataCollectorServices.getInstance().onChatTable(this.tableId, userName, message);
} else if (this.gameId != null) {
DataCollectorServices.getInstance().onChatGame(this.gameId, userName, message);
}
break;
case GAME:
// game logs processing in other place
break;
case WHISPER_FROM:
case WHISPER_TO:
// ignore private messages
break;
default:
throw new IllegalStateException("Unsupported message type " + messageType);
}
// TODO: wtf, remove all that locks/tries and make it simpler
Set<UUID> clientsToRemove = new HashSet<>();
ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
new ChatMessage(userName, message, (withTime ? new Date() : null), game, color, messageType, soundToPlay));
ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, chatMessage);
List<UUID> chatUserIds = new ArrayList<>();
final Lock r = lock.readLock();
r.lock();

View file

@ -5,6 +5,7 @@ import mage.cards.decks.DeckCardLists;
import mage.cards.decks.DeckValidatorFactory;
import mage.cards.repository.CardRepository;
import mage.cards.repository.ExpansionRepository;
import mage.collectors.DataCollectorServices;
import mage.constants.Constants;
import mage.constants.ManaType;
import mage.constants.PlayerAction;
@ -29,6 +30,7 @@ import mage.server.managers.ManagerFactory;
import mage.server.services.impl.FeedbackServiceImpl;
import mage.server.tournament.TournamentFactory;
import mage.server.util.ServerMessagesUtil;
import mage.util.DebugUtil;
import mage.utils.*;
import mage.view.*;
import mage.view.ChatMessage.MessageColor;
@ -68,6 +70,13 @@ public class MageServerImpl implements MageServer {
this.detailsMode = detailsMode;
this.callExecutor = managerFactory.threadExecutor().getCallExecutor();
ServerMessagesUtil.instance.getMessages();
// additional logs
DataCollectorServices.init(
DebugUtil.SERVER_DATA_COLLECTORS_ENABLE_PRINT_GAME_LOGS,
DebugUtil.SERVER_DATA_COLLECTORS_ENABLE_SAVE_GAME_HISTORY
);
DataCollectorServices.getInstance().onServerStart();
}
@Override

View file

@ -14,7 +14,7 @@ public abstract class RoomImpl implements Room {
public RoomImpl(ChatManager chatManager) {
roomId = UUID.randomUUID();
chatId = chatManager.createChatSession("Room " + roomId);
chatId = chatManager.createRoomChatSession(roomId);
}
/**

View file

@ -8,7 +8,6 @@ import mage.util.ThreadUtils;
import org.apache.log4j.Logger;
import org.jboss.remoting.callback.InvokerCallbackHandler;
import javax.annotation.Nonnull;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@ -29,7 +28,7 @@ public class SessionManagerImpl implements SessionManager {
}
@Override
public Optional<Session> getSession(@Nonnull String sessionId) {
public Optional<Session> getSession(String sessionId) {
return Optional.ofNullable(sessions.getOrDefault(sessionId, null));
}
@ -180,12 +179,12 @@ public class SessionManagerImpl implements SessionManager {
}
@Override
public boolean isValidSession(@Nonnull String sessionId) {
public boolean isValidSession(String sessionId) {
return sessions.containsKey(sessionId);
}
@Override
public Optional<User> getUser(@Nonnull String sessionId) {
public Optional<User> getUser(String sessionId) {
Session session = sessions.get(sessionId);
if (session != null) {
return managerFactory.userManager().getUser(sessions.get(sessionId).getUserId());

View file

@ -72,7 +72,7 @@ public class TableController {
}
this.table = new Table(roomId, options.getGameType(), options.getName(), controllerName, DeckValidatorFactory.instance.createDeckValidator(options.getDeckType()),
options.getPlayerTypes(), new TableRecorderImpl(managerFactory.userManager()), match, options.getBannedUsers(), options.isPlaneChase());
this.chatId = managerFactory.chatManager().createChatSession("Match Table " + table.getId());
this.chatId = managerFactory.chatManager().createTableChatSession(table);
init();
}
@ -94,7 +94,7 @@ public class TableController {
}
table = new Table(roomId, options.getTournamentType(), options.getName(), controllerName, DeckValidatorFactory.instance.createDeckValidator(options.getMatchOptions().getDeckType()),
options.getPlayerTypes(), new TableRecorderImpl(managerFactory.userManager()), tournament, options.getMatchOptions().getBannedUsers(), options.isPlaneChase());
chatId = managerFactory.chatManager().createChatSession("Tourney table " + table.getId());
chatId = managerFactory.chatManager().createTableChatSession(table);
}
private void init() {

View file

@ -1,25 +0,0 @@
package mage.server.challenge;
import mage.constants.Zone;
import mage.game.match.Match;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* C U R R E N T L Y U N U S E D
*
* Loads challenges from scenarios.
* Configure games by initializing starting game board.
*/
public enum ChallengeManager {
instance;
public void prepareChallenge(UUID playerId, Match match) {
Map<Zone, String> commands = new HashMap<>();
commands.put(Zone.OUTSIDE, "life:3");
match.getGame().cheat(playerId, commands);
}
}

View file

@ -1,7 +0,0 @@
package mage.server.exceptions;
/**
* Created by igoudt on 14-1-2017.
*/
public class UserNotFoundException extends Exception {
}

View file

@ -85,11 +85,11 @@ public class GameController implements GameCallback {
public GameController(ManagerFactory managerFactory, Game game, ConcurrentMap<UUID, UUID> userPlayerMap, UUID tableId, UUID choosingPlayerId, GameOptions gameOptions) {
this.managerFactory = managerFactory;
gameExecutor = managerFactory.threadExecutor().getGameExecutor();
responseIdleTimeoutExecutor = managerFactory.threadExecutor().getTimeoutIdleExecutor();
gameSessionId = UUID.randomUUID();
this.gameExecutor = managerFactory.threadExecutor().getGameExecutor();
this.responseIdleTimeoutExecutor = managerFactory.threadExecutor().getTimeoutIdleExecutor();
this.gameSessionId = UUID.randomUUID();
this.userPlayerMap = userPlayerMap;
chatId = managerFactory.chatManager().createChatSession("Game " + game.getId());
this.chatId = managerFactory.chatManager().createGameChatSession(game);
this.userRequestingRollback = null;
this.game = game;
this.game.setSaveGame(managerFactory.configSettings().isSaveGameActivated());

View file

@ -1,9 +1,10 @@
package mage.server.managers;
import mage.game.Game;
import mage.game.Table;
import mage.game.tournament.Tournament;
import mage.server.ChatSession;
import mage.server.DisconnectReason;
import mage.server.exceptions.UserNotFoundException;
import mage.view.ChatMessage;
import java.util.List;
@ -11,7 +12,13 @@ import java.util.UUID;
public interface ChatManager {
UUID createChatSession(String info);
UUID createRoomChatSession(UUID roomId);
UUID createTourneyChatSession(Tournament tournament);
UUID createTableChatSession(Table table);
UUID createGameChatSession(Game game);
void joinChat(UUID chatId, UUID userId);

View file

@ -7,12 +7,11 @@ import mage.server.Session;
import mage.server.User;
import org.jboss.remoting.callback.InvokerCallbackHandler;
import javax.annotation.Nonnull;
import java.util.Optional;
public interface SessionManager {
Optional<Session> getSession(@Nonnull String sessionId);
Optional<Session> getSession(String sessionId);
void createSession(String sessionId, InvokerCallbackHandler callbackHandler);
@ -37,9 +36,9 @@ public interface SessionManager {
boolean checkAdminAccess(String sessionId);
boolean isValidSession(@Nonnull String sessionId);
boolean isValidSession(String sessionId);
Optional<User> getUser(@Nonnull String sessionId);
Optional<User> getUser(String sessionId);
boolean extendUserSession(String sessionId, String pingInfo);

View file

@ -1,18 +0,0 @@
package mage.server.services;
/**
* Responsible for gathering logs and storing them in DB.
*
* @author noxx
*/
@FunctionalInterface
public interface LogService {
/**
* Logs any information
*
* @param key Log key. Should be the same for the same types of logs.
* @param args Any parameters in string representation.
*/
void log(String key, String... args);
}

View file

@ -1,18 +0,0 @@
package mage.server.services;
/**
* Common interface for all services.
*
* @author noxx
*/
public interface MageService {
/**
* Restores data on startup.
*/
void initService();
/**
* Dumps data to DB.
*/
void saveData();
}

View file

@ -54,7 +54,7 @@ public class TournamentController {
public TournamentController(ManagerFactory managerFactory, Tournament tournament, ConcurrentMap<UUID, UUID> userPlayerMap, UUID tableId) {
this.managerFactory = managerFactory;
this.userPlayerMap = userPlayerMap;
chatId = managerFactory.chatManager().createChatSession("Tournament " + tournament.getId());
this.chatId = managerFactory.chatManager().createTourneyChatSession(tournament);
this.tournament = tournament;
this.tableId = tableId;
init();
@ -261,9 +261,7 @@ public class TournamentController {
table.setState(TableState.STARTING);
tableManager.startTournamentSubMatch(null, table.getId());
tableManager.getMatch(table.getId()).ifPresent(match -> {
match.setTableId(tableId);
pair.setMatch(match);
pair.setTableId(table.getId());
pair.setMatchAndTable(match, table.getId());
player1.setState(TournamentPlayerState.DUELING);
player2.setState(TournamentPlayerState.DUELING);
});
@ -291,9 +289,7 @@ public class TournamentController {
table.setState(TableState.STARTING);
tableManager.startTournamentSubMatch(null, table.getId());
tableManager.getMatch(table.getId()).ifPresent(match -> {
match.setTableId(tableId);
round.setMatch(match);
round.setTableId(table.getId());
round.setMatchAndTable(match, table.getId());
for (TournamentPlayer player : round.getAllPlayers()) {
player.setState(TournamentPlayerState.DUELING);
}

View file

@ -79,7 +79,7 @@ class AbsoluteVirtueAbility extends ProtectionAbility {
.ofNullable(source)
.map(MageItem::getId)
.map(game::getControllerId)
.map(uuid -> !game.getOpponents(this.getControllerId()).contains(uuid))
.map(uuid -> !game.getOpponents(this.getSourceId()).contains(uuid))
.orElse(true);
}
}

View file

@ -0,0 +1,62 @@
package mage.cards.a;
import mage.abilities.Ability;
import mage.abilities.common.ActivateAsSorceryActivatedAbility;
import mage.abilities.common.EntersBattlefieldTappedAbility;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.CreateTokenCopyTargetEffect;
import mage.abilities.keyword.StationAbility;
import mage.abilities.keyword.StationLevelAbility;
import mage.abilities.mana.WhiteManaAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.filter.StaticFilters;
import mage.target.TargetPermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class AdagiaWindsweptBastion extends CardImpl {
public AdagiaWindsweptBastion(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.LAND}, "");
this.subtype.add(SubType.PLANET);
// This land enters tapped.
this.addAbility(new EntersBattlefieldTappedAbility());
// {T}: Add {W}.
this.addAbility(new WhiteManaAbility());
// Station
this.addAbility(new StationAbility());
// STATION 12+
// {3}{W}, {T}: Create a token that's a copy of target artifact or enchantment you control, except it's legendary. Activate only as a sorcery.
Ability ability = new ActivateAsSorceryActivatedAbility(
new CreateTokenCopyTargetEffect()
.setPermanentModifier(token -> token.addSuperType(SuperType.LEGENDARY))
.setText("create a token that's a copy of target artifact or enchantment you control, except it's legendary"),
new ManaCostsImpl<>("{3}{W}")
);
ability.addCost(new TapSourceCost());
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CONTROLLED_ARTIFACT_OR_ENCHANTMENT));
this.addAbility(new StationLevelAbility(12).withLevelAbility(ability));
}
private AdagiaWindsweptBastion(final AdagiaWindsweptBastion card) {
super(card);
}
@Override
public AdagiaWindsweptBastion copy() {
return new AdagiaWindsweptBastion(this);
}
}

View file

@ -36,7 +36,7 @@ public final class AdelineResplendentCathar extends CardImpl {
// Adeline, Resplendent Cathar's power is equal to the number of creatures you control.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerSourceEffect(
CreaturesYouControlCount.instance)).addHint(CreaturesYouControlHint.instance)
CreaturesYouControlCount.PLURAL)).addHint(CreaturesYouControlHint.instance)
);
// Whenever you attack, for each opponent, create a 1/1 white Human creature token that's tapped and attacking that player or a planeswalker they control.

View file

@ -3,36 +3,26 @@
package mage.cards.a;
import java.util.UUID;
import mage.abilities.effects.common.DestroyTargetEffect;
import mage.abilities.effects.common.GainLifeEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.filter.StaticFilters;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
*
* @author LevelX2
*/
public final class AerialPredation extends CardImpl {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature with flying");
static {
filter.add(new AbilityPredicate(FlyingAbility.class));
}
public AerialPredation(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{2}{G}");
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{G}");
// Destroy target creature with flying. You gain 2 life.
this.getSpellAbility().addTarget(new TargetPermanent(filter));
this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_CREATURE_FLYING));
this.getSpellAbility().addEffect(new DestroyTargetEffect());
this.getSpellAbility().addEffect(new GainLifeEffect(2));
}

View file

@ -7,34 +7,23 @@ import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.SacrificeSourceCost;
import mage.abilities.dynamicvalue.common.SourcePermanentPowerValue;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.PersistAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.filter.StaticFilters;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
*
* @author jeffwadsworth
*/
public final class AerieOuphes extends CardImpl {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature with flying");
static {
filter.add(new AbilityPredicate(FlyingAbility.class));
}
public AerieOuphes(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{4}{G}");
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}");
this.subtype.add(SubType.OUPHE);
this.power = new MageInt(3);
@ -43,7 +32,7 @@ public final class AerieOuphes extends CardImpl {
// Sacrifice Aerie Ouphes: Aerie Ouphes deals damage equal to its power to target creature with flying.
Ability ability = new SimpleActivatedAbility(new DamageTargetEffect(SourcePermanentPowerValue.NOT_NEGATIVE)
.setText("it deals damage equal to its power to target creature with flying"), new SacrificeSourceCost());
ability.addTarget(new TargetPermanent(filter));
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CREATURE_FLYING));
this.addAbility(ability);
// Persist

View file

@ -1,19 +1,22 @@
package mage.cards.a;
import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.common.SpellCastControllerTriggeredAbility;
import mage.abilities.costs.common.PayEnergyCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.counter.GetEnergyCountersControllerEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.cards.CardsImpl;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.counters.CounterType;
import mage.filter.StaticFilters;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.stack.Spell;
import mage.players.Player;
@ -34,8 +37,8 @@ public final class AetherfluxConduit extends CardImpl {
this.addAbility(new SpellCastControllerTriggeredAbility(new AetherfluxConduitManaEffect(), false));
// {T}, Pay fifty {E}: Draw seven cards. You may cast any number of spells from your hand without paying their mana costs.
final Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(7), new TapSourceCost());
ability.addCost(new PayEnergyCost(50).setText("Pay fifty {E}"));
Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(7), new TapSourceCost());
ability.addCost(new PayEnergyCost(50).setText("pay fifty {E}"));
ability.addEffect(new AetherfluxConduitCastEffect());
this.addAbility(ability);
}
@ -68,10 +71,21 @@ class AetherfluxConduitManaEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Optional.ofNullable(this.getValue("spellCast"))
int amount = Optional
.ofNullable(this.getValue("spellCast"))
.map(Spell.class::cast)
.ifPresent(spell -> new GetEnergyCountersControllerEffect(spell.getManaValue()).apply(game, source));
return true;
.map(Spell::getStackAbility)
.map(Ability::getManaCostsToPay)
.map(ManaCost::getUsedManaToPay)
.map(Mana::count)
.orElse(0);
return amount > 0
&& Optional
.ofNullable(source)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.filter(player -> player.addCounters(CounterType.ENERGY.createInstance(amount), player.getId(), source, game))
.isPresent();
}
}

View file

@ -1,12 +1,8 @@
package mage.cards.a;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.common.ActivateAsSorceryActivatedAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.OneShotEffect;
@ -15,6 +11,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.TimingRule;
import mage.counters.Counter;
import mage.counters.CounterType;
import mage.game.Game;
@ -22,6 +19,10 @@ import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author sobiech
*/
@ -35,8 +36,9 @@ public final class AethericAmplifier extends CardImpl {
// {4}, {T}: Choose one. Activate only as a sorcery.
// * Double the number of each kind of counter on target permanent.
final Ability ability = new ActivateAsSorceryActivatedAbility(new AethericAmplifierDoublePermanentEffect(), new GenericManaCost(4))
.withShowActivateText(false);
Ability ability = new SimpleActivatedAbility(
new AethericAmplifierDoublePermanentEffect(), new GenericManaCost(4)
).setTiming(TimingRule.SORCERY);
ability.addCost(new TapSourceCost());
ability.addTarget(new TargetPermanent());
ability.getModes().setChooseText("choose one. Activate only as a sorcery.");
@ -145,5 +147,3 @@ class AethericAmplifierDoubleControllerEffect extends OneShotEffect {
return new AethericAmplifierDoubleControllerEffect(this);
}
}

View file

@ -35,7 +35,7 @@ public final class AetherworksMarvel extends CardImpl {
this.addAbility(new PutIntoGraveFromBattlefieldAllTriggeredAbility(
new GetEnergyCountersControllerEffect(1), false,
StaticFilters.FILTER_CONTROLLED_A_PERMANENT, false
));
).setTriggerPhrase("Whenever a permanent you control is put into a graveyard, "));
// {T}, Pay {E}{E}{E}{E}{E}{E}: Look at the top six cards of your library.
// You may cast a card from among them without paying its mana cost.

View file

@ -61,18 +61,22 @@ class AettirAndPriwenEffect extends ContinuousEffectImpl {
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
Permanent permanent = Optional
.ofNullable(source.getSourcePermanentIfItStillExists(game))
.map(Permanent::getAttachedTo)
.map(game::getPermanent)
.orElse(null);
if (permanent == null) {
return false;
}
int life = Optional
.ofNullable(source)
Optional.ofNullable(source)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.map(Player::getLife)
.orElse(0);
permanent.getPower().setModifiedBaseValue(life);
permanent.getToughness().setModifiedBaseValue(life);
.ifPresent(life -> {
permanent.getPower().setModifiedBaseValue(life);
permanent.getToughness().setModifiedBaseValue(life);
});
return true;
}
}

View file

@ -32,7 +32,7 @@ public final class AgentOfAcquisitions extends CardImpl {
// Instead of drafting a card from a booster pack, you may draft each card in that booster pack, one at a time. If you do, turn Agent of Acquisitions face down and you cant draft cards for the rest of this draft round.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new InfoEffect("Instead of drafting a card from a booster pack, "
+ "you may draft each card in that booster pack, one at a time. If you do, turn Agent of Acquisitions face down and "
+ "you may draft each card in that booster pack, one at a time. If you do, turn {this} face down and "
+ "you can't draft cards for the rest of this draft round - not implemented.")));
}

View file

@ -2,7 +2,6 @@
package mage.cards.a;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
@ -13,33 +12,27 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.filter.StaticFilters;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public final class AirServant extends CardImpl {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature with flying");
static {
filter.add(new AbilityPredicate(FlyingAbility.class));
}
public AirServant(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{4}{U}");
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{U}");
this.subtype.add(SubType.ELEMENTAL);
this.power = new MageInt(4);
this.toughness = new MageInt(3);
this.addAbility(FlyingAbility.getInstance());
Ability ability = new SimpleActivatedAbility(new TapTargetEffect(), new ManaCostsImpl<>("{2}{U}"));
ability.addTarget(new TargetPermanent(filter));
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CREATURE_FLYING));
this.addAbility(ability);
}

View file

@ -110,7 +110,7 @@ class AjaniNacatlAvengerZeroEffect extends OneShotEffect {
}
ReflexiveTriggeredAbility reflexive = new ReflexiveTriggeredAbility(
new DamageTargetEffect(CreaturesYouControlCount.instance),
new DamageTargetEffect(CreaturesYouControlCount.PLURAL),
false,
"When you do, if you control a red permanent other than {this}, "
+ "he deals damage equal to the number of creatures you control to any target.",

View file

@ -31,7 +31,7 @@ public final class AjaniWiseCounselor extends CardImpl {
this.setStartingLoyalty(5);
// +2: You gain 1 life for each creature you control.
this.addAbility(new LoyaltyAbility(new GainLifeEffect(CreaturesYouControlCount.instance)
this.addAbility(new LoyaltyAbility(new GainLifeEffect(CreaturesYouControlCount.PLURAL)
.setText("you gain 1 life for each creature you control"), 2));
// 3: Creatures you control get +2/+2 until end of turn.

View file

@ -0,0 +1,46 @@
package mage.cards.a;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.SacrificeSourceCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.dynamicvalue.common.DifferentlyNamedPermanentCount;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.mana.AnyColorManaAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class AllFatesScroll extends CardImpl {
private static final DifferentlyNamedPermanentCount xValue = new DifferentlyNamedPermanentCount(StaticFilters.FILTER_CONTROLLED_PERMANENT_LANDS);
public AllFatesScroll(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}");
// {T}: Add one mana of any color.
this.addAbility(new AnyColorManaAbility());
// {7}, {T}, Sacrifice this artifact: Draw X cards, where X is the number of differently named lands you control.
Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(xValue), new GenericManaCost(7));
ability.addCost(new TapSourceCost());
ability.addCost(new SacrificeSourceCost());
this.addAbility(ability.addHint(xValue.getHint()));
}
private AllFatesScroll(final AllFatesScroll card) {
super(card);
}
@Override
public AllFatesScroll copy() {
return new AllFatesScroll(this);
}
}

View file

@ -0,0 +1,55 @@
package mage.cards.a;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.ExileUntilSourceLeavesEffect;
import mage.abilities.keyword.WarpAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.Predicates;
import mage.target.TargetPermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class AllFatesStalker extends CardImpl {
private static final FilterPermanent filter = new FilterCreaturePermanent("non-Assassin creature");
static {
filter.add(Predicates.not(SubType.ASSASSIN.getPredicate()));
}
public AllFatesStalker(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}");
this.subtype.add(SubType.DRIX);
this.subtype.add(SubType.ASSASSIN);
this.power = new MageInt(2);
this.toughness = new MageInt(3);
// When this creature enters, exile up to one target non-Assassin creature until this creature leaves the battlefield.
Ability ability = new EntersBattlefieldTriggeredAbility(new ExileUntilSourceLeavesEffect());
ability.addTarget(new TargetPermanent(0, 1, filter));
this.addAbility(ability);
// Warp {1}{W}
this.addAbility(new WarpAbility(this, "{1}{W}"));
}
private AllFatesStalker(final AllFatesStalker card) {
super(card);
}
@Override
public AllFatesStalker copy() {
return new AllFatesStalker(this);
}
}

View file

@ -0,0 +1,49 @@
package mage.cards.a;
import mage.MageInt;
import mage.abilities.common.AttacksTriggeredAbility;
import mage.abilities.condition.common.VoidCondition;
import mage.abilities.costs.common.DiscardCardCost;
import mage.abilities.effects.common.LoseHalfLifeTargetEffect;
import mage.abilities.keyword.WardAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.watchers.common.VoidWatcher;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class AlpharaelStonechosen extends CardImpl {
public AlpharaelStonechosen(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.CLERIC);
this.power = new MageInt(3);
this.toughness = new MageInt(3);
// Ward--Discard a card at random.
this.addAbility(new WardAbility(new DiscardCardCost(true)));
// Void -- Whenever Alpharael attacks, if a nonland permanent left the battlefield this turn or a spell was warped this turn, defending player loses half their life, rounded up.
this.addAbility(new AttacksTriggeredAbility(
new LoseHalfLifeTargetEffect()
.setText("defending player loses half their life, rounded up"),
false, null, SetTargetPointer.PLAYER
).withInterveningIf(VoidCondition.instance).setAbilityWord(AbilityWord.VOID).addHint(VoidCondition.getHint()), new VoidWatcher());
}
private AlpharaelStonechosen(final AlpharaelStonechosen card) {
super(card);
}
@Override
public AlpharaelStonechosen copy() {
return new AlpharaelStonechosen(this);
}
}

View file

@ -27,7 +27,7 @@ public final class AngelOfRenewal extends CardImpl {
// Flying
this.addAbility(FlyingAbility.getInstance());
// When Angel of Renewal enters the battlefield, you gain 1 life for each creature you control.
this.addAbility(new EntersBattlefieldTriggeredAbility(new GainLifeEffect(CreaturesYouControlCount.instance).setText("you gain 1 life for each creature you control")));
this.addAbility(new EntersBattlefieldTriggeredAbility(new GainLifeEffect(CreaturesYouControlCount.PLURAL).setText("you gain 1 life for each creature you control")));
}
private AngelOfRenewal(final AngelOfRenewal card) {

View file

@ -21,7 +21,7 @@ public final class AngelicExaltation extends CardImpl {
// Whenever a creature you control attacks alone, it gets +X/+X until end of turn, where X is the number of creatures you control.
this.addAbility(new AttacksAloneControlledTriggeredAbility(
new BoostTargetEffect(CreaturesYouControlCount.instance, CreaturesYouControlCount.instance, Duration.EndOfTurn),
new BoostTargetEffect(CreaturesYouControlCount.PLURAL, CreaturesYouControlCount.PLURAL, Duration.EndOfTurn),
true, false).addHint(CreaturesYouControlHint.instance));
}

View file

@ -0,0 +1,71 @@
package mage.cards.a;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.LeavesBattlefieldTriggeredAbility;
import mage.abilities.dynamicvalue.common.LandsYouControlCount;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.PutCardFromHandOntoBattlefieldEffect;
import mage.abilities.hint.common.LandsYouControlHint;
import mage.abilities.keyword.WarpAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.FilterCard;
import mage.filter.common.FilterPermanentCard;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.game.Game;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class AnticausalVestige extends CardImpl {
private static final FilterCard filter = new FilterPermanentCard(
"a permanent card with mana value less than or equal to the number of lands you control"
);
static {
filter.add(AnticausalVestigePredicate.instance);
}
public AnticausalVestige(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{6}");
this.subtype.add(SubType.ELDRAZI);
this.power = new MageInt(7);
this.toughness = new MageInt(5);
// When this creature leaves the battlefield, draw a card, then you may put a permanent card with mana value less than or equal to the number of lands you control from your hand onto the battlefield tapped.
Ability ability = new LeavesBattlefieldTriggeredAbility(new DrawCardSourceControllerEffect(1));
ability.addEffect(new PutCardFromHandOntoBattlefieldEffect(filter, false, true).concatBy(", then"));
this.addAbility(ability.addHint(LandsYouControlHint.instance));
// Warp {4}
this.addAbility(new WarpAbility(this, "{4}"));
}
private AnticausalVestige(final AnticausalVestige card) {
super(card);
}
@Override
public AnticausalVestige copy() {
return new AnticausalVestige(this);
}
}
enum AnticausalVestigePredicate implements ObjectSourcePlayerPredicate<Card> {
instance;
@Override
public boolean apply(ObjectSourcePlayer<Card> input, Game game) {
return input.getObject().getManaValue()
<= LandsYouControlCount.instance.calculate(game, input.getSource(), null);
}
}

View file

@ -32,7 +32,7 @@ public final class AppealAuthority extends SplitCard {
// Until end of turn, target creature gains trample and gets +X/+X, where X is the number of creatures you control.
getLeftHalfCard().getSpellAbility().addEffect(new GainAbilityTargetEffect(TrampleAbility.getInstance(), Duration.EndOfTurn)
.setText("Until end of turn, target creature gains trample"));
getLeftHalfCard().getSpellAbility().addEffect(new BoostTargetEffect(CreaturesYouControlCount.instance, CreaturesYouControlCount.instance, Duration.EndOfTurn)
getLeftHalfCard().getSpellAbility().addEffect(new BoostTargetEffect(CreaturesYouControlCount.PLURAL, CreaturesYouControlCount.PLURAL, Duration.EndOfTurn)
.setText("and gets +X/+X, where X is the number of creatures you control"));
getLeftHalfCard().getSpellAbility().addTarget(new TargetCreaturePermanent());
getLeftHalfCard().getSpellAbility().addHint(CreaturesYouControlHint.instance);

View file

@ -1,7 +1,6 @@
package mage.cards.a;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
@ -11,31 +10,23 @@ import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.common.DamageAllEffect;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.abilities.keyword.ChannelAbility;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.filter.StaticFilters;
import mage.target.TargetPermanent;
import java.util.UUID;
/**
*
* @author LevelX2
*/
public final class ArashiTheSkyAsunder extends CardImpl {
static final private FilterCreaturePermanent filter = new FilterCreaturePermanent("creature with flying");
static {
filter.add(new AbilityPredicate(FlyingAbility.class));
}
public ArashiTheSkyAsunder(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{G}{G}");
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}{G}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.SPIRIT);
@ -45,11 +36,11 @@ public final class ArashiTheSkyAsunder extends CardImpl {
// {X}{G}, {tap}: Arashi, the Sky Asunder deals X damage to target creature with flying.
Ability ability = new SimpleActivatedAbility(new DamageTargetEffect(GetXValue.instance), new ManaCostsImpl<>("{X}{G}"));
ability.addCost(new TapSourceCost());
ability.addTarget(new TargetPermanent(filter));
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CREATURE_FLYING));
this.addAbility(ability);
// Channel - {X}{G}{G}, Discard Arashi: Arashi deals X damage to each creature with flying.
this.addAbility(new ChannelAbility("{X}{G}{G}", new DamageAllEffect(GetXValue.instance, filter)));
this.addAbility(new ChannelAbility("{X}{G}{G}", new DamageAllEffect(GetXValue.instance, StaticFilters.FILTER_CREATURE_FLYING)));
}
private ArashiTheSkyAsunder(final ArashiTheSkyAsunder card) {

View file

@ -0,0 +1,62 @@
package mage.cards.a;
import mage.abilities.Mode;
import mage.abilities.effects.common.ExileTargetEffect;
import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.abilities.keyword.LifelinkAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.counters.CounterType;
import mage.filter.FilterCard;
import mage.filter.predicate.Predicates;
import mage.target.common.TargetCardInYourGraveyard;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetCreatureOrPlaneswalker;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class ArchenemysCharm extends CardImpl {
private static final FilterCard filter = new FilterCard("creature and/or planeswalker cards from your graveyard");
static {
filter.add(Predicates.or(
CardType.CREATURE.getPredicate(),
CardType.PLANESWALKER.getPredicate()
));
}
public ArchenemysCharm(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{B}{B}{B}");
// Choose one --
// * Exile target creature or planeswalker.
this.getSpellAbility().addEffect(new ExileTargetEffect());
this.getSpellAbility().addTarget(new TargetCreatureOrPlaneswalker());
// * Return one or two target creature and/or planeswalker cards from your graveyard to your hand.
this.getSpellAbility().addMode(new Mode(new ReturnFromGraveyardToHandTargetEffect())
.addTarget(new TargetCardInYourGraveyard(1, 2, filter)));
// * Put two +1/+1 counters on target creature you control. It gains lifelink until end of turn.
this.getSpellAbility().addMode(new Mode(new AddCountersTargetEffect(CounterType.P1P1.createInstance(2)))
.addEffect(new GainAbilityTargetEffect(LifelinkAbility.getInstance())
.setText("It gains lifelink until end of turn"))
.addTarget(new TargetControlledCreaturePermanent()));
}
private ArchenemysCharm(final ArchenemysCharm card) {
super(card);
}
@Override
public ArchenemysCharm copy() {
return new ArchenemysCharm(this);
}
}

View file

@ -12,7 +12,6 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.target.common.TargetControlledCreaturePermanent;
/**
*
@ -26,7 +25,7 @@ public final class AshnodsAltar extends CardImpl {
// Sacrifice a creature: Add {C}{C}.
SacrificeTargetCost cost = new SacrificeTargetCost(StaticFilters.FILTER_PERMANENT_CREATURE);
this.addAbility(new SimpleManaAbility(Zone.BATTLEFIELD,
new BasicManaEffect(Mana.ColorlessMana(2), CreaturesYouControlCount.instance),
new BasicManaEffect(Mana.ColorlessMana(2), CreaturesYouControlCount.PLURAL),
cost));
}

View file

@ -1,7 +1,6 @@
package mage.cards.a;
import java.util.UUID;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DamageTargetEffect;
@ -12,6 +11,8 @@ import mage.constants.SpellAbilityType;
import mage.game.permanent.token.ElephantToken;
import mage.target.common.TargetAnyTarget;
import java.util.UUID;
public final class AssaultBattery extends SplitCard {
public AssaultBattery(UUID ownerId, CardSetInfo setInfo) {
@ -20,7 +21,7 @@ public final class AssaultBattery extends SplitCard {
// Assault
// Assault deals 2 damage to any target.
Effect effect = new DamageTargetEffect(2);
effect.setText("Assault deals 2 damage to any target");
effect.setText("{this} deals 2 damage to any target");
getLeftHalfCard().getSpellAbility().addEffect(effect);
getLeftHalfCard().getSpellAbility().addTarget(new TargetAnyTarget());

View file

@ -1,18 +1,13 @@
package mage.cards.a;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CounterUnlessPaysEffect;
import mage.abilities.effects.keyword.IncubateEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.filter.FilterSpell;
import mage.filter.predicate.Predicates;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetSpell;
import java.util.UUID;
@ -35,7 +30,10 @@ public final class AssimilateEssence extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{U}");
// Counter target creature or battle spell unless its controller pays {4}. If they do, you incubate 2.
this.getSpellAbility().addEffect(new AssimilateEssenceEffect());
this.getSpellAbility().addEffect(
new CounterUnlessPaysEffect(new GenericManaCost(4))
.withIfTheyDo(new IncubateEffect(2).setText("you incubate 2"))
);
this.getSpellAbility().addTarget(new TargetSpell(filter));
}
@ -47,36 +45,4 @@ public final class AssimilateEssence extends CardImpl {
public AssimilateEssence copy() {
return new AssimilateEssence(this);
}
}
class AssimilateEssenceEffect extends OneShotEffect {
AssimilateEssenceEffect() {
super(Outcome.Benefit);
staticText = "counter target creature or battle spell unless its controller pays {4}. If they do, you incubate 2";
}
private AssimilateEssenceEffect(final AssimilateEssenceEffect effect) {
super(effect);
}
@Override
public AssimilateEssenceEffect copy() {
return new AssimilateEssenceEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
UUID targetId = getTargetPointer().getFirst(game, source);
Player player = game.getPlayer(game.getControllerId(targetId));
Cost cost = new GenericManaCost(4);
if (player == null
|| !cost.canPay(source, source, player.getId(), game)
|| !player.chooseUse(outcome, "Pay {4}?", source, game)
|| !cost.pay(source, game, source, player.getId(), false)) {
game.getStack().counter(targetId, source, game);
return true;
}
return IncubateEffect.doIncubate(2, source.getControllerId(), game, source);
}
}
}

View file

@ -0,0 +1,80 @@
package mage.cards.a;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.WarpAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.FilterCard;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.PermanentPredicate;
import mage.game.Game;
import mage.target.common.TargetCardInYourGraveyard;
import mage.watchers.common.ManaPaidSourceWatcher;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class AstelliReclaimer extends CardImpl {
private static final FilterCard filter = new FilterCard(
"noncreature, nonland permanent card with mana value X or less"
);
static {
filter.add(Predicates.not(CardType.CREATURE.getPredicate()));
filter.add(Predicates.not(CardType.LAND.getPredicate()));
filter.add(PermanentPredicate.instance);
filter.add(AstelliReclaimerPredicate.instance);
}
public AstelliReclaimer(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}{W}");
this.subtype.add(SubType.ANGEL);
this.subtype.add(SubType.WARRIOR);
this.power = new MageInt(5);
this.toughness = new MageInt(4);
// Flying
this.addAbility(FlyingAbility.getInstance());
// When this creature enters, return target noncreature, nonland permanent card with mana value X or less from your graveyard to the battlefield, where X is the amount of mana spent to cast this creature.
Ability ability = new EntersBattlefieldTriggeredAbility(new ReturnFromGraveyardToBattlefieldTargetEffect()
.setText("return target noncreature, nonland permanent card with mana value X or less from " +
"your graveyard to the battlefield, where X is the amount of mana spent to cast this creature"));
ability.addTarget(new TargetCardInYourGraveyard(filter));
this.addAbility(ability);
// Warp {2}{W}
this.addAbility(new WarpAbility(this, "{2}{W}"));
}
private AstelliReclaimer(final AstelliReclaimer card) {
super(card);
}
@Override
public AstelliReclaimer copy() {
return new AstelliReclaimer(this);
}
}
enum AstelliReclaimerPredicate implements ObjectSourcePlayerPredicate<Card> {
instance;
@Override
public boolean apply(ObjectSourcePlayer<Card> input, Game game) {
return input.getObject().getManaValue() <= ManaPaidSourceWatcher.getTotalPaid(input.getSourceId(), game);
}
}

View file

@ -9,8 +9,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.FilterPermanent;
import mage.filter.predicate.Predicates;
import mage.filter.StaticFilters;
import mage.target.TargetPermanent;
import java.util.UUID;
@ -20,12 +19,6 @@ import java.util.UUID;
*/
public final class AstralDragon extends CardImpl {
private static final FilterPermanent filter = new FilterPermanent("noncreature permanent");
static {
filter.add(Predicates.not(CardType.CREATURE.getPredicate()));
}
public AstralDragon(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{6}{U}{U}");
@ -37,14 +30,13 @@ public final class AstralDragon extends CardImpl {
this.addAbility(FlyingAbility.getInstance());
// Project Image When Astral Dragon enters the battlefield, create two tokens that are copies of target noncreature permanent, except they're 3/3 Dragon creatures in addition to their other types, and they have flying.
CreateTokenCopyTargetEffect effect = new CreateTokenCopyTargetEffect(
Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenCopyTargetEffect(
null, CardType.CREATURE, false, 2, false,
false, null, 3, 3, true);
effect.setText("create two tokens that are copies of target noncreature permanent, " +
"except they're 3/3 Dragon creatures in addition to their other types, and they have flying");
effect.withAdditionalSubType(SubType.DRAGON);
Ability ability = new EntersBattlefieldTriggeredAbility(effect);
ability.addTarget(new TargetPermanent(filter));
false, null, 3, 3, true
).withAdditionalSubType(SubType.DRAGON)
.setText("create two tokens that are copies of target noncreature permanent, " +
"except they're 3/3 Dragon creatures in addition to their other types, and they have flying"));
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_NON_CREATURE));
this.addAbility(ability.withFlavorWord("Project Image"));
}

View file

@ -0,0 +1,54 @@
package mage.cards.a;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.counter.AddCountersAllEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.StationAbility;
import mage.abilities.keyword.StationLevelAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.counters.CounterType;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class AtmosphericGreenhouse extends CardImpl {
public AtmosphericGreenhouse(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}{G}");
this.subtype.add(SubType.SPACECRAFT);
// When this Spacecraft enters, put a +1/+1 counter on each creature you control.
this.addAbility(new EntersBattlefieldTriggeredAbility(new AddCountersAllEffect(
CounterType.P1P1.createInstance(), StaticFilters.FILTER_CONTROLLED_CREATURE
)));
// Station
this.addAbility(new StationAbility());
// STATION 8+
// Flying
// Trample
// 5/4
this.addAbility(new StationLevelAbility(8)
.withLevelAbility(FlyingAbility.getInstance())
.withLevelAbility(TrampleAbility.getInstance())
.withPT(5, 4));
}
private AtmosphericGreenhouse(final AtmosphericGreenhouse card) {
super(card);
}
@Override
public AtmosphericGreenhouse copy() {
return new AtmosphericGreenhouse(this);
}
}

View file

@ -0,0 +1,52 @@
package mage.cards.a;
import mage.abilities.Ability;
import mage.abilities.common.AttacksAttachedTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.combat.CantBeBlockedTargetEffect;
import mage.abilities.effects.common.continuous.BoostEquippedEffect;
import mage.abilities.effects.common.continuous.SetBasePowerToughnessTargetEffect;
import mage.abilities.keyword.EquipAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class AtomicMicrosizer extends CardImpl {
public AtomicMicrosizer(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{U}");
this.subtype.add(SubType.EQUIPMENT);
// Equipped creature gets +1/+0.
this.addAbility(new SimpleStaticAbility(new BoostEquippedEffect(1, 0)));
// Whenever equipped creature attacks, choose up to one target creature. That creature can't be blocked this turn and has base power and toughness 1/1 until end of turn.
Ability ability = new AttacksAttachedTriggeredAbility(new CantBeBlockedTargetEffect()
.setText("choose up to one target creature. That creature can't be blocked this turn"));
ability.addEffect(new SetBasePowerToughnessTargetEffect(1, 1, Duration.EndOfTurn)
.setText("and has base power and toughness 1/1 until end of turn"));
ability.addTarget(new TargetCreaturePermanent(0, 1));
this.addAbility(ability);
// Equip {2}
this.addAbility(new EquipAbility(2));
}
private AtomicMicrosizer(final AtomicMicrosizer card) {
super(card);
}
@Override
public AtomicMicrosizer copy() {
return new AtomicMicrosizer(this);
}
}

View file

@ -1,19 +1,14 @@
package mage.cards.a;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.dynamicvalue.common.DifferentlyNamedPermanentCount;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueHint;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.PermanentToken;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.predicate.permanent.TokenPredicate;
import mage.game.permanent.token.PlantToken;
import java.util.UUID;
@ -23,14 +18,22 @@ import java.util.UUID;
*/
public final class AudienceWithTrostani extends CardImpl {
private static final FilterPermanent filter = new FilterControlledCreaturePermanent("creature tokens you control");
static {
filter.add(TokenPredicate.TRUE);
}
private static final DifferentlyNamedPermanentCount xValue = new DifferentlyNamedPermanentCount(filter);
public AudienceWithTrostani(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{G}");
// Create a 0/1 green Plant creature token, then draw cards equal to the number of differently named creature tokens you control.
this.getSpellAbility().addEffect(new CreateTokenEffect(new PlantToken()));
this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(AudienceWithTrostaniValue.instance)
this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(xValue)
.setText(", then draw cards equal to the number of differently named creature tokens you control"));
this.getSpellAbility().addHint(AudienceWithTrostaniValue.getHint());
this.getSpellAbility().addHint(xValue.getHint());
}
private AudienceWithTrostani(final AudienceWithTrostani card) {
@ -42,46 +45,3 @@ public final class AudienceWithTrostani extends CardImpl {
return new AudienceWithTrostani(this);
}
}
enum AudienceWithTrostaniValue implements DynamicValue {
instance;
private static final Hint hint = new ValueHint(
"Different names among creature tokens you control", instance
);
public static Hint getHint() {
return hint;
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return game
.getBattlefield()
.getActivePermanents(
StaticFilters.FILTER_CONTROLLED_CREATURE,
sourceAbility.getControllerId(), sourceAbility, game
)
.stream()
.filter(PermanentToken.class::isInstance)
.map(MageObject::getName)
.filter(s -> !s.isEmpty())
.distinct()
.mapToInt(x -> 1)
.sum();
}
@Override
public AudienceWithTrostaniValue copy() {
return this;
}
@Override
public String getMessage() {
return "";
}
@Override
public String toString() {
return "1";
}
}

View file

@ -44,7 +44,7 @@ public final class AurraSingBaneOfJedi extends CardImpl {
ability.addTarget(new TargetCreaturePermanent());
this.addAbility(ability);
// -4: Target player gets an emblem wiht "Whenever a nontoken creature you control leave the battlefied, discard a card.".
// -4: Target player gets an emblem with "Whenever a nontoken creature you control leave the battlefied, discard a card.".
ability = new LoyaltyAbility(new GetEmblemTargetPlayerEffect(new AurraSingBaneOfJediEmblem()), -4);
ability.addTarget(new TargetPlayer());
this.addAbility(ability);

View file

@ -0,0 +1,52 @@
package mage.cards.a;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.CreateTokenAttachSourceEffect;
import mage.abilities.effects.common.continuous.BoostEquippedEffect;
import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect;
import mage.abilities.keyword.EquipAbility;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.AttachmentType;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.game.permanent.token.RobotToken;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class AuxiliaryBoosters extends CardImpl {
public AuxiliaryBoosters(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}{W}");
this.subtype.add(SubType.EQUIPMENT);
// When this Equipment enters, create a 2/2 colorless Robot artifact creature token and attach this Equipment to it.
this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenAttachSourceEffect(new RobotToken())));
// Equipped creature gets +1/+2 and has flying.
Ability ability = new SimpleStaticAbility(new BoostEquippedEffect(1, 2));
ability.addEffect(new GainAbilityAttachedEffect(
FlyingAbility.getInstance(), AttachmentType.EQUIPMENT
).setText("and has flying"));
this.addAbility(ability);
// Equip {3}
this.addAbility(new EquipAbility(3));
}
private AuxiliaryBoosters(final AuxiliaryBoosters card) {
super(card);
}
@Override
public AuxiliaryBoosters copy() {
return new AuxiliaryBoosters(this);
}
}

View file

@ -1,29 +1,25 @@
package mage.cards.a;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.dynamicvalue.common.DifferentlyNamedPermanentCount;
import mage.abilities.effects.common.continuous.SetBasePowerToughnessSourceEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
*
* @author LevelX2
*/
public final class AwakenedAmalgam extends CardImpl {
private static final DifferentlyNamedPermanentCount xValue = new DifferentlyNamedPermanentCount(StaticFilters.FILTER_CONTROLLED_PERMANENT_LANDS);
public AwakenedAmalgam(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{4}");
@ -32,8 +28,9 @@ public final class AwakenedAmalgam extends CardImpl {
this.toughness = new MageInt(0);
// Awakened Amalgam's power and toughness are each equal to the number of differently named lands you control.
DynamicValue value = (new AwakenedAmalgamLandNamesCount());
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(value)));
this.addAbility(new SimpleStaticAbility(
Zone.ALL, new SetBasePowerToughnessSourceEffect(xValue)
).addHint(xValue.getHint()));
}
private AwakenedAmalgam(final AwakenedAmalgam card) {
@ -45,35 +42,3 @@ public final class AwakenedAmalgam extends CardImpl {
return new AwakenedAmalgam(this);
}
}
class AwakenedAmalgamLandNamesCount implements DynamicValue {
public AwakenedAmalgamLandNamesCount() {
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
Set<String> landNames = new HashSet<>();
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(sourceAbility.getControllerId())) {
if (permanent.isLand(game)) {
landNames.add(permanent.getName());
}
}
return landNames.size();
}
@Override
public AwakenedAmalgamLandNamesCount copy() {
return this;
}
@Override
public String toString() {
return "1";
}
@Override
public String getMessage() {
return "differently named lands you control";
}
}

View file

@ -0,0 +1,66 @@
package mage.cards.b;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.common.SacrificePermanentTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.SacrificeTargetCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.GainLifeEffect;
import mage.abilities.effects.common.TapSourceEffect;
import mage.abilities.effects.common.UntapSourceEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.counters.CounterType;
import mage.filter.StaticFilters;
import mage.game.permanent.token.BeastToken2;
import java.util.UUID;
/**
* @author Susucr
*/
public final class BalothPrime extends CardImpl {
public BalothPrime(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}");
this.subtype.add(SubType.BEAST);
this.power = new MageInt(10);
this.toughness = new MageInt(10);
// This creature enters tapped with six stun counters on it. (If a permanent with a stun counter would become untapped, remove one from it instead.)
Ability ability = new EntersBattlefieldAbility(
new TapSourceEffect(true), "tapped with six stun counters on it. "
+ "<i>(If a permanent with a stun counter would become untapped, remove one from it instead.)</i>"
);
ability.addEffect(new AddCountersSourceEffect(CounterType.STUN.createInstance(6)));
this.addAbility(ability);
// Whenever you sacrifice a land, create a tapped 4/4 green Beast creature token and untap this creature.
ability = new SacrificePermanentTriggeredAbility(
new CreateTokenEffect(new BeastToken2(), 1, true), StaticFilters.FILTER_LAND
);
ability.addEffect(new UntapSourceEffect().concatBy("and"));
this.addAbility(ability);
// {4}, Sacrifice a land: You gain 2 life.
ability = new SimpleActivatedAbility(new GainLifeEffect(2), new GenericManaCost(4));
ability.addCost(new SacrificeTargetCost(StaticFilters.FILTER_LAND));
this.addAbility(ability);
}
private BalothPrime(final BalothPrime card) {
super(card);
}
@Override
public BalothPrime copy() {
return new BalothPrime(this);
}
}

View file

@ -5,14 +5,12 @@ import mage.abilities.Ability;
import mage.abilities.effects.common.DestroyTargetEffect;
import mage.abilities.keyword.ChannelAbility;
import mage.abilities.keyword.DefenderAbility;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.ReachAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.filter.StaticFilters;
import mage.target.TargetPermanent;
import java.util.UUID;
@ -22,12 +20,6 @@ import java.util.UUID;
*/
public final class BambooGroveArcher extends CardImpl {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature with flying");
static {
filter.add(new AbilityPredicate(FlyingAbility.class));
}
public BambooGroveArcher(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT, CardType.CREATURE}, "{1}{G}");
@ -44,7 +36,7 @@ public final class BambooGroveArcher extends CardImpl {
// Channel {4}{G}, Discard Bamboo Grove Archer: Destroy target creature with flying.
Ability ability = new ChannelAbility("{4}{G}", new DestroyTargetEffect());
ability.addTarget(new TargetPermanent(filter));
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CREATURE_FLYING));
this.addAbility(ability);
}

View file

@ -18,7 +18,7 @@ public final class BattleHymn extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{R}");
// Add {R} for each creature you control.
this.getSpellAbility().addEffect(new DynamicManaEffect(Mana.RedMana(1), CreaturesYouControlCount.instance));
this.getSpellAbility().addEffect(new DynamicManaEffect(Mana.RedMana(1), CreaturesYouControlCount.SINGULAR));
}
private BattleHymn(final BattleHymn card) {

View file

@ -29,7 +29,7 @@ public final class BattleSquadron extends CardImpl {
this.addAbility(FlyingAbility.getInstance());
// Battle Squadron's power and toughness are each equal to the number of creatures you control.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CreaturesYouControlCount.instance))
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CreaturesYouControlCount.PLURAL))
.addHint(CreaturesYouControlHint.instance));
}

View file

@ -0,0 +1,39 @@
package mage.cards.b;
import mage.MageInt;
import mage.abilities.common.DiesSourceTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.game.permanent.token.LanderToken;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class BeamsawProspector extends CardImpl {
public BeamsawProspector(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}");
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.ARTIFICER);
this.power = new MageInt(2);
this.toughness = new MageInt(1);
// When this creature dies, create a Lander token.
this.addAbility(new DiesSourceTriggeredAbility(new CreateTokenEffect(new LanderToken())));
}
private BeamsawProspector(final BeamsawProspector card) {
super(card);
}
@Override
public BeamsawProspector copy() {
return new BeamsawProspector(this);
}
}

View file

@ -0,0 +1,42 @@
package mage.cards.b;
import mage.abilities.effects.common.ExileAllEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.FilterPermanent;
import mage.filter.predicate.Predicates;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class BeyondTheQuiet extends CardImpl {
private static final FilterPermanent filter = new FilterPermanent("creatures and Spacecraft");
static {
filter.add(Predicates.or(
CardType.CREATURE.getPredicate(),
SubType.SPACECRAFT.getPredicate()
));
}
public BeyondTheQuiet(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{W}{W}");
// Exile all creatures and Spacecraft.
this.getSpellAbility().addEffect(new ExileAllEffect(filter));
}
private BeyondTheQuiet(final BeyondTheQuiet card) {
super(card);
}
@Override
public BeyondTheQuiet copy() {
return new BeyondTheQuiet(this);
}
}

View file

@ -0,0 +1,144 @@
package mage.cards.b;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.hint.Hint;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.WatcherScope;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.EntersTheBattlefieldEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.LanderToken;
import mage.util.CardUtil;
import mage.watchers.Watcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class BioengineeredFuture extends CardImpl {
public BioengineeredFuture(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{G}{G}");
// When this enchantment enters, create a Lander token.
this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new LanderToken())));
// Each creature you control enters with an additional +1/+1 counter on it for each land that entered the battlefield under your control this turn.
this.addAbility(new SimpleStaticAbility(new BioengineeredFutureEffect())
.addHint(BioengineeredFutureHint.instance), new BioengineeredFutureWatcher());
}
private BioengineeredFuture(final BioengineeredFuture card) {
super(card);
}
@Override
public BioengineeredFuture copy() {
return new BioengineeredFuture(this);
}
}
class BioengineeredFutureEffect extends ReplacementEffectImpl {
BioengineeredFutureEffect() {
super(Duration.WhileOnBattlefield, Outcome.BoostCreature);
staticText = "each creature you control enters with an additional +1/+1 counter on it " +
"for each land that entered the battlefield under your control this turn";
}
private BioengineeredFutureEffect(final BioengineeredFutureEffect effect) {
super(effect);
}
@Override
public BioengineeredFutureEffect copy() {
return new BioengineeredFutureEffect(this);
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
Permanent permanent = ((EntersTheBattlefieldEvent) event).getTarget();
return permanent != null
&& permanent.isControlledBy(source.getControllerId())
&& permanent.isCreature(game);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Permanent creature = ((EntersTheBattlefieldEvent) event).getTarget();
int count = BioengineeredFutureWatcher.getCount(game, source);
if (creature != null && count > 0) {
creature.addCounters(
CounterType.P1P1.createInstance(count), source.getControllerId(),
source, game, event.getAppliedEffects()
);
}
return false;
}
}
class BioengineeredFutureWatcher extends Watcher {
private final Map<UUID, Integer> map = new HashMap<>();
BioengineeredFutureWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() != GameEvent.EventType.ENTERS_THE_BATTLEFIELD) {
return;
}
Permanent permanent = ((EntersTheBattlefieldEvent) event).getTarget();
if (permanent != null && permanent.isLand(game)) {
map.compute(permanent.getControllerId(), CardUtil::setOrIncrementValue);
}
}
@Override
public void reset() {
super.reset();
map.clear();
}
static int getCount(Game game, Ability source) {
return game
.getState()
.getWatcher(BioengineeredFutureWatcher.class)
.map
.getOrDefault(source.getControllerId(), 0);
}
}
enum BioengineeredFutureHint implements Hint {
instance;
@Override
public String getText(Game game, Ability ability) {
return "Lands that entered under your control this turn: " + BioengineeredFutureWatcher.getCount(game, ability);
}
@Override
public Hint copy() {
return this;
}
}

View file

@ -0,0 +1,49 @@
package mage.cards.b;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.game.permanent.token.LanderToken;
import mage.game.permanent.token.RobotToken;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class BiomechanEngineer extends CardImpl {
public BiomechanEngineer(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{U}");
this.subtype.add(SubType.INSECT);
this.subtype.add(SubType.ARTIFICER);
this.power = new MageInt(2);
this.toughness = new MageInt(2);
// When this creature enters, create a Lander token.
this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new LanderToken())));
// {8}: Draw two cards and create a 2/2 colorless Robot artifact creature token.
Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(2), new GenericManaCost(8));
ability.addEffect(new CreateTokenEffect(new RobotToken()).concatBy("and"));
this.addAbility(ability);
}
private BiomechanEngineer(final BiomechanEngineer card) {
super(card);
}
@Override
public BiomechanEngineer copy() {
return new BiomechanEngineer(this);
}
}

View file

@ -0,0 +1,42 @@
package mage.cards.b;
import mage.abilities.effects.common.UntapTargetEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.abilities.keyword.IndestructibleAbility;
import mage.abilities.keyword.ReachAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.counters.CounterType;
import mage.target.common.TargetControlledCreaturePermanent;
import java.util.UUID;
/**
* @author Susucr
*/
public final class BiosynthicBurst extends CardImpl {
public BiosynthicBurst(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{G}");
// Put a +1/+1 counter on target creature you control. It gains reach, trample, and indestructible until end of turn. Untap it.
this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance()));
this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent());
this.getSpellAbility().addEffect(new GainAbilityTargetEffect(ReachAbility.getInstance()).setText("it gains reach"));
this.getSpellAbility().addEffect(new GainAbilityTargetEffect(TrampleAbility.getInstance()).setText(", trample"));
this.getSpellAbility().addEffect(new GainAbilityTargetEffect(IndestructibleAbility.getInstance()).setText(", and indestructible until end of turn"));
this.getSpellAbility().addEffect(new UntapTargetEffect("untap it"));
}
private BiosynthicBurst(final BiosynthicBurst card) {
super(card);
}
@Override
public BiosynthicBurst copy() {
return new BiosynthicBurst(this);
}
}

View file

@ -0,0 +1,51 @@
package mage.cards.b;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SacrificePermanentTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.StaticFilters;
import mage.game.permanent.token.LanderToken;
import mage.target.common.TargetOpponent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class BiotechSpecialist extends CardImpl {
public BiotechSpecialist(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}{G}");
this.subtype.add(SubType.INSECT);
this.subtype.add(SubType.SCIENTIST);
this.power = new MageInt(1);
this.toughness = new MageInt(3);
// When this creature enters, create a Lander token.
this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new LanderToken())));
// Whenever you sacrifice an artifact, this creature deals 2 damage to target opponent.
Ability ability = new SacrificePermanentTriggeredAbility(
new DamageTargetEffect(2), StaticFilters.FILTER_PERMANENT_ARTIFACT
);
ability.addTarget(new TargetOpponent());
this.addAbility(ability);
}
private BiotechSpecialist(final BiotechSpecialist card) {
super(card);
}
@Override
public BiotechSpecialist copy() {
return new BiotechSpecialist(this);
}
}

View file

@ -0,0 +1,60 @@
package mage.cards.b;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.PutOnLibraryTargetEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.keyword.WarpAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.counters.CounterType;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.target.common.TargetCardInExile;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class BladeOfTheSwarm extends CardImpl {
private static final FilterCard filter = new FilterCard("exiled card with warp");
static {
filter.add(new AbilityPredicate(WarpAbility.class));
}
public BladeOfTheSwarm(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}");
this.subtype.add(SubType.INSECT);
this.subtype.add(SubType.ASSASSIN);
this.power = new MageInt(3);
this.toughness = new MageInt(1);
// When this creature enters, choose one --
// * Put two +1/+1 counters on this creature.
Ability ability = new EntersBattlefieldTriggeredAbility(
new AddCountersSourceEffect(CounterType.P1P1.createInstance(2))
);
// * Put target exiled card with warp on the bottom of its owner's library.
ability.addMode(new Mode(new PutOnLibraryTargetEffect(false))
.addTarget(new TargetCardInExile(filter)));
this.addAbility(ability);
}
private BladeOfTheSwarm(final BladeOfTheSwarm card) {
super(card);
}
@Override
public BladeOfTheSwarm copy() {
return new BladeOfTheSwarm(this);
}
}

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