diff --git a/Mage.Client/src/main/java/mage/client/dialog/CardInfoWindowDialog.java b/Mage.Client/src/main/java/mage/client/dialog/CardInfoWindowDialog.java index 8285714bbbd..8b4d90ef0dc 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/CardInfoWindowDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/CardInfoWindowDialog.java @@ -30,7 +30,7 @@ public class CardInfoWindowDialog extends MageDialog { private static final Logger LOGGER = Logger.getLogger(CardInfoWindowDialog.class); public enum ShowType { - REVEAL, REVEAL_TOP_LIBRARY, LOOKED_AT, EXILE, GRAVEYARD, OTHER + REVEAL, REVEAL_TOP_LIBRARY, LOOKED_AT, EXILE, GRAVEYARD, COMPANION, OTHER } private final ShowType showType; @@ -72,6 +72,10 @@ public class CardInfoWindowDialog extends MageDialog { case EXILE: this.setFrameIcon(new ImageIcon(ImageManagerImpl.instance.getExileImage())); break; + case COMPANION: + this.setFrameIcon(new ImageIcon(ImageManagerImpl.instance.getTokenIconImage())); + this.setClosable(false); + break; default: // no icon yet } diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 7078b93a2be..b5578b3289e 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -40,6 +40,7 @@ import javax.swing.plaf.basic.BasicSplitPaneDivider; import javax.swing.plaf.basic.BasicSplitPaneUI; import java.awt.*; import java.awt.event.*; +import java.beans.PropertyVetoException; import java.io.Serializable; import java.util.List; import java.util.*; @@ -74,6 +75,7 @@ public final class GamePanel extends javax.swing.JPanel { private final Map revealed = new HashMap<>(); private final Map lookedAt = new HashMap<>(); private final Map graveyardWindows = new HashMap<>(); + private final Map companion = new HashMap<>(); private final Map graveyards = new HashMap<>(); private final ArrayList pickTarget = new ArrayList<>(); @@ -241,6 +243,10 @@ public final class GamePanel extends javax.swing.JPanel { lookedAtDialog.cleanUp(); lookedAtDialog.removeDialog(); } + for (CardInfoWindowDialog companionDialog : companion.values()) { + companionDialog.cleanUp(); + companionDialog.removeDialog(); + } for (ShowCardsDialog pickTargetDialog : pickTarget) { pickTargetDialog.cleanUp(); pickTargetDialog.removeDialog(); @@ -275,6 +281,9 @@ public final class GamePanel extends javax.swing.JPanel { for (CardInfoWindowDialog cardInfoWindowDialog : lookedAt.values()) { cardInfoWindowDialog.changeGUISize(); } + for (CardInfoWindowDialog cardInfoWindowDialog : companion.values()) { + cardInfoWindowDialog.changeGUISize(); + } for (CardInfoWindowDialog cardInfoWindowDialog : graveyardWindows.values()) { cardInfoWindowDialog.changeGUISize(); } @@ -781,6 +790,7 @@ public final class GamePanel extends javax.swing.JPanel { showRevealed(game); showLookedAt(game); + showCompanion(game); if (!game.getCombat().isEmpty()) { CombatManager.instance.showCombat(game.getCombat(), gameId); } else { @@ -1085,6 +1095,9 @@ public final class GamePanel extends javax.swing.JPanel { for (CardInfoWindowDialog lookedAtDialog : lookedAt.values()) { lookedAtDialog.hideDialog(); } + for (CardInfoWindowDialog companionDialog : companion.values()) { + companionDialog.hideDialog(); + } } // Called if the game frame comes to front again @@ -1102,6 +1115,9 @@ public final class GamePanel extends javax.swing.JPanel { for (CardInfoWindowDialog lookedAtDialog : lookedAt.values()) { lookedAtDialog.show(); } + for (CardInfoWindowDialog companionDialog : companion.values()) { + companionDialog.show(); + } } public void openGraveyardWindow(String playerName) { @@ -1146,6 +1162,23 @@ public final class GamePanel extends javax.swing.JPanel { removeClosedCardInfoWindows(lookedAt); } + private void showCompanion(GameView game) { + for (RevealedView revealView : game.getCompanion()) { + handleGameInfoWindow(companion, ShowType.COMPANION, revealView.getName(), revealView.getCards()); + } + // Close the companion view if not in the game view + companion.forEach((name, companionDialog) -> { + if (game.getCompanion().stream().noneMatch(revealedView -> revealedView.getName().equals(name))) { + try { + companionDialog.setClosed(true); + } catch (PropertyVetoException e) { + logger.error("Couldn't close companion dialog", e); + } + } + }); + removeClosedCardInfoWindows(companion); + } + private void handleGameInfoWindow(Map windowMap, ShowType showType, String name, LinkedHashMap cardsView) { CardInfoWindowDialog cardInfoWindowDialog; if (!windowMap.containsKey(name)) { @@ -1160,6 +1193,7 @@ public final class GamePanel extends javax.swing.JPanel { switch (showType) { case REVEAL: case REVEAL_TOP_LIBRARY: + case COMPANION: cardInfoWindowDialog.loadCards((CardsView) cardsView, bigCard, gameId); break; case LOOKED_AT: @@ -1332,6 +1366,22 @@ public final class GamePanel extends javax.swing.JPanel { } } + // companion + for (RevealedView rev : gameView.getCompanion()) { + for (Map.Entry card : rev.getCards().entrySet()) { + if (needSelectable.contains(card.getKey())) { + card.getValue().setChoosable(true); + } + if (needChoosen.contains(card.getKey())) { + card.getValue().setSelected(true); + } + if (needPlayable.containsKey(card.getKey())) { + card.getValue().setPlayable(true); + card.getValue().setPlayableAmount(needPlayable.get(card.getKey())); + } + } + } + // looked at for (LookedAtView look : gameView.getLookedAt()) { for (Map.Entry card : look.getCards().entrySet()) { diff --git a/Mage.Common/src/main/java/mage/view/GameView.java b/Mage.Common/src/main/java/mage/view/GameView.java index 20ef9eedd59..a98a8a19bc7 100644 --- a/Mage.Common/src/main/java/mage/view/GameView.java +++ b/Mage.Common/src/main/java/mage/view/GameView.java @@ -49,6 +49,7 @@ public class GameView implements Serializable { private final List exiles = new ArrayList<>(); private final List revealed = new ArrayList<>(); private List lookedAt = new ArrayList<>(); + private final List companion = new ArrayList<>(); private final List combat = new ArrayList<>(); private final TurnPhase phase; private final PhaseStep step; @@ -149,6 +150,12 @@ public class GameView implements Serializable { for (String name : state.getRevealed().keySet()) { revealed.add(new RevealedView(name, state.getRevealed().get(name), game)); } + for (String name : state.getCompanion().keySet()) { + // Only show the companion window when the companion is still outside the game. + if (state.getCompanion().get(name).stream().anyMatch(cardId -> state.getZone(cardId) == Zone.OUTSIDE)) { + companion.add(new RevealedView(name, state.getCompanion().get(name), game)); + } + } this.phase = state.getTurn().getPhaseType(); this.step = state.getTurn().getStepType(); this.turn = state.getTurnNum(); @@ -266,6 +273,10 @@ public class GameView implements Serializable { return lookedAt; } + public List getCompanion() { + return companion; + } + public void setLookedAt(List list) { this.lookedAt = list; } diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Brawl.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Brawl.java index 7737857e9d0..8c8b301af6a 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Brawl.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Brawl.java @@ -1,6 +1,8 @@ package mage.deck; +import mage.abilities.Ability; import mage.abilities.common.CanBeYourCommanderAbility; +import mage.abilities.keyword.CompanionAbility; import mage.cards.Card; import mage.cards.decks.Constructed; import mage.cards.decks.Deck; @@ -39,9 +41,50 @@ public class Brawl extends Constructed { @Override public boolean validate(Deck deck) { boolean valid = true; + Card brawler = null; + Card companion = null; FilterMana colorIdentity = new FilterMana(); - if (deck.getCards().size() + deck.getSideboard().size() != getDeckMinSize()) { + if (deck.getSideboard().size() == 1) { + for (Card card : deck.getSideboard()) { + brawler = card; + } + } else if (deck.getSideboard().size() == 2) { + Iterator iter = deck.getSideboard().iterator(); + Card card1 = iter.next(); + Card card2 = iter.next(); + if (card1.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card1; + brawler = card2; + } else if (card2.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card2; + brawler = card1; + } else { + invalid.put("Brawl", "Sideboard must contain only the brawler and up to 1 companion"); + valid = false; + } + } else { + invalid.put("Brawl", "Sideboard must contain only the brawler and up to 1 companion"); + valid = false; + } + + if (brawler != null) { + ManaUtil.collectColorIdentity(colorIdentity, brawler.getColorIdentity()); + if (bannedCommander.contains(brawler.getName())) { + invalid.put("Brawl", "Brawler banned (" + brawler.getName() + ')'); + valid = false; + } + if (!((brawler.isCreature() && brawler.isLegendary()) + || brawler.isPlaneswalker() || brawler.getAbilities().contains(CanBeYourCommanderAbility.getInstance()))) { + invalid.put("Brawl", "Invalid Brawler (" + brawler.getName() + ')'); + valid = false; + } + } + + if (companion != null && deck.getCards().size() + deck.getSideboard().size() != getDeckMinSize() + 1) { + invalid.put("Deck", "Must contain " + (getDeckMinSize() + 1) + " cards (companion doesn't count in deck size requirement): has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards"); + valid = false; + } else if (companion == null && deck.getCards().size() + deck.getSideboard().size() != getDeckMinSize()) { invalid.put("Deck", "Must contain " + getDeckMinSize() + " cards: has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards"); valid = false; } @@ -58,23 +101,6 @@ public class Brawl extends Constructed { } } - if (deck.getSideboard().size() != 1) { - invalid.put("Brawl", "Sideboard must contain only the commander)"); - valid = false; - } else { - for (Card commander : deck.getSideboard()) { - if (bannedCommander.contains(commander.getName())) { - invalid.put("Brawl", "Brawl banned (" + commander.getName() + ')'); - valid = false; - } - if (!((commander.isCreature() && commander.isLegendary()) - || commander.isPlaneswalker() || commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance()))) { - invalid.put("Brawl", "Invalid Commander (" + commander.getName() + ')'); - valid = false; - } - ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity()); - } - } Set basicsInDeck = new HashSet<>(); if (colorIdentity.isColorless()) { for (Card card : deck.getCards()) { @@ -92,6 +118,15 @@ public class Brawl extends Constructed { valid = false; } } + for (Card card : deck.getSideboard()) { + if (!ManaUtil.isColorIdentityCompatible(colorIdentity, card.getColorIdentity()) + && !(colorIdentity.isColorless() + && basicsInDeck.size() == 1 + && basicsInDeck.contains(card.getName()))) { + invalid.put(card.getName(), "Invalid color (" + colorIdentity.toString() + ')'); + valid = false; + } + } for (Card card : deck.getCards()) { if (!isSetAllowed(card.getExpansionSetCode())) { if (!legalSets(card)) { @@ -108,7 +143,22 @@ public class Brawl extends Constructed { } } } + // Check for companion legality + if (companion != null) { + Set cards = new HashSet<>(deck.getCards()); + cards.add(brawler); + for (Ability ability : companion.getAbilities()) { + if (ability instanceof CompanionAbility) { + CompanionAbility companionAbility = (CompanionAbility) ability; + if (!companionAbility.isLegal(cards)) { + invalid.put(companion.getName(), "Deck invalid for companion"); + valid = false; + } + break; + } + } + } + return valid; } - } diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java index 89309735351..dda9400490e 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Commander.java @@ -4,6 +4,7 @@ import mage.ObjectColor; import mage.abilities.Ability; import mage.abilities.common.CanBeYourCommanderAbility; import mage.abilities.costs.mana.ManaCost; +import mage.abilities.keyword.CompanionAbility; import mage.abilities.keyword.PartnerAbility; import mage.abilities.keyword.PartnerWithAbility; import mage.cards.Card; @@ -91,8 +92,55 @@ public class Commander extends Constructed { public boolean validate(Deck deck) { boolean valid = true; FilterMana colorIdentity = new FilterMana(); + Set commanders = new HashSet<>(); + Card companion = null; - if (deck.getCards().size() + deck.getSideboard().size() != 100) { + if (deck.getSideboard().size() == 1) { + commanders.add(deck.getSideboard().iterator().next()); + } else if (deck.getSideboard().size() == 2) { + Iterator iter = deck.getSideboard().iterator(); + Card card1 = iter.next(); + Card card2 = iter.next(); + if (card1.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card1; + commanders.add(card2); + } else if (card2.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card2; + commanders.add(card1); + } else { + commanders.add(card1); + commanders.add(card2); + } + } else if (deck.getSideboard().size() == 3) { + Iterator iter = deck.getSideboard().iterator(); + Card card1 = iter.next(); + Card card2 = iter.next(); + Card card3 = iter.next(); + if (card1.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card1; + commanders.add(card2); + commanders.add(card3); + } else if (card2.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card2; + commanders.add(card1); + commanders.add(card3); + } else if (card3.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card3; + commanders.add(card1); + commanders.add(card2); + } else { + invalid.put("Commander", "Sideboard must contain only the commander(s) and up to 1 companion"); + valid = false; + } + } else { + invalid.put("Commander", "Sideboard must contain only the commander(s) and up to 1 companion"); + valid = false; + } + + if (companion != null && deck.getCards().size() + deck.getSideboard().size() != 101) { + invalid.put("Deck", "Must contain " + 101 + " cards (companion doesn't count for deck size): has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards"); + valid = false; + } else if (companion == null && deck.getCards().size() + deck.getSideboard().size() != 100) { invalid.put("Deck", "Must contain " + 100 + " cards: has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards"); valid = false; } @@ -109,48 +157,40 @@ public class Commander extends Constructed { } } - if (deck.getSideboard().isEmpty() || deck.getSideboard().size() > 2) { - if ((deck.getSideboard().size() > 1 && !partnerAllowed)) { - invalid.put("Commander", "You may only have one commander"); + Set commanderNames = new HashSet<>(); + for (Card commander : commanders) { + commanderNames.add(commander.getName()); + } + for (Card commander : commanders) { + if (bannedCommander.contains(commander.getName())) { + invalid.put("Commander", "Commander banned (" + commander.getName() + ')'); + valid = false; } - invalid.put("Commander", "Sideboard must contain only the commander(s)"); - valid = false; - } else { - Set commanderNames = new HashSet<>(); - for (Card commander : deck.getSideboard()) { - commanderNames.add(commander.getName()); + if ((!commander.isCreature() || !commander.isLegendary()) + && (!commander.isPlaneswalker() || !commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance()))) { + invalid.put("Commander", "Commander invalid (" + commander.getName() + ')'); + valid = false; } - for (Card commander : deck.getSideboard()) { - if (bannedCommander.contains(commander.getName())) { - invalid.put("Commander", "Commander banned (" + commander.getName() + ')'); - valid = false; - } - if ((!commander.isCreature() || !commander.isLegendary()) - && (!commander.isPlaneswalker() || !commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance()))) { - invalid.put("Commander", "Commander invalid (" + commander.getName() + ')'); - valid = false; - } - if (deck.getSideboard().size() == 2) { - if (commander.getAbilities().contains(PartnerAbility.getInstance())) { - if (bannedPartner.contains(commander.getName())) { - invalid.put("Commander", "Partner banned (" + commander.getName() + ')'); - valid = false; - } - } else { - boolean partnersWith = commander.getAbilities() - .stream() - .filter(PartnerWithAbility.class::isInstance) - .map(PartnerWithAbility.class::cast) - .map(PartnerWithAbility::getPartnerName) - .anyMatch(commanderNames::contains); - if (!partnersWith) { - invalid.put("Commander", "Commander without Partner (" + commander.getName() + ')'); - valid = false; - } + if (commanders.size() == 2) { + if (commander.getAbilities().contains(PartnerAbility.getInstance())) { + if (bannedPartner.contains(commander.getName())) { + invalid.put("Commander", "Partner banned (" + commander.getName() + ')'); + valid = false; + } + } else { + boolean partnersWith = commander.getAbilities() + .stream() + .filter(PartnerWithAbility.class::isInstance) + .map(PartnerWithAbility.class::cast) + .map(PartnerWithAbility::getPartnerName) + .anyMatch(commanderNames::contains); + if (!partnersWith) { + invalid.put("Commander", "Commander without Partner (" + commander.getName() + ')'); + valid = false; } } - ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity()); } + ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity()); } // no needs in cards check on wrong commanders @@ -164,6 +204,12 @@ public class Commander extends Constructed { valid = false; } } + for (Card card : deck.getSideboard()) { + if (!ManaUtil.isColorIdentityCompatible(colorIdentity, card.getColorIdentity())) { + invalid.put(card.getName(), "Invalid color (" + colorIdentity.toString() + ')'); + valid = false; + } + } for (Card card : deck.getCards()) { if (!isSetAllowed(card.getExpansionSetCode())) { if (!legalSets(card)) { @@ -180,6 +226,21 @@ public class Commander extends Constructed { } } } + // Check for companion legality + if (companion != null) { + Set cards = new HashSet<>(deck.getCards()); + cards.addAll(commanders); + for (Ability ability : companion.getAbilities()) { + if (ability instanceof CompanionAbility) { + CompanionAbility companionAbility = (CompanionAbility) ability; + if (!companionAbility.isLegal(cards)) { + invalid.put(companion.getName(), "Deck invalid for companion"); + valid = false; + } + break; + } + } + } return valid; } diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/FreeformCommander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/FreeformCommander.java index 8a1a41c6316..d53dd54dd6f 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/FreeformCommander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/FreeformCommander.java @@ -1,6 +1,7 @@ package mage.deck; import mage.abilities.Ability; +import mage.abilities.keyword.CompanionAbility; import mage.abilities.keyword.PartnerAbility; import mage.abilities.keyword.PartnerWithAbility; import mage.cards.Card; @@ -49,8 +50,55 @@ public class FreeformCommander extends Constructed { public boolean validate(Deck deck) { boolean valid = true; FilterMana colorIdentity = new FilterMana(); + Set commanders = new HashSet<>(); + Card companion = null; - if (deck.getCards().size() + deck.getSideboard().size() != 100) { + if (deck.getSideboard().size() == 1) { + commanders.add(deck.getSideboard().iterator().next()); + } else if (deck.getSideboard().size() == 2) { + Iterator iter = deck.getSideboard().iterator(); + Card card1 = iter.next(); + Card card2 = iter.next(); + if (card1.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card1; + commanders.add(card2); + } else if (card2.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card2; + commanders.add(card1); + } else { + commanders.add(card1); + commanders.add(card2); + } + } else if (deck.getSideboard().size() == 3) { + Iterator iter = deck.getSideboard().iterator(); + Card card1 = iter.next(); + Card card2 = iter.next(); + Card card3 = iter.next(); + if (card1.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card1; + commanders.add(card2); + commanders.add(card3); + } else if (card2.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card2; + commanders.add(card1); + commanders.add(card3); + } else if (card3.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card3; + commanders.add(card1); + commanders.add(card2); + } else { + invalid.put("Commander", "Sideboard must contain only the commander(s) and up to 1 companion"); + valid = false; + } + } else { + invalid.put("Commander", "Sideboard must contain only the commander(s) and up to 1 companion"); + valid = false; + } + + if (companion != null && deck.getCards().size() + deck.getSideboard().size() != 101) { + invalid.put("Deck", "Must contain " + 101 + " cards (companion doesn't count for deck size): has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards"); + valid = false; + } else if (companion == null && deck.getCards().size() + deck.getSideboard().size() != 100) { invalid.put("Deck", "Must contain " + 100 + " cards: has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards"); valid = false; } @@ -58,39 +106,32 @@ public class FreeformCommander extends Constructed { Map counts = new HashMap<>(); countCards(counts, deck.getCards()); countCards(counts, deck.getSideboard()); - valid = checkCounts(1, counts) && valid; - if (deck.getSideboard().isEmpty() || deck.getSideboard().size() > 2) { - invalid.put("Commander", "Sideboard must contain only the commander(s)"); - valid = false; - } else { - Set commanderNames = new HashSet<>(); - for (Card commander : deck.getSideboard()) { - commanderNames.add(commander.getName()); + Set commanderNames = new HashSet<>(); + for (Card commander : commanders) { + commanderNames.add(commander.getName()); + } + for (Card commander : commanders) { + if (!commander.isCreature() || !commander.isLegendary()) { + invalid.put("Commander", "For Freeform Commander, the commander must be a creature or be legendary. Yours was: " + commander.getName()); + valid = false; } - for (Card commander : deck.getSideboard()) { - if (!(commander.isCreature() - || commander.isLegendary())) { - invalid.put("Commander", "For Freeform Commander, the commander must be a creature or be legendary. Yours was: " + commander.getName()); - valid = false; - } - if (deck.getSideboard().size() == 2 && !commander.getAbilities().contains(PartnerAbility.getInstance())) { - boolean partnersWith = false; - for (Ability ability : commander.getAbilities()) { - if (ability instanceof PartnerWithAbility - && commanderNames.contains(((PartnerWithAbility) ability).getPartnerName())) { - partnersWith = true; - break; - } - } + if (commanders.size() == 2) { + if (!commander.getAbilities().contains(PartnerAbility.getInstance())) { + boolean partnersWith = commander.getAbilities() + .stream() + .filter(PartnerWithAbility.class::isInstance) + .map(PartnerWithAbility.class::cast) + .map(PartnerWithAbility::getPartnerName) + .anyMatch(commanderNames::contains); if (!partnersWith) { invalid.put("Commander", "Commander without Partner (" + commander.getName() + ')'); valid = false; } } - ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity()); } + ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity()); } // no needs in cards check on wrong commanders @@ -104,12 +145,24 @@ public class FreeformCommander extends Constructed { valid = false; } } - for (Card card : deck.getSideboard()) { - if (!isSetAllowed(card.getExpansionSetCode())) { - if (!legalSets(card)) { - invalid.put(card.getName(), "Not allowed Set: " + card.getExpansionSetCode()); - valid = false; + if (!ManaUtil.isColorIdentityCompatible(colorIdentity, card.getColorIdentity())) { + invalid.put(card.getName(), "Invalid color (" + colorIdentity.toString() + ')'); + valid = false; + } + } + // Check for companion legality + if (companion != null) { + Set cards = new HashSet<>(deck.getCards()); + cards.addAll(commanders); + for (Ability ability : companion.getAbilities()) { + if (ability instanceof CompanionAbility) { + CompanionAbility companionAbility = (CompanionAbility) ability; + if (!companionAbility.isLegal(cards)) { + invalid.put(companion.getName(), "Deck invalid for companion"); + valid = false; + } + break; } } } diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/PennyDreadfulCommander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/PennyDreadfulCommander.java index 0be8aa63a97..967578f66be 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/PennyDreadfulCommander.java +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/PennyDreadfulCommander.java @@ -2,6 +2,7 @@ package mage.deck; import mage.abilities.Ability; import mage.abilities.common.CanBeYourCommanderAbility; +import mage.abilities.keyword.CompanionAbility; import mage.abilities.keyword.PartnerAbility; import mage.abilities.keyword.PartnerWithAbility; import mage.cards.Card; @@ -51,25 +52,63 @@ public class PennyDreadfulCommander extends Constructed { public boolean validate(Deck deck) { boolean valid = true; FilterMana colorIdentity = new FilterMana(); + Set commanders = new HashSet<>(); + Card companion = null; - if (deck.getCards().size() + deck.getSideboard().size() != 100) { + if (deck.getSideboard().size() == 1) { + commanders.add(deck.getSideboard().iterator().next()); + } else if (deck.getSideboard().size() == 2) { + Iterator iter = deck.getSideboard().iterator(); + Card card1 = iter.next(); + Card card2 = iter.next(); + if (card1.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card1; + commanders.add(card2); + } else if (card2.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card2; + commanders.add(card1); + } else { + commanders.add(card1); + commanders.add(card2); + } + } else if (deck.getSideboard().size() == 3) { + Iterator iter = deck.getSideboard().iterator(); + Card card1 = iter.next(); + Card card2 = iter.next(); + Card card3 = iter.next(); + if (card1.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card1; + commanders.add(card2); + commanders.add(card3); + } else if (card2.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card2; + commanders.add(card1); + commanders.add(card3); + } else if (card3.getAbilities().stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + companion = card3; + commanders.add(card1); + commanders.add(card2); + } else { + invalid.put("Commander", "Sideboard must contain only the commander(s) and up to 1 companion"); + valid = false; + } + } else { + invalid.put("Commander", "Sideboard must contain only the commander(s) and up to 1 companion"); + valid = false; + } + + if (companion != null && deck.getCards().size() + deck.getSideboard().size() != 101) { + invalid.put("Deck", "Must contain " + 101 + " cards (companion doesn't count for deck size): has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards"); + valid = false; + } else if (companion == null && deck.getCards().size() + deck.getSideboard().size() != 100) { invalid.put("Deck", "Must contain " + 100 + " cards: has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards"); valid = false; } - List basicLandNames = new ArrayList<>(Arrays.asList("Forest", "Island", "Mountain", "Swamp", "Plains", "Wastes")); Map counts = new HashMap<>(); countCards(counts, deck.getCards()); countCards(counts, deck.getSideboard()); - - for (Map.Entry entry : counts.entrySet()) { - if (entry.getValue() > 1) { - if (!basicLandNames.contains(entry.getKey())) { - invalid.put(entry.getKey(), "Too many: " + entry.getValue()); - valid = false; - } - } - } + valid = checkCounts(1, counts) && valid; generatePennyDreadfulHash(); for (String wantedCard : counts.keySet()) { @@ -79,36 +118,35 @@ public class PennyDreadfulCommander extends Constructed { } } - if (deck.getSideboard().isEmpty() || deck.getSideboard().size() > 2) { - invalid.put("Commander", "Sideboard must contain only the commander(s)"); - valid = false; - } else { - Set commanderNames = new HashSet<>(); - for (Card commander : deck.getSideboard()) { - commanderNames.add(commander.getName()); + Set commanderNames = new HashSet<>(); + for (Card commander : commanders) { + commanderNames.add(commander.getName()); + } + for (Card commander : commanders) { + if (bannedCommander.contains(commander.getName())) { + invalid.put("Commander", "Commander banned (" + commander.getName() + ')'); + valid = false; } - for (Card commander : deck.getSideboard()) { - if ((!commander.isCreature() || !commander.isLegendary()) - && (!commander.isPlaneswalker() || !commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance()))) { - invalid.put("Commander", "Commander invalid (" + commander.getName() + ')'); - valid = false; - } - if (deck.getSideboard().size() == 2 && !commander.getAbilities().contains(PartnerAbility.getInstance())) { - boolean partnersWith = false; - for (Ability ability : commander.getAbilities()) { - if (ability instanceof PartnerWithAbility - && commanderNames.contains(((PartnerWithAbility) ability).getPartnerName())) { - partnersWith = true; - break; - } - } + if ((!commander.isCreature() || !commander.isLegendary()) + && (!commander.isPlaneswalker() || !commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance()))) { + invalid.put("Commander", "Commander invalid (" + commander.getName() + ')'); + valid = false; + } + if (commanders.size() == 2) { + if (!commander.getAbilities().contains(PartnerAbility.getInstance())) { + boolean partnersWith = commander.getAbilities() + .stream() + .filter(PartnerWithAbility.class::isInstance) + .map(PartnerWithAbility.class::cast) + .map(PartnerWithAbility::getPartnerName) + .anyMatch(commanderNames::contains); if (!partnersWith) { invalid.put("Commander", "Commander without Partner (" + commander.getName() + ')'); valid = false; } } - ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity()); } + ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity()); } // no needs in cards check on wrong commanders @@ -122,6 +160,12 @@ public class PennyDreadfulCommander extends Constructed { valid = false; } } + for (Card card : deck.getSideboard()) { + if (!ManaUtil.isColorIdentityCompatible(colorIdentity, card.getColorIdentity())) { + invalid.put(card.getName(), "Invalid color (" + colorIdentity.toString() + ')'); + valid = false; + } + } for (Card card : deck.getCards()) { if (!isSetAllowed(card.getExpansionSetCode())) { if (!legalSets(card)) { @@ -138,6 +182,21 @@ public class PennyDreadfulCommander extends Constructed { } } } + // Check for companion legality + if (companion != null) { + Set cards = new HashSet<>(deck.getCards()); + cards.addAll(commanders); + for (Ability ability : companion.getAbilities()) { + if (ability instanceof CompanionAbility) { + CompanionAbility companionAbility = (CompanionAbility) ability; + if (!companionAbility.isLegal(cards)) { + invalid.put(companion.getName(), "Deck invalid for companion"); + valid = false; + } + break; + } + } + } return valid; } diff --git a/Mage.Sets/src/mage/cards/k/KerugaTheMacrosage.java b/Mage.Sets/src/mage/cards/k/KerugaTheMacrosage.java new file mode 100644 index 00000000000..70a956ca1a7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KerugaTheMacrosage.java @@ -0,0 +1,71 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.keyword.CompanionAbility; +import mage.abilities.keyword.CompanionCondition; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.mageobject.ConvertedManaCostPredicate; +import mage.filter.predicate.permanent.AnotherPredicate; + +import java.util.Set; +import java.util.UUID; + +/** + * @author emerald000 + */ +public final class KerugaTheMacrosage extends CardImpl { + + private static final FilterControlledPermanent filter = new FilterControlledPermanent("other permanent you control with converted mana cost 3 or greater"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(new ConvertedManaCostPredicate(ComparisonType.MORE_THAN, 2)); + } + + public KerugaTheMacrosage(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G/U}{G/U}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.DINOSAUR); + this.subtype.add(SubType.HIPPO); + this.power = new MageInt(5); + this.toughness = new MageInt(4); + + // Companion — Your starting deck contains only cards with converted mana cost 3 or greater and land cards. + this.addAbility(new CompanionAbility(new KerugaCondition())); + // When Keruga, the Macrosage enters the battlefield, draw a card for each other permanent you control with converted mana cost 3 or greater. + this.addAbility(new EntersBattlefieldTriggeredAbility(new DrawCardSourceControllerEffect(new PermanentsOnBattlefieldCount(filter)))); + } + + private KerugaTheMacrosage(final KerugaTheMacrosage card) { + super(card); + } + + @Override + public KerugaTheMacrosage copy() { + return new KerugaTheMacrosage(this); + } +} + +class KerugaCondition implements CompanionCondition { + + @Override + public String getRule() { + return "Your starting deck contains only cards with converted mana cost 3 or greater and land cards."; + } + + @Override + public boolean isLegal(Set deck) { + return deck.stream().allMatch(card -> card.isLand() || card.getConvertedManaCost() >= 3); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/u/UmoriTheCollector.java b/Mage.Sets/src/mage/cards/u/UmoriTheCollector.java new file mode 100644 index 00000000000..feca96c306e --- /dev/null +++ b/Mage.Sets/src/mage/cards/u/UmoriTheCollector.java @@ -0,0 +1,85 @@ +package mage.cards.u; + +import mage.MageInt; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.ChooseCardTypeEffect; +import mage.abilities.effects.common.cost.SpellsCostReductionAllOfChosenCardTypeEffect; +import mage.abilities.keyword.CompanionAbility; +import mage.abilities.keyword.CompanionCondition; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.FilterCard; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author emerald000 + */ +public final class UmoriTheCollector extends CardImpl { + + private static final FilterCard filter = new FilterCard("Spells you cast of the chosen type"); + + public UmoriTheCollector(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B/G}{B/G}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.OOZE); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // Companion — Each nonland card in your starting deck shares a card type. + this.addAbility(new CompanionAbility(new UmoriCondition())); + + // As Umori, the Collector enters the battlefield, choose a card type. + this.addAbility(new AsEntersBattlefieldAbility(new ChooseCardTypeEffect(Outcome.Benefit))); + + // Spells you cast of the chosen type cost {1} less to cast. + this.addAbility(new SimpleStaticAbility(new SpellsCostReductionAllOfChosenCardTypeEffect(filter, 1, true))); + } + + private UmoriTheCollector(final UmoriTheCollector card) { + super(card); + } + + @Override + public UmoriTheCollector copy() { + return new UmoriTheCollector(this); + } +} + +class UmoriCondition implements CompanionCondition { + + @Override + public String getRule() { + return "Each nonland card in your starting deck shares a card type."; + } + + @Override + public boolean isLegal(Set deck) { + Set cardTypes = new HashSet<>(); + for (Card card : deck) { + // Lands are fine. + if (card.isLand()) { + continue; + } + // First nonland checked. + if (cardTypes.isEmpty()) { + cardTypes.addAll(card.getCardType()); + } else { + cardTypes.retainAll(card.getCardType()); + if (cardTypes.isEmpty()) { + return false; + } + } + } + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/IkoriaLairOfBehemoths.java b/Mage.Sets/src/mage/sets/IkoriaLairOfBehemoths.java index 0312a46bdcb..71ff7c0a4df 100644 --- a/Mage.Sets/src/mage/sets/IkoriaLairOfBehemoths.java +++ b/Mage.Sets/src/mage/sets/IkoriaLairOfBehemoths.java @@ -182,6 +182,7 @@ public final class IkoriaLairOfBehemoths extends ExpansionSet { cards.add(new SetCardInfo("Jungle Hollow", 249, Rarity.COMMON, mage.cards.j.JungleHollow.class)); cards.add(new SetCardInfo("Keensight Mentor", 18, Rarity.UNCOMMON, mage.cards.k.KeensightMentor.class)); cards.add(new SetCardInfo("Keep Safe", 56, Rarity.COMMON, mage.cards.k.KeepSafe.class)); + cards.add(new SetCardInfo("Keruga, the Macrosage", 354, Rarity.RARE, mage.cards.k.KerugaTheMacrosage.class)); cards.add(new SetCardInfo("Ketria Crystal", 236, Rarity.UNCOMMON, mage.cards.k.KetriaCrystal.class)); cards.add(new SetCardInfo("Ketria Triome", 250, Rarity.RARE, mage.cards.k.KetriaTriome.class)); cards.add(new SetCardInfo("Kogla, the Titan Ape", 162, Rarity.RARE, mage.cards.k.KoglaTheTitanApe.class)); @@ -279,6 +280,7 @@ public final class IkoriaLairOfBehemoths extends ExpansionSet { cards.add(new SetCardInfo("Titanoth Rex", 174, Rarity.UNCOMMON, mage.cards.t.TitanothRex.class)); cards.add(new SetCardInfo("Tranquil Cove", 257, Rarity.COMMON, mage.cards.t.TranquilCove.class)); cards.add(new SetCardInfo("Trumpeting Gnarr", 213, Rarity.UNCOMMON, mage.cards.t.TrumpetingGnarr.class)); + cards.add(new SetCardInfo("Umori, the Collector", 231, Rarity.RARE, mage.cards.u.UmoriTheCollector.class)); cards.add(new SetCardInfo("Unbreakable Bond", 101, Rarity.UNCOMMON, mage.cards.u.UnbreakableBond.class)); cards.add(new SetCardInfo("Unexpected Fangs", 102, Rarity.COMMON, mage.cards.u.UnexpectedFangs.class)); cards.add(new SetCardInfo("Unlikely Aid", 103, Rarity.COMMON, mage.cards.u.UnlikelyAid.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/ChooseCardTypeEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ChooseCardTypeEffect.java new file mode 100644 index 00000000000..6041004401a --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/ChooseCardTypeEffect.java @@ -0,0 +1,53 @@ +package mage.abilities.effects.common; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.choices.Choice; +import mage.choices.ChoiceCardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.util.CardUtil; + +/** + * @author emerald000 + */ +public class ChooseCardTypeEffect extends OneShotEffect { + + public ChooseCardTypeEffect(Outcome outcome) { + super(outcome); + staticText = "choose a card type"; + } + + private ChooseCardTypeEffect(final ChooseCardTypeEffect effect) { + super(effect); + } + + @Override + public ChooseCardTypeEffect copy() { + return new ChooseCardTypeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + MageObject mageObject = game.getPermanentEntering(source.getSourceId()); + if (mageObject == null) { + mageObject = game.getObject(source.getSourceId()); + } + if (controller != null && mageObject != null) { + Choice typeChoice = new ChoiceCardType(); + if (controller.choose(outcome, typeChoice, game)) { + game.informPlayers(mageObject.getLogName() + ": " + controller.getLogName() + " has chosen: " + typeChoice.getChoice()); + game.getState().setValue(source.getSourceId() + "_type", typeChoice.getChoice()); + if (mageObject instanceof Permanent) { + ((Permanent) mageObject).addInfo("chosen type", CardUtil.addToolTipMarkTags("Chosen type: " + typeChoice.getChoice()), game); + } + return true; + } + } + return false; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/cost/SpellsCostReductionAllOfChosenCardTypeEffect.java b/Mage/src/main/java/mage/abilities/effects/common/cost/SpellsCostReductionAllOfChosenCardTypeEffect.java new file mode 100644 index 00000000000..35d89abd513 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/cost/SpellsCostReductionAllOfChosenCardTypeEffect.java @@ -0,0 +1,39 @@ +package mage.abilities.effects.common.cost; + +import mage.abilities.Ability; +import mage.cards.Card; +import mage.constants.CardType; +import mage.filter.FilterCard; +import mage.game.Game; + +/** + * @author emerald000 + */ +public class SpellsCostReductionAllOfChosenCardTypeEffect extends SpellsCostReductionAllEffect { + + public SpellsCostReductionAllOfChosenCardTypeEffect(FilterCard filter, int amount) { + this(filter, amount, false); + } + + public SpellsCostReductionAllOfChosenCardTypeEffect(FilterCard filter, int amount, boolean onlyControlled) { + super(filter, amount, false, onlyControlled); + } + + public SpellsCostReductionAllOfChosenCardTypeEffect(final SpellsCostReductionAllOfChosenCardTypeEffect effect) { + super(effect); + } + + @Override + public SpellsCostReductionAllOfChosenCardTypeEffect copy() { + return new SpellsCostReductionAllOfChosenCardTypeEffect(this); + } + + @Override + protected boolean selectedByRuntimeData(Card card, Ability source, Game game) { + Object savedType = game.getState().getValue(source.getSourceId() + "_type"); + if (savedType instanceof String) { + return card.getCardType().contains(CardType.fromString((String) savedType)); + } + return false; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/CompanionEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/CompanionEffect.java new file mode 100644 index 00000000000..3b5421483e9 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/keyword/CompanionEffect.java @@ -0,0 +1,40 @@ +package mage.abilities.effects.keyword; + +import mage.abilities.Ability; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.constants.AsThoughEffectType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Game; + +import java.util.UUID; + +/* + * @author emerald000 + */ +public class CompanionEffect extends AsThoughEffectImpl { + + public CompanionEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.Custom, Outcome.Benefit); + staticText = "Once during the game, you may cast your chosen companion from your sideboard"; + } + + private CompanionEffect(final CompanionEffect effect) { + super(effect); + } + + @Override + public CompanionEffect copy() { + return new CompanionEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + return objectId.equals(source.getSourceId()) && affectedControllerId.equals(source.getControllerId()); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/CompanionAbility.java b/Mage/src/main/java/mage/abilities/keyword/CompanionAbility.java new file mode 100644 index 00000000000..b4f6ff5ebac --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/CompanionAbility.java @@ -0,0 +1,41 @@ +package mage.abilities.keyword; + +import mage.abilities.StaticAbility; +import mage.abilities.effects.keyword.CompanionEffect; +import mage.cards.Card; +import mage.constants.Zone; + +import java.util.Set; + +/* + * @author emerald000 + */ +public class CompanionAbility extends StaticAbility { + + private final CompanionCondition condition; + + public CompanionAbility(CompanionCondition condition) { + super(Zone.OUTSIDE, new CompanionEffect()); + this.condition = condition; + } + + private CompanionAbility(final CompanionAbility ability) { + super(ability); + this.condition = ability.condition; + } + + @Override + public CompanionAbility copy() { + return new CompanionAbility(this); + } + + @Override + public String getRule() { + return "Companion — " + condition.getRule(); + } + + public boolean isLegal(Set cards) { + return condition.isLegal(cards); + } +} + diff --git a/Mage/src/main/java/mage/abilities/keyword/CompanionCondition.java b/Mage/src/main/java/mage/abilities/keyword/CompanionCondition.java new file mode 100644 index 00000000000..ce43630cc22 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/CompanionCondition.java @@ -0,0 +1,23 @@ +package mage.abilities.keyword; + +import mage.cards.Card; + +import java.io.Serializable; +import java.util.Set; + +/* + * @author emerald000 + */ +public interface CompanionCondition extends Serializable { + + /** + * @return The rule to get added to the card text. (Everything after the dash) + */ + String getRule(); + + /** + * @param deck The set of cards to check. + * @return Whether the companion is valid for that deck. + */ + boolean isLegal(Set deck); +} diff --git a/Mage/src/main/java/mage/choices/ChoiceCardType.java b/Mage/src/main/java/mage/choices/ChoiceCardType.java new file mode 100644 index 00000000000..45463124c43 --- /dev/null +++ b/Mage/src/main/java/mage/choices/ChoiceCardType.java @@ -0,0 +1,31 @@ +package mage.choices; + +import mage.constants.CardType; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * @author emerald000 + */ +public class ChoiceCardType extends ChoiceImpl { + + public ChoiceCardType() { + this(true); + } + + public ChoiceCardType(boolean required) { + super(required); + this.choices.addAll(Arrays.stream(CardType.values()).map(CardType::toString).collect(Collectors.toList())); + this.message = "Choose a card type"; + } + + private ChoiceCardType(final ChoiceCardType choice) { + super(choice); + } + + @Override + public ChoiceCardType copy() { + return new ChoiceCardType(this); + } +} diff --git a/Mage/src/main/java/mage/constants/CardType.java b/Mage/src/main/java/mage/constants/CardType.java index 821db4a1ba3..e316543006c 100644 --- a/Mage/src/main/java/mage/constants/CardType.java +++ b/Mage/src/main/java/mage/constants/CardType.java @@ -17,9 +17,13 @@ public enum CardType { ENCHANTMENT("Enchantment", true), INSTANT("Instant", false), LAND("Land", true), + PHENOMENON("Phenomenon", false), + PLANE("Plane", false), PLANESWALKER("Planeswalker", true), + SCHEME("Scheme", false), SORCERY("Sorcery", false), - TRIBAL("Tribal", false); + TRIBAL("Tribal", false), + VANGUARD("Vanguard", false); private final String text; private final boolean permanentType; diff --git a/Mage/src/main/java/mage/game/GameCommanderImpl.java b/Mage/src/main/java/mage/game/GameCommanderImpl.java index 6731f4f39f3..36af5d30f8f 100644 --- a/Mage/src/main/java/mage/game/GameCommanderImpl.java +++ b/Mage/src/main/java/mage/game/GameCommanderImpl.java @@ -1,12 +1,11 @@ package mage.game; -import java.util.Map; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.continuous.CommanderReplacementEffect; import mage.abilities.effects.common.cost.CommanderCostModification; +import mage.abilities.keyword.CompanionAbility; import mage.cards.Card; import mage.constants.MultiplayerAttackOption; import mage.constants.PhaseStep; @@ -18,6 +17,9 @@ import mage.players.Player; import mage.watchers.common.CommanderInfoWatcher; import mage.watchers.common.CommanderPlaysCountWatcher; +import java.util.Map; +import java.util.UUID; + public abstract class GameCommanderImpl extends GameImpl { // private final Map mulliganedCards = new HashMap<>(); @@ -54,9 +56,13 @@ public abstract class GameCommanderImpl extends GameImpl { if (player != null) { // add new commanders for (UUID id : player.getSideboard()) { - Card commander = this.getCard(id); - if (commander != null) { - addCommander(commander, player); + Card card = this.getCard(id); + if (card != null) { + // Check for companions. If it is the only card in the sideboard, it is the commander, not a companion. + if (player.getSideboard().size() > 1 && card.getAbilities(this).stream().anyMatch(ability -> ability instanceof CompanionAbility)) { + continue; + } + addCommander(card, player); } } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index d9cd7acba94..f5c99ecd054 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -12,6 +12,7 @@ import mage.abilities.effects.Effect; import mage.abilities.effects.PreventionEffectData; import mage.abilities.effects.common.CopyEffect; import mage.abilities.keyword.BestowAbility; +import mage.abilities.keyword.CompanionAbility; import mage.abilities.keyword.MorphAbility; import mage.abilities.keyword.TransformAbility; import mage.abilities.mana.DelayedTriggeredManaAbility; @@ -929,6 +930,39 @@ public abstract class GameImpl implements Game, Serializable { return; } + // Handle companions + Map playerCompanionMap = new HashMap<>(); + for (Player player : state.getPlayers().values()) { + // Make a list of legal companions present in the sideboard + Set potentialCompanions = new HashSet<>(); + for (Card card : player.getSideboard().getUniqueCards(this)) { + for (Ability ability : card.getAbilities(this)) { + if (ability instanceof CompanionAbility) { + CompanionAbility companionAbility = (CompanionAbility) ability; + if (companionAbility.isLegal(new HashSet<>(player.getLibrary().getCards(this)))) { + potentialCompanions.add(card); + break; + } + } + } + } + // Choose a companion from the list of legal companions + for (Card card : potentialCompanions) { + if (player.chooseUse(Outcome.Benefit, "Use " + card.getName() + " as your companion?", null, this)) { + playerCompanionMap.put(player, card); + break; + } + } + } + + // Announce companions and set the companion effect + playerCompanionMap.forEach((player, companion) -> { + if (companion != null) { + this.informPlayers(player.getLogName() + " has chosen " + companion.getLogName() + " as their companion."); + this.getState().getCompanion().update(player.getName() + "'s companion", new CardsImpl(companion)); + } + }); + //20091005 - 103.1 if (!gameOptions.skipInitShuffling) { //don't shuffle in test mode for card injection on top of player's libraries for (Player player : state.getPlayers().values()) { diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 5f61dc0dc12..f9f344c6662 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -59,6 +59,7 @@ public class GameState implements Serializable, Copyable { // revealed cards >, will be reset if all players pass priority private final Revealed revealed; private final Map lookedAt = new HashMap<>(); + private final Revealed companion; private DelayedTriggeredAbilities delayed; private SpecialActions specialActions; @@ -106,6 +107,7 @@ public class GameState implements Serializable, Copyable { command = new Command(); exile = new Exile(); revealed = new Revealed(); + companion = new Revealed(); battlefield = new Battlefield(); effects = new ContinuousEffects(); triggers = new TriggeredAbilities(); @@ -123,6 +125,7 @@ public class GameState implements Serializable, Copyable { this.choosingPlayerId = state.choosingPlayerId; this.revealed = state.revealed.copy(); this.lookedAt.putAll(state.lookedAt); + this.companion = state.companion.copy(); this.gameOver = state.gameOver; this.paused = state.paused; @@ -473,6 +476,10 @@ public class GameState implements Serializable, Copyable { return lookedAt.get(playerId); } + public Revealed getCompanion() { + return companion; + } + public void clearRevealed() { revealed.clear(); } @@ -481,6 +488,10 @@ public class GameState implements Serializable, Copyable { lookedAt.clear(); } + public void clearCompanion() { + companion.clear(); + } + public Turn getTurn() { return turn; } @@ -1067,6 +1078,7 @@ public class GameState implements Serializable, Copyable { isPlaneChase = false; revealed.clear(); lookedAt.clear(); + companion.clear(); turnNum = 0; stepNum = 0; extraTurn = false; diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 861e4eaef15..74125259fb6 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -3446,6 +3446,15 @@ public abstract class PlayerImpl implements Player, Serializable { } } + // check to play companion cards + if (fromAll || fromZone == Zone.OUTSIDE) { + for (Cards companionCards : game.getState().getCompanion().values()) { + for (Card card : companionCards.getCards(game)) { + getPlayableFromNonHandCardAll(game, Zone.OUTSIDE, card, availableMana, playable); + } + } + } + // check if it's possible to play the top card of a library if (fromAll || fromZone == Zone.LIBRARY) { for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) {