[IKO] Implement Companion and 2 companions

Keruga, the Macrosage and Umori, the Collector
This commit is contained in:
emerald000 2020-04-12 08:23:04 -04:00
parent 395ae9ec11
commit c3684a732b
21 changed files with 866 additions and 128 deletions

View file

@ -30,7 +30,7 @@ public class CardInfoWindowDialog extends MageDialog {
private static final Logger LOGGER = Logger.getLogger(CardInfoWindowDialog.class); private static final Logger LOGGER = Logger.getLogger(CardInfoWindowDialog.class);
public enum ShowType { 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; private final ShowType showType;
@ -72,6 +72,10 @@ public class CardInfoWindowDialog extends MageDialog {
case EXILE: case EXILE:
this.setFrameIcon(new ImageIcon(ImageManagerImpl.instance.getExileImage())); this.setFrameIcon(new ImageIcon(ImageManagerImpl.instance.getExileImage()));
break; break;
case COMPANION:
this.setFrameIcon(new ImageIcon(ImageManagerImpl.instance.getTokenIconImage()));
this.setClosable(false);
break;
default: default:
// no icon yet // no icon yet
} }

View file

@ -40,6 +40,7 @@ import javax.swing.plaf.basic.BasicSplitPaneDivider;
import javax.swing.plaf.basic.BasicSplitPaneUI; import javax.swing.plaf.basic.BasicSplitPaneUI;
import java.awt.*; import java.awt.*;
import java.awt.event.*; import java.awt.event.*;
import java.beans.PropertyVetoException;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.*; import java.util.*;
@ -74,6 +75,7 @@ public final class GamePanel extends javax.swing.JPanel {
private final Map<String, CardInfoWindowDialog> revealed = new HashMap<>(); private final Map<String, CardInfoWindowDialog> revealed = new HashMap<>();
private final Map<String, CardInfoWindowDialog> lookedAt = new HashMap<>(); private final Map<String, CardInfoWindowDialog> lookedAt = new HashMap<>();
private final Map<String, CardInfoWindowDialog> graveyardWindows = new HashMap<>(); private final Map<String, CardInfoWindowDialog> graveyardWindows = new HashMap<>();
private final Map<String, CardInfoWindowDialog> companion = new HashMap<>();
private final Map<String, CardsView> graveyards = new HashMap<>(); private final Map<String, CardsView> graveyards = new HashMap<>();
private final ArrayList<ShowCardsDialog> pickTarget = new ArrayList<>(); private final ArrayList<ShowCardsDialog> pickTarget = new ArrayList<>();
@ -241,6 +243,10 @@ public final class GamePanel extends javax.swing.JPanel {
lookedAtDialog.cleanUp(); lookedAtDialog.cleanUp();
lookedAtDialog.removeDialog(); lookedAtDialog.removeDialog();
} }
for (CardInfoWindowDialog companionDialog : companion.values()) {
companionDialog.cleanUp();
companionDialog.removeDialog();
}
for (ShowCardsDialog pickTargetDialog : pickTarget) { for (ShowCardsDialog pickTargetDialog : pickTarget) {
pickTargetDialog.cleanUp(); pickTargetDialog.cleanUp();
pickTargetDialog.removeDialog(); pickTargetDialog.removeDialog();
@ -275,6 +281,9 @@ public final class GamePanel extends javax.swing.JPanel {
for (CardInfoWindowDialog cardInfoWindowDialog : lookedAt.values()) { for (CardInfoWindowDialog cardInfoWindowDialog : lookedAt.values()) {
cardInfoWindowDialog.changeGUISize(); cardInfoWindowDialog.changeGUISize();
} }
for (CardInfoWindowDialog cardInfoWindowDialog : companion.values()) {
cardInfoWindowDialog.changeGUISize();
}
for (CardInfoWindowDialog cardInfoWindowDialog : graveyardWindows.values()) { for (CardInfoWindowDialog cardInfoWindowDialog : graveyardWindows.values()) {
cardInfoWindowDialog.changeGUISize(); cardInfoWindowDialog.changeGUISize();
} }
@ -781,6 +790,7 @@ public final class GamePanel extends javax.swing.JPanel {
showRevealed(game); showRevealed(game);
showLookedAt(game); showLookedAt(game);
showCompanion(game);
if (!game.getCombat().isEmpty()) { if (!game.getCombat().isEmpty()) {
CombatManager.instance.showCombat(game.getCombat(), gameId); CombatManager.instance.showCombat(game.getCombat(), gameId);
} else { } else {
@ -1085,6 +1095,9 @@ public final class GamePanel extends javax.swing.JPanel {
for (CardInfoWindowDialog lookedAtDialog : lookedAt.values()) { for (CardInfoWindowDialog lookedAtDialog : lookedAt.values()) {
lookedAtDialog.hideDialog(); lookedAtDialog.hideDialog();
} }
for (CardInfoWindowDialog companionDialog : companion.values()) {
companionDialog.hideDialog();
}
} }
// Called if the game frame comes to front again // 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()) { for (CardInfoWindowDialog lookedAtDialog : lookedAt.values()) {
lookedAtDialog.show(); lookedAtDialog.show();
} }
for (CardInfoWindowDialog companionDialog : companion.values()) {
companionDialog.show();
}
} }
public void openGraveyardWindow(String playerName) { public void openGraveyardWindow(String playerName) {
@ -1146,6 +1162,23 @@ public final class GamePanel extends javax.swing.JPanel {
removeClosedCardInfoWindows(lookedAt); 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<String, CardInfoWindowDialog> windowMap, ShowType showType, String name, LinkedHashMap cardsView) { private void handleGameInfoWindow(Map<String, CardInfoWindowDialog> windowMap, ShowType showType, String name, LinkedHashMap cardsView) {
CardInfoWindowDialog cardInfoWindowDialog; CardInfoWindowDialog cardInfoWindowDialog;
if (!windowMap.containsKey(name)) { if (!windowMap.containsKey(name)) {
@ -1160,6 +1193,7 @@ public final class GamePanel extends javax.swing.JPanel {
switch (showType) { switch (showType) {
case REVEAL: case REVEAL:
case REVEAL_TOP_LIBRARY: case REVEAL_TOP_LIBRARY:
case COMPANION:
cardInfoWindowDialog.loadCards((CardsView) cardsView, bigCard, gameId); cardInfoWindowDialog.loadCards((CardsView) cardsView, bigCard, gameId);
break; break;
case LOOKED_AT: case LOOKED_AT:
@ -1332,6 +1366,22 @@ public final class GamePanel extends javax.swing.JPanel {
} }
} }
// companion
for (RevealedView rev : gameView.getCompanion()) {
for (Map.Entry<UUID, CardView> 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 // looked at
for (LookedAtView look : gameView.getLookedAt()) { for (LookedAtView look : gameView.getLookedAt()) {
for (Map.Entry<UUID, SimpleCardView> card : look.getCards().entrySet()) { for (Map.Entry<UUID, SimpleCardView> card : look.getCards().entrySet()) {

View file

@ -49,6 +49,7 @@ public class GameView implements Serializable {
private final List<ExileView> exiles = new ArrayList<>(); private final List<ExileView> exiles = new ArrayList<>();
private final List<RevealedView> revealed = new ArrayList<>(); private final List<RevealedView> revealed = new ArrayList<>();
private List<LookedAtView> lookedAt = new ArrayList<>(); private List<LookedAtView> lookedAt = new ArrayList<>();
private final List<RevealedView> companion = new ArrayList<>();
private final List<CombatGroupView> combat = new ArrayList<>(); private final List<CombatGroupView> combat = new ArrayList<>();
private final TurnPhase phase; private final TurnPhase phase;
private final PhaseStep step; private final PhaseStep step;
@ -149,6 +150,12 @@ public class GameView implements Serializable {
for (String name : state.getRevealed().keySet()) { for (String name : state.getRevealed().keySet()) {
revealed.add(new RevealedView(name, state.getRevealed().get(name), game)); revealed.add(new RevealedView(name, state.getRevealed().get(name), game));
} }
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.phase = state.getTurn().getPhaseType();
this.step = state.getTurn().getStepType(); this.step = state.getTurn().getStepType();
this.turn = state.getTurnNum(); this.turn = state.getTurnNum();
@ -266,6 +273,10 @@ public class GameView implements Serializable {
return lookedAt; return lookedAt;
} }
public List<RevealedView> getCompanion() {
return companion;
}
public void setLookedAt(List<LookedAtView> list) { public void setLookedAt(List<LookedAtView> list) {
this.lookedAt = list; this.lookedAt = list;
} }

View file

@ -1,6 +1,8 @@
package mage.deck; package mage.deck;
import mage.abilities.Ability;
import mage.abilities.common.CanBeYourCommanderAbility; import mage.abilities.common.CanBeYourCommanderAbility;
import mage.abilities.keyword.CompanionAbility;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.decks.Constructed; import mage.cards.decks.Constructed;
import mage.cards.decks.Deck; import mage.cards.decks.Deck;
@ -39,9 +41,50 @@ public class Brawl extends Constructed {
@Override @Override
public boolean validate(Deck deck) { public boolean validate(Deck deck) {
boolean valid = true; boolean valid = true;
Card brawler = null;
Card companion = null;
FilterMana colorIdentity = new FilterMana(); 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<Card> 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"); invalid.put("Deck", "Must contain " + getDeckMinSize() + " cards: has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards");
valid = false; 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<String> basicsInDeck = new HashSet<>(); Set<String> basicsInDeck = new HashSet<>();
if (colorIdentity.isColorless()) { if (colorIdentity.isColorless()) {
for (Card card : deck.getCards()) { for (Card card : deck.getCards()) {
@ -92,6 +118,15 @@ public class Brawl extends Constructed {
valid = false; 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()) { for (Card card : deck.getCards()) {
if (!isSetAllowed(card.getExpansionSetCode())) { if (!isSetAllowed(card.getExpansionSetCode())) {
if (!legalSets(card)) { if (!legalSets(card)) {
@ -108,7 +143,22 @@ public class Brawl extends Constructed {
} }
} }
} }
// Check for companion legality
if (companion != null) {
Set<Card> 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; return valid;
} }
} }

View file

@ -4,6 +4,7 @@ import mage.ObjectColor;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.CanBeYourCommanderAbility; import mage.abilities.common.CanBeYourCommanderAbility;
import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCost;
import mage.abilities.keyword.CompanionAbility;
import mage.abilities.keyword.PartnerAbility; import mage.abilities.keyword.PartnerAbility;
import mage.abilities.keyword.PartnerWithAbility; import mage.abilities.keyword.PartnerWithAbility;
import mage.cards.Card; import mage.cards.Card;
@ -91,8 +92,55 @@ public class Commander extends Constructed {
public boolean validate(Deck deck) { public boolean validate(Deck deck) {
boolean valid = true; boolean valid = true;
FilterMana colorIdentity = new FilterMana(); FilterMana colorIdentity = new FilterMana();
Set<Card> 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<Card> 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<Card> 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"); invalid.put("Deck", "Must contain " + 100 + " cards: has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards");
valid = false; valid = false;
} }
@ -109,48 +157,40 @@ public class Commander extends Constructed {
} }
} }
if (deck.getSideboard().isEmpty() || deck.getSideboard().size() > 2) { Set<String> commanderNames = new HashSet<>();
if ((deck.getSideboard().size() > 1 && !partnerAllowed)) { for (Card commander : commanders) {
invalid.put("Commander", "You may only have one commander"); 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)"); if ((!commander.isCreature() || !commander.isLegendary())
valid = false; && (!commander.isPlaneswalker() || !commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance()))) {
} else { invalid.put("Commander", "Commander invalid (" + commander.getName() + ')');
Set<String> commanderNames = new HashSet<>(); valid = false;
for (Card commander : deck.getSideboard()) {
commanderNames.add(commander.getName());
} }
for (Card commander : deck.getSideboard()) { if (commanders.size() == 2) {
if (bannedCommander.contains(commander.getName())) { if (commander.getAbilities().contains(PartnerAbility.getInstance())) {
invalid.put("Commander", "Commander banned (" + commander.getName() + ')'); if (bannedPartner.contains(commander.getName())) {
valid = false; invalid.put("Commander", "Partner banned (" + commander.getName() + ')');
} valid = false;
if ((!commander.isCreature() || !commander.isLegendary()) }
&& (!commander.isPlaneswalker() || !commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance()))) { } else {
invalid.put("Commander", "Commander invalid (" + commander.getName() + ')'); boolean partnersWith = commander.getAbilities()
valid = false; .stream()
} .filter(PartnerWithAbility.class::isInstance)
if (deck.getSideboard().size() == 2) { .map(PartnerWithAbility.class::cast)
if (commander.getAbilities().contains(PartnerAbility.getInstance())) { .map(PartnerWithAbility::getPartnerName)
if (bannedPartner.contains(commander.getName())) { .anyMatch(commanderNames::contains);
invalid.put("Commander", "Partner banned (" + commander.getName() + ')'); if (!partnersWith) {
valid = false; invalid.put("Commander", "Commander without Partner (" + 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 // no needs in cards check on wrong commanders
@ -164,6 +204,12 @@ public class Commander extends Constructed {
valid = false; 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()) { for (Card card : deck.getCards()) {
if (!isSetAllowed(card.getExpansionSetCode())) { if (!isSetAllowed(card.getExpansionSetCode())) {
if (!legalSets(card)) { if (!legalSets(card)) {
@ -180,6 +226,21 @@ public class Commander extends Constructed {
} }
} }
} }
// Check for companion legality
if (companion != null) {
Set<Card> 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; return valid;
} }

View file

@ -1,6 +1,7 @@
package mage.deck; package mage.deck;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.keyword.CompanionAbility;
import mage.abilities.keyword.PartnerAbility; import mage.abilities.keyword.PartnerAbility;
import mage.abilities.keyword.PartnerWithAbility; import mage.abilities.keyword.PartnerWithAbility;
import mage.cards.Card; import mage.cards.Card;
@ -49,8 +50,55 @@ public class FreeformCommander extends Constructed {
public boolean validate(Deck deck) { public boolean validate(Deck deck) {
boolean valid = true; boolean valid = true;
FilterMana colorIdentity = new FilterMana(); FilterMana colorIdentity = new FilterMana();
Set<Card> 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<Card> 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<Card> 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"); invalid.put("Deck", "Must contain " + 100 + " cards: has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards");
valid = false; valid = false;
} }
@ -58,39 +106,32 @@ public class FreeformCommander extends Constructed {
Map<String, Integer> counts = new HashMap<>(); Map<String, Integer> counts = new HashMap<>();
countCards(counts, deck.getCards()); countCards(counts, deck.getCards());
countCards(counts, deck.getSideboard()); countCards(counts, deck.getSideboard());
valid = checkCounts(1, counts) && valid; valid = checkCounts(1, counts) && valid;
if (deck.getSideboard().isEmpty() || deck.getSideboard().size() > 2) { Set<String> commanderNames = new HashSet<>();
invalid.put("Commander", "Sideboard must contain only the commander(s)"); for (Card commander : commanders) {
valid = false; commanderNames.add(commander.getName());
} else { }
Set<String> commanderNames = new HashSet<>(); for (Card commander : commanders) {
for (Card commander : deck.getSideboard()) { if (!commander.isCreature() || !commander.isLegendary()) {
commanderNames.add(commander.getName()); 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 (commanders.size() == 2) {
if (!(commander.isCreature() if (!commander.getAbilities().contains(PartnerAbility.getInstance())) {
|| commander.isLegendary())) { boolean partnersWith = commander.getAbilities()
invalid.put("Commander", "For Freeform Commander, the commander must be a creature or be legendary. Yours was: " + commander.getName()); .stream()
valid = false; .filter(PartnerWithAbility.class::isInstance)
} .map(PartnerWithAbility.class::cast)
if (deck.getSideboard().size() == 2 && !commander.getAbilities().contains(PartnerAbility.getInstance())) { .map(PartnerWithAbility::getPartnerName)
boolean partnersWith = false; .anyMatch(commanderNames::contains);
for (Ability ability : commander.getAbilities()) {
if (ability instanceof PartnerWithAbility
&& commanderNames.contains(((PartnerWithAbility) ability).getPartnerName())) {
partnersWith = true;
break;
}
}
if (!partnersWith) { if (!partnersWith) {
invalid.put("Commander", "Commander without Partner (" + commander.getName() + ')'); invalid.put("Commander", "Commander without Partner (" + commander.getName() + ')');
valid = false; valid = false;
} }
} }
ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity());
} }
ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity());
} }
// no needs in cards check on wrong commanders // no needs in cards check on wrong commanders
@ -104,12 +145,24 @@ public class FreeformCommander extends Constructed {
valid = false; valid = false;
} }
} }
for (Card card : deck.getSideboard()) { for (Card card : deck.getSideboard()) {
if (!isSetAllowed(card.getExpansionSetCode())) { if (!ManaUtil.isColorIdentityCompatible(colorIdentity, card.getColorIdentity())) {
if (!legalSets(card)) { invalid.put(card.getName(), "Invalid color (" + colorIdentity.toString() + ')');
invalid.put(card.getName(), "Not allowed Set: " + card.getExpansionSetCode()); valid = false;
valid = false; }
}
// Check for companion legality
if (companion != null) {
Set<Card> 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;
} }
} }
} }

View file

@ -2,6 +2,7 @@ package mage.deck;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.CanBeYourCommanderAbility; import mage.abilities.common.CanBeYourCommanderAbility;
import mage.abilities.keyword.CompanionAbility;
import mage.abilities.keyword.PartnerAbility; import mage.abilities.keyword.PartnerAbility;
import mage.abilities.keyword.PartnerWithAbility; import mage.abilities.keyword.PartnerWithAbility;
import mage.cards.Card; import mage.cards.Card;
@ -51,25 +52,63 @@ public class PennyDreadfulCommander extends Constructed {
public boolean validate(Deck deck) { public boolean validate(Deck deck) {
boolean valid = true; boolean valid = true;
FilterMana colorIdentity = new FilterMana(); FilterMana colorIdentity = new FilterMana();
Set<Card> 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<Card> 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<Card> 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"); invalid.put("Deck", "Must contain " + 100 + " cards: has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards");
valid = false; valid = false;
} }
List<String> basicLandNames = new ArrayList<>(Arrays.asList("Forest", "Island", "Mountain", "Swamp", "Plains", "Wastes"));
Map<String, Integer> counts = new HashMap<>(); Map<String, Integer> counts = new HashMap<>();
countCards(counts, deck.getCards()); countCards(counts, deck.getCards());
countCards(counts, deck.getSideboard()); countCards(counts, deck.getSideboard());
valid = checkCounts(1, counts) && valid;
for (Map.Entry<String, Integer> entry : counts.entrySet()) {
if (entry.getValue() > 1) {
if (!basicLandNames.contains(entry.getKey())) {
invalid.put(entry.getKey(), "Too many: " + entry.getValue());
valid = false;
}
}
}
generatePennyDreadfulHash(); generatePennyDreadfulHash();
for (String wantedCard : counts.keySet()) { for (String wantedCard : counts.keySet()) {
@ -79,36 +118,35 @@ public class PennyDreadfulCommander extends Constructed {
} }
} }
if (deck.getSideboard().isEmpty() || deck.getSideboard().size() > 2) { Set<String> commanderNames = new HashSet<>();
invalid.put("Commander", "Sideboard must contain only the commander(s)"); for (Card commander : commanders) {
valid = false; commanderNames.add(commander.getName());
} else { }
Set<String> commanderNames = new HashSet<>(); for (Card commander : commanders) {
for (Card commander : deck.getSideboard()) { if (bannedCommander.contains(commander.getName())) {
commanderNames.add(commander.getName()); invalid.put("Commander", "Commander banned (" + commander.getName() + ')');
valid = false;
} }
for (Card commander : deck.getSideboard()) { if ((!commander.isCreature() || !commander.isLegendary())
if ((!commander.isCreature() || !commander.isLegendary()) && (!commander.isPlaneswalker() || !commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance()))) {
&& (!commander.isPlaneswalker() || !commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance()))) { invalid.put("Commander", "Commander invalid (" + commander.getName() + ')');
invalid.put("Commander", "Commander invalid (" + commander.getName() + ')'); valid = false;
valid = false; }
} if (commanders.size() == 2) {
if (deck.getSideboard().size() == 2 && !commander.getAbilities().contains(PartnerAbility.getInstance())) { if (!commander.getAbilities().contains(PartnerAbility.getInstance())) {
boolean partnersWith = false; boolean partnersWith = commander.getAbilities()
for (Ability ability : commander.getAbilities()) { .stream()
if (ability instanceof PartnerWithAbility .filter(PartnerWithAbility.class::isInstance)
&& commanderNames.contains(((PartnerWithAbility) ability).getPartnerName())) { .map(PartnerWithAbility.class::cast)
partnersWith = true; .map(PartnerWithAbility::getPartnerName)
break; .anyMatch(commanderNames::contains);
}
}
if (!partnersWith) { if (!partnersWith) {
invalid.put("Commander", "Commander without Partner (" + commander.getName() + ')'); invalid.put("Commander", "Commander without Partner (" + commander.getName() + ')');
valid = false; valid = false;
} }
} }
ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity());
} }
ManaUtil.collectColorIdentity(colorIdentity, commander.getColorIdentity());
} }
// no needs in cards check on wrong commanders // no needs in cards check on wrong commanders
@ -122,6 +160,12 @@ public class PennyDreadfulCommander extends Constructed {
valid = false; 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()) { for (Card card : deck.getCards()) {
if (!isSetAllowed(card.getExpansionSetCode())) { if (!isSetAllowed(card.getExpansionSetCode())) {
if (!legalSets(card)) { if (!legalSets(card)) {
@ -138,6 +182,21 @@ public class PennyDreadfulCommander extends Constructed {
} }
} }
} }
// Check for companion legality
if (companion != null) {
Set<Card> 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; return valid;
} }

View file

@ -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<Card> deck) {
return deck.stream().allMatch(card -> card.isLand() || card.getConvertedManaCost() >= 3);
}
}

View file

@ -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<Card> deck) {
Set<CardType> 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;
}
}

View file

@ -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("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("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("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 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("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)); 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("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("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("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("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("Unexpected Fangs", 102, Rarity.COMMON, mage.cards.u.UnexpectedFangs.class));
cards.add(new SetCardInfo("Unlikely Aid", 103, Rarity.COMMON, mage.cards.u.UnlikelyAid.class)); cards.add(new SetCardInfo("Unlikely Aid", 103, Rarity.COMMON, mage.cards.u.UnlikelyAid.class));

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 &mdash; " + condition.getRule();
}
public boolean isLegal(Set<Card> cards) {
return condition.isLegal(cards);
}
}

View file

@ -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<Card> deck);
}

View file

@ -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);
}
}

View file

@ -17,9 +17,13 @@ public enum CardType {
ENCHANTMENT("Enchantment", true), ENCHANTMENT("Enchantment", true),
INSTANT("Instant", false), INSTANT("Instant", false),
LAND("Land", true), LAND("Land", true),
PHENOMENON("Phenomenon", false),
PLANE("Plane", false),
PLANESWALKER("Planeswalker", true), PLANESWALKER("Planeswalker", true),
SCHEME("Scheme", false),
SORCERY("Sorcery", false), SORCERY("Sorcery", false),
TRIBAL("Tribal", false); TRIBAL("Tribal", false),
VANGUARD("Vanguard", false);
private final String text; private final String text;
private final boolean permanentType; private final boolean permanentType;

View file

@ -1,12 +1,11 @@
package mage.game; package mage.game;
import java.util.Map;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.continuous.CommanderReplacementEffect; import mage.abilities.effects.common.continuous.CommanderReplacementEffect;
import mage.abilities.effects.common.cost.CommanderCostModification; import mage.abilities.effects.common.cost.CommanderCostModification;
import mage.abilities.keyword.CompanionAbility;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.MultiplayerAttackOption; import mage.constants.MultiplayerAttackOption;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
@ -18,6 +17,9 @@ import mage.players.Player;
import mage.watchers.common.CommanderInfoWatcher; import mage.watchers.common.CommanderInfoWatcher;
import mage.watchers.common.CommanderPlaysCountWatcher; import mage.watchers.common.CommanderPlaysCountWatcher;
import java.util.Map;
import java.util.UUID;
public abstract class GameCommanderImpl extends GameImpl { public abstract class GameCommanderImpl extends GameImpl {
// private final Map<UUID, Cards> mulliganedCards = new HashMap<>(); // private final Map<UUID, Cards> mulliganedCards = new HashMap<>();
@ -54,9 +56,13 @@ public abstract class GameCommanderImpl extends GameImpl {
if (player != null) { if (player != null) {
// add new commanders // add new commanders
for (UUID id : player.getSideboard()) { for (UUID id : player.getSideboard()) {
Card commander = this.getCard(id); Card card = this.getCard(id);
if (commander != null) { if (card != null) {
addCommander(commander, player); // 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);
} }
} }

View file

@ -12,6 +12,7 @@ import mage.abilities.effects.Effect;
import mage.abilities.effects.PreventionEffectData; import mage.abilities.effects.PreventionEffectData;
import mage.abilities.effects.common.CopyEffect; import mage.abilities.effects.common.CopyEffect;
import mage.abilities.keyword.BestowAbility; import mage.abilities.keyword.BestowAbility;
import mage.abilities.keyword.CompanionAbility;
import mage.abilities.keyword.MorphAbility; import mage.abilities.keyword.MorphAbility;
import mage.abilities.keyword.TransformAbility; import mage.abilities.keyword.TransformAbility;
import mage.abilities.mana.DelayedTriggeredManaAbility; import mage.abilities.mana.DelayedTriggeredManaAbility;
@ -929,6 +930,39 @@ public abstract class GameImpl implements Game, Serializable {
return; return;
} }
// Handle companions
Map<Player, Card> playerCompanionMap = new HashMap<>();
for (Player player : state.getPlayers().values()) {
// Make a list of legal companions present in the sideboard
Set<Card> 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 //20091005 - 103.1
if (!gameOptions.skipInitShuffling) { //don't shuffle in test mode for card injection on top of player's libraries if (!gameOptions.skipInitShuffling) { //don't shuffle in test mode for card injection on top of player's libraries
for (Player player : state.getPlayers().values()) { for (Player player : state.getPlayers().values()) {

View file

@ -59,6 +59,7 @@ public class GameState implements Serializable, Copyable<GameState> {
// revealed cards <Name, <Cards>>, will be reset if all players pass priority // revealed cards <Name, <Cards>>, will be reset if all players pass priority
private final Revealed revealed; private final Revealed revealed;
private final Map<UUID, LookedAt> lookedAt = new HashMap<>(); private final Map<UUID, LookedAt> lookedAt = new HashMap<>();
private final Revealed companion;
private DelayedTriggeredAbilities delayed; private DelayedTriggeredAbilities delayed;
private SpecialActions specialActions; private SpecialActions specialActions;
@ -106,6 +107,7 @@ public class GameState implements Serializable, Copyable<GameState> {
command = new Command(); command = new Command();
exile = new Exile(); exile = new Exile();
revealed = new Revealed(); revealed = new Revealed();
companion = new Revealed();
battlefield = new Battlefield(); battlefield = new Battlefield();
effects = new ContinuousEffects(); effects = new ContinuousEffects();
triggers = new TriggeredAbilities(); triggers = new TriggeredAbilities();
@ -123,6 +125,7 @@ public class GameState implements Serializable, Copyable<GameState> {
this.choosingPlayerId = state.choosingPlayerId; this.choosingPlayerId = state.choosingPlayerId;
this.revealed = state.revealed.copy(); this.revealed = state.revealed.copy();
this.lookedAt.putAll(state.lookedAt); this.lookedAt.putAll(state.lookedAt);
this.companion = state.companion.copy();
this.gameOver = state.gameOver; this.gameOver = state.gameOver;
this.paused = state.paused; this.paused = state.paused;
@ -473,6 +476,10 @@ public class GameState implements Serializable, Copyable<GameState> {
return lookedAt.get(playerId); return lookedAt.get(playerId);
} }
public Revealed getCompanion() {
return companion;
}
public void clearRevealed() { public void clearRevealed() {
revealed.clear(); revealed.clear();
} }
@ -481,6 +488,10 @@ public class GameState implements Serializable, Copyable<GameState> {
lookedAt.clear(); lookedAt.clear();
} }
public void clearCompanion() {
companion.clear();
}
public Turn getTurn() { public Turn getTurn() {
return turn; return turn;
} }
@ -1067,6 +1078,7 @@ public class GameState implements Serializable, Copyable<GameState> {
isPlaneChase = false; isPlaneChase = false;
revealed.clear(); revealed.clear();
lookedAt.clear(); lookedAt.clear();
companion.clear();
turnNum = 0; turnNum = 0;
stepNum = 0; stepNum = 0;
extraTurn = false; extraTurn = false;

View file

@ -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 // check if it's possible to play the top card of a library
if (fromAll || fromZone == Zone.LIBRARY) { if (fromAll || fromZone == Zone.LIBRARY) {
for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) {