diff --git a/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java b/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java index 1fece57129..503f59e6b6 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java @@ -668,6 +668,12 @@ public class NewTableDialog extends MageDialog { return false; } break; + case "Variant Magic - Oathbreaker": + if (!options.getGameType().startsWith("Oathbreaker")) { + JOptionPane.showMessageDialog(MageFrame.getDesktop(), "Deck type Oathbreaker needs also a Oathbreaker game type", "Error", JOptionPane.ERROR_MESSAGE); + return false; + } + break; } // game => deck @@ -704,6 +710,13 @@ public class NewTableDialog extends MageDialog { return false; } break; + case "Oathbreaker Two Player Duel": + case "Oathbreaker Free For All": + if (!options.getDeckType().equals("Variant Magic - Oathbreaker")) { + JOptionPane.showMessageDialog(MageFrame.getDesktop(), "Deck type Oathbreaker needs also a Oathbreaker game type", "Error", JOptionPane.ERROR_MESSAGE); + return false; + } + break; } return true; } diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Oathbreaker.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Oathbreaker.java new file mode 100644 index 0000000000..a8223820aa --- /dev/null +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Oathbreaker.java @@ -0,0 +1,215 @@ +package mage.deck; + +import mage.abilities.Ability; +import mage.abilities.keyword.PartnerAbility; +import mage.abilities.keyword.PartnerWithAbility; +import mage.cards.Card; +import mage.cards.decks.Deck; +import mage.filter.FilterMana; + +import java.util.*; + +/** + * @author JayDi85 + */ +public class Oathbreaker extends Vintage { + + protected List bannedCommander = new ArrayList<>(); + private static final Map pdAllowed = new HashMap<>(); + + public Oathbreaker() { + super(); + this.name = "Oathbreaker"; + + // banned = vintage + oathbreaker's list: https://weirdcards.org/oathbreaker-ban-list + // last updated 4/4/19 - High Tide banned + banned.add("Ad Nauseam"); + banned.add("Ancestral Recall"); + banned.add("Balance"); + banned.add("Biorhythm"); + banned.add("Black Lotus"); + banned.add("Channel"); + banned.add("Doomsday"); + banned.add("Emrakul, the Aeons Torn"); + banned.add("Expropriate"); + banned.add("Fastbond"); + banned.add("Gifts Ungiven"); + banned.add("Griselbrand"); + banned.add("High Tide"); + banned.add("Library of Alexandria"); + banned.add("Limited Resources"); + banned.add("Lion's Eye Diamond"); + banned.add("Mana Crypt"); + banned.add("Mana Geyser"); + banned.add("Mana Vault"); + banned.add("Mox Emerald"); + banned.add("Mox Jet"); + banned.add("Mox Pearl"); + banned.add("Mox Ruby"); + banned.add("Mox Sapphire"); + banned.add("Natural Order"); + banned.add("Painter's Servant"); + banned.add("Panoptic Mirror"); + banned.add("Primal Surge"); + banned.add("Primeval Titan"); + banned.add("Recurring Nightmare"); + banned.add("Saheeli, the Gifted"); + banned.add("Sol Ring"); + banned.add("Sundering Titan"); + banned.add("Sway of the Stars"); + banned.add("Sylvan Primordial"); + banned.add("Time Vault"); + banned.add("Time Walk"); + banned.add("Tinker"); + banned.add("Tolarian Academy"); + banned.add("Tooth and Nail"); + banned.add("Trade Secrets"); + banned.add("Upheaval"); + banned.add("Worldfire"); + banned.add("Yawgmoth's Bargain"); + } + + @Override + public int getDeckMinSize() { + return 60 - (1 + 2); // spell + 2 x partner oathbreakers + } + + @Override + public int getSideboardMinSize() { + return 2; // spell + oathbreaker + } + + @Override + public boolean validate(Deck deck) { + boolean valid = true; + FilterMana colorIdentity = new FilterMana(); + + if (deck.getCards().size() + deck.getSideboard().size() != 60) { + invalid.put("Deck", "Must contain " + 60 + " cards: has " + (deck.getCards().size() + deck.getSideboard().size()) + " cards"); + valid = false; + } + + 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()) && !anyNumberCardsAllowed.contains(entry.getKey())) { + invalid.put(entry.getKey(), "Too many: " + entry.getValue()); + valid = false; + } + } + } + + Set commanderNames = new HashSet<>(); + String signatureSpell = null; + if (deck.getSideboard().size() < 2 || deck.getSideboard().size() > 3) { + invalid.put("Oathbreaker", "Sideboard must contain only the oathbreaker(s) with signature spell"); + valid = false; + } else { + for (Card commander : deck.getSideboard()) { + if (commander.isInstantOrSorcery()) { + if (signatureSpell == null) { + signatureSpell = commander.getName(); + } else { + invalid.put("Signature spell", "Only one signature spell allows, but found: " + signatureSpell + " and " + commander.getName()); + valid = false; + } + } else { + if (commander.isPlaneswalker()) { + commanderNames.add(commander.getName()); + } else { + invalid.put("Oathbreaker", "Only planeswalker can be Oathbreaker, not " + commander.getName()); + valid = false; + } + } + } + + for (Card commander : deck.getSideboard()) { + if (commanderNames.contains(commander.getName())) { + // partner checks + if (commanderNames.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 (!partnersWith) { + invalid.put("Oathbreaker", "Oathbreaker without Partner (" + commander.getName() + ')'); + valid = false; + } + } + + // color identity from commanders only, not spell + FilterMana commanderColor = commander.getColorIdentity(); + if (commanderColor.isWhite()) { + colorIdentity.setWhite(true); + } + if (commanderColor.isBlue()) { + colorIdentity.setBlue(true); + } + if (commanderColor.isBlack()) { + colorIdentity.setBlack(true); + } + if (commanderColor.isRed()) { + colorIdentity.setRed(true); + } + if (commanderColor.isGreen()) { + colorIdentity.setGreen(true); + } + } + } + + if (commanderNames.size() == 0) { + invalid.put("Sideboard", "Can't find any oathbreaker"); + valid = false; + } + if (signatureSpell == null) { + invalid.put("Sideboard", "Can't find signature spell"); + valid = false; + } + } + + // signature spell color + for (Card card : deck.getSideboard()) { + if (card.getName().equals(signatureSpell) && !cardHasValidColor(colorIdentity, card)) { + invalid.put(card.getName(), "Invalid color for signature spell (" + colorIdentity.toString() + ')'); + valid = false; + } + } + + // no needs in cards check on wrong commanders + if (!valid) { + return false; + } + + for (Card card : deck.getCards()) { + if (!cardHasValidColor(colorIdentity, card)) { + invalid.put(card.getName(), "Invalid color (" + colorIdentity.toString() + ')'); + 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; + } + } + } + return valid; + } + + public boolean cardHasValidColor(FilterMana commander, Card card) { + FilterMana cardColor = card.getColorIdentity(); + return !(cardColor.isBlack() && !commander.isBlack() + || cardColor.isBlue() && !commander.isBlue() + || cardColor.isGreen() && !commander.isGreen() + || cardColor.isRed() && !commander.isRed() + || cardColor.isWhite() && !commander.isWhite()); + } +} diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml new file mode 100644 index 0000000000..b7f1d06232 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + org.mage + mage-server-plugins + 1.4.35 + + + mage-game-oathbreakerfreeforall + jar + Mage Game Oathbreaker Free For All + + + + ${project.groupId} + mage + ${project.version} + + + + + src + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + maven-resources-plugin + + UTF-8 + + + + + + mage-game-freeforall + + + + + diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAll.java b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAll.java new file mode 100644 index 0000000000..323fda1d68 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAll.java @@ -0,0 +1,141 @@ +package mage.game; + +import mage.abilities.Ability; +import mage.abilities.common.SignatureSpellCastOnlyWithOathbreakerEffect; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.OathbreakerOnBattlefieldCondition; +import mage.abilities.effects.common.InfoEffect; +import mage.abilities.hint.ConditionHint; +import mage.cards.Card; +import mage.constants.MultiplayerAttackOption; +import mage.constants.RangeOfInfluence; +import mage.game.match.MatchType; +import mage.game.mulligan.Mulligan; +import mage.players.Player; +import mage.watchers.common.CommanderInfoWatcher; + +import java.util.*; + +/** + * @author JayDi85 + */ +public class OathbreakerFreeForAll extends GameCommanderImpl { + + private int numPlayers; + private Map playerSignatureSpell = new HashMap<>(); + private Map> playerCommanders = new HashMap<>(); + + public OathbreakerFreeForAll(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife) { + super(attackOption, range, mulligan, startLife); + } + + public OathbreakerFreeForAll(final OathbreakerFreeForAll game) { + super(game); + this.numPlayers = game.numPlayers; + this.playerSignatureSpell.putAll(game.playerSignatureSpell); + game.playerCommanders.forEach((key, value) -> this.playerCommanders.put(key, new ArrayList<>(value))); + } + + @Override + protected void init(UUID choosingPlayerId) { + /* + // prepare commanders and signature spells info + playerSignatureSpell.clear(); + playerCommanders.clear(); + for (UUID playerId : state.getPlayerList(startingPlayerId)) { + UUID signatureSpell = null; + List commanders = new ArrayList<>(); + + Player player = getPlayer(playerId); + List searchList = new ArrayList<>(); + searchList.addAll(player.getCommandersIds()); + searchList.addAll(new ArrayList<>(player.getSideboard())); + for (UUID id : searchList) { + Card commander = this.getCard(id); + if (commander != null) { + if (commander.isInstantOrSorcery()) { + signatureSpell = commander.getId(); + } else if (!commanders.contains(commander.getId())) { + commanders.add(commander.getId()); + } + } + } + + playerSignatureSpell.put(playerId, signatureSpell); + playerCommanders.put(playerId, commanders); + } + */ + + // init base commander game + startingPlayerSkipsDraw = false; + super.init(choosingPlayerId); + } + + @Override + public CommanderInfoWatcher initCommanderWatcher(Card commander, boolean checkCommanderDamage) { + String commanderType; + if (commander.isInstantOrSorcery()) { + commanderType = "Signature Spell"; + } else { + commanderType = "Oathbreaker"; + } + return new CommanderInfoWatcher(commanderType, commander.getId(), checkCommanderDamage); + } + + @Override + public void initCommanderEffects(Card commander, Player player, Ability commanderAbility) { + // all commander effects must be independent from sourceId or controllerId + super.initCommanderEffects(commander, player, commanderAbility); + + // signature spell restrict (spell can be casted on player's commander on battlefield) + if (commander.getId().equals(this.playerSignatureSpell.getOrDefault(player.getId(), null))) { + Condition condition = new OathbreakerOnBattlefieldCondition(player.getId(), this.playerCommanders.getOrDefault(player.getId(), null)); + commanderAbility.addEffect(new SignatureSpellCastOnlyWithOathbreakerEffect(condition, commander.getId())); + + // hint must be added to card, not global ability + Ability ability = new SimpleStaticAbility(new InfoEffect("Signature spell hint")); + ability.addHint(new ConditionHint(condition, "Oathbreaker on battlefield")); + ability.setRuleVisible(false); + commander.addAbility(ability); + } + } + + @Override + public void addCommander(Card card, Player player) { + super.addCommander(card, player); + + // prepare signature and commanders info + if (card.isInstantOrSorcery()) { + this.playerSignatureSpell.put(player.getId(), card.getId()); + } else { + List list = this.playerCommanders.getOrDefault(player.getId(), null); + if (list == null) { + list = new ArrayList<>(); + this.playerCommanders.put(player.getId(), list); + } + if (!list.contains(card.getId())) { + list.add(card.getId()); + } + } + } + + @Override + public MatchType getGameType() { + return new OathbreakerFreeForAllType(); + } + + @Override + public int getNumPlayers() { + return numPlayers; + } + + public void setNumPlayers(int numPlayers) { + this.numPlayers = numPlayers; + } + + @Override + public OathbreakerFreeForAll copy() { + return new OathbreakerFreeForAll(this); + } +} diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAllMatch.java b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAllMatch.java new file mode 100644 index 0000000000..0a7086a7c2 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAllMatch.java @@ -0,0 +1,29 @@ +package mage.game; + +import mage.game.match.MatchImpl; +import mage.game.match.MatchOptions; +import mage.game.mulligan.Mulligan; + +/** + * @author JayDi85 + */ +public class OathbreakerFreeForAllMatch extends MatchImpl { + + public OathbreakerFreeForAllMatch(MatchOptions options) { + super(options); + } + + @Override + public void startGame() throws GameException { + int startLife = 20; + boolean alsoHand = true; + Mulligan mulligan = options.getMulliganType().getMulligan(options.getFreeMulligans()); + OathbreakerFreeForAll game = new OathbreakerFreeForAll(options.getAttackOption(), options.getRange(), mulligan, startLife); + game.setStartMessage(this.createGameStartMessage()); + game.setAlsoHand(alsoHand); + game.setAlsoLibrary(true); + initGame(game); + games.add(game); + } + +} diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAllType.java b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAllType.java new file mode 100644 index 0000000000..ab867ce046 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/src/mage/game/OathbreakerFreeForAllType.java @@ -0,0 +1,29 @@ +package mage.game; + +import mage.game.match.MatchType; + + +/** + * @author JayDi85 + */ +public class OathbreakerFreeForAllType extends MatchType { + + public OathbreakerFreeForAllType() { + this.name = "Oathbreaker Free For All"; + this.maxPlayers = 10; + this.minPlayers = 3; + this.numTeams = 0; + this.useAttackOption = true; + this.useRange = true; + this.sideboardingAllowed = false; + } + + protected OathbreakerFreeForAllType(final OathbreakerFreeForAllType matchType) { + super(matchType); + } + + @Override + public OathbreakerFreeForAllType copy() { + return new OathbreakerFreeForAllType(this); + } +} diff --git a/Mage.Server.Plugins/pom.xml b/Mage.Server.Plugins/pom.xml index 248976e212..497e462c37 100644 --- a/Mage.Server.Plugins/pom.xml +++ b/Mage.Server.Plugins/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -26,19 +27,20 @@ Mage.Game.CanadianHighlanderDuel Mage.Game.PennyDreadfulCommanderFreeForAll Mage.Game.FreeformCommanderDuel - Mage.Game.FreeformCommanderFreeForAll - Mage.Game.BrawlDuel + Mage.Game.FreeformCommanderFreeForAll + Mage.Game.BrawlDuel Mage.Game.BrawlFreeForAll + Mage.Game.OathbreakerFreeForAll Mage.Game.TwoPlayerDuel Mage.Player.AI Mage.Player.AIMinimax Mage.Player.AI.MA Mage.Player.AIMCTS - Mage.Player.AI.DraftBot + Mage.Player.AI.DraftBot Mage.Player.Human Mage.Tournament.BoosterDraft Mage.Tournament.Constructed - Mage.Tournament.Sealed + Mage.Tournament.Sealed diff --git a/Mage.Server/config/config.xml b/Mage.Server/config/config.xml index aa2223ceab..500b4c29b2 100644 --- a/Mage.Server/config/config.xml +++ b/Mage.Server/config/config.xml @@ -69,123 +69,234 @@ - + - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - - - - - - - + + + + + + + + - - - + + + - - + + - - - + + + + - - - + + + - - - - - + + + + + - - + + diff --git a/Mage.Server/pom.xml b/Mage.Server/pom.xml index 72d63a77d6..b835f3e7df 100644 --- a/Mage.Server/pom.xml +++ b/Mage.Server/pom.xml @@ -178,6 +178,7 @@ ${project.version} runtime + ${project.groupId} mage-game-freeformcommanderfreeforall @@ -191,6 +192,13 @@ runtime + + ${project.groupId} + mage-game-oathbreakerfreeforall + ${project.version} + runtime + + ${project.groupId} mage-game-momirduel diff --git a/Mage.Server/release/config/config.xml b/Mage.Server/release/config/config.xml index bea0cd3d65..37cdb35f4c 100644 --- a/Mage.Server/release/config/config.xml +++ b/Mage.Server/release/config/config.xml @@ -61,125 +61,264 @@ mailFromAddress="" /> - - - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mage.Server/src/main/java/mage/server/util/SystemUtil.java b/Mage.Server/src/main/java/mage/server/util/SystemUtil.java index ae2a758128..ab298d44b5 100644 --- a/Mage.Server/src/main/java/mage/server/util/SystemUtil.java +++ b/Mage.Server/src/main/java/mage/server/util/SystemUtil.java @@ -522,10 +522,8 @@ public final class SystemUtil { // as commander (only commander games, look at init code in GameCommanderImpl) if (game instanceof GameCommanderImpl) { GameCommanderImpl gameCommander = (GameCommanderImpl) game; - for (Card card : cardsToLoad) { - player.addCommanderId(card.getId()); - gameCommander.initCommander(card, player); - } + cardsToLoad.forEach(card -> gameCommander.addCommander(card, player)); + cardsToLoad.forEach(card -> gameCommander.initCommander(card, player)); } else { logger.fatal("Commander card can be used in commander game only: " + command.cardName); } diff --git a/Mage/src/main/java/mage/abilities/common/SignatureSpellCastOnlyWithOathbreakerEffect.java b/Mage/src/main/java/mage/abilities/common/SignatureSpellCastOnlyWithOathbreakerEffect.java new file mode 100644 index 0000000000..ad64d77eb8 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/SignatureSpellCastOnlyWithOathbreakerEffect.java @@ -0,0 +1,61 @@ +package mage.abilities.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.UUID; + +/** + * For oathbreaker game mode + * + * @author JayDi85 + */ +public class SignatureSpellCastOnlyWithOathbreakerEffect extends ContinuousRuleModifyingEffectImpl { + + private final Condition condition; + private final UUID signatureSpell; + + public SignatureSpellCastOnlyWithOathbreakerEffect(Condition condition, UUID signatureSpell) { + super(Duration.EndOfGame, Outcome.Detriment); + this.condition = condition; + this.signatureSpell = signatureSpell; + staticText = setText(); + } + + private SignatureSpellCastOnlyWithOathbreakerEffect(final SignatureSpellCastOnlyWithOathbreakerEffect effect) { + super(effect); + this.condition = effect.condition; + this.signatureSpell = effect.signatureSpell; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CAST_SPELL; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (event.getSourceId().equals(signatureSpell)) { + return condition != null && !condition.apply(game, source); + } + return false; // cast not prevented by this effect + } + + @Override + public SignatureSpellCastOnlyWithOathbreakerEffect copy() { + return new SignatureSpellCastOnlyWithOathbreakerEffect(this); + } + + private String setText() { + StringBuilder sb = new StringBuilder("cast this spell only "); + if (condition != null) { + sb.append(' ').append(condition.toString()); + } + return sb.toString(); + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/OathbreakerOnBattlefieldCondition.java b/Mage/src/main/java/mage/abilities/condition/common/OathbreakerOnBattlefieldCondition.java new file mode 100644 index 0000000000..06b9e57718 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/OathbreakerOnBattlefieldCondition.java @@ -0,0 +1,51 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.PermanentIdPredicate; +import mage.game.Game; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * For Oathbreaker game mode + * + * @author JayDi85 + */ +public class OathbreakerOnBattlefieldCondition implements Condition { + + private UUID playerId; + private FilterControlledPermanent filter; + + public OathbreakerOnBattlefieldCondition(UUID playerId, List oathbreakersToSearch) { + this.playerId = playerId; + this.filter = new FilterControlledPermanent("oathbreaker on battlefield"); + if (oathbreakersToSearch != null && !oathbreakersToSearch.isEmpty()) { + // any commander on battlefield + List idsList = new ArrayList<>(); + for (UUID id : oathbreakersToSearch) { + idsList.add(new PermanentIdPredicate(id)); + } + this.filter.add(Predicates.or(idsList)); + } else { + // random id to disable condition + this.filter.add(new PermanentIdPredicate(UUID.randomUUID())); + } + } + + @Override + public boolean apply(Game game, Ability source) { + // source.getSourceId() is null for commander's effects + int permanentsOnBattlefield = game.getBattlefield().count(this.filter, source.getSourceId(), playerId, game); + return permanentsOnBattlefield > 0; + } + + @Override + public String toString() { + return filter.getMessage(); + } +} diff --git a/Mage/src/main/java/mage/game/GameCommanderImpl.java b/Mage/src/main/java/mage/game/GameCommanderImpl.java index eb5094f735..c36b09fff9 100644 --- a/Mage/src/main/java/mage/game/GameCommanderImpl.java +++ b/Mage/src/main/java/mage/game/GameCommanderImpl.java @@ -41,6 +41,7 @@ public abstract class GameCommanderImpl extends GameImpl { @Override protected void init(UUID choosingPlayerId) { + // Karn Liberated calls it to restart game, all data and commanders must be re-initialized // plays watcher state.addWatcher(new CommanderPlaysCountWatcher()); @@ -49,20 +50,19 @@ public abstract class GameCommanderImpl extends GameImpl { for (UUID playerId : state.getPlayerList(startingPlayerId)) { Player player = getPlayer(playerId); if (player != null) { - if (player.getSideboard().isEmpty()) { // needed for restart game of e.g. Karn Liberated - for (UUID commanderId : player.getCommandersIds()) { - Card commander = this.getCard(commanderId); - if (commander != null) { - initCommander(commander, player); - } + // add new commanders + for (UUID id : player.getSideboard()) { + Card commander = this.getCard(id); + if (commander != null) { + addCommander(commander, player); } - } else { - while (!player.getSideboard().isEmpty()) { - Card commander = this.getCard(player.getSideboard().iterator().next()); - if (commander != null) { - player.addCommanderId(commander.getId()); - initCommander(commander, player); - } + } + + // init commanders + for (UUID commanderId : player.getCommandersIds()) { + Card commander = this.getCard(commanderId); + if (commander != null) { + initCommander(commander, player); } } } @@ -75,17 +75,27 @@ public abstract class GameCommanderImpl extends GameImpl { } public void initCommander(Card commander, Player player) { - Ability ability = new SimpleStaticAbility(Zone.COMMAND, new InfoEffect("Commander effects")); commander.moveToZone(Zone.COMMAND, null, this, true); commander.getAbilities().setControllerId(player.getId()); - ability.addEffect(new CommanderReplacementEffect(commander.getId(), alsoHand, alsoLibrary)); - ability.addEffect(new CommanderCostModification(commander.getId())); - CommanderInfoWatcher watcher = new CommanderInfoWatcher(commander.getId(), checkCommanderDamage); + + Ability ability = new SimpleStaticAbility(Zone.COMMAND, new InfoEffect("Commander effects")); + initCommanderEffects(commander, player, ability); + CommanderInfoWatcher watcher = initCommanderWatcher(commander, checkCommanderDamage); getState().addWatcher(watcher); watcher.addCardInfoToCommander(this); this.getState().addAbility(ability, null); } + public CommanderInfoWatcher initCommanderWatcher(Card commander, boolean checkCommanderDamage) { + return new CommanderInfoWatcher("Commander", commander.getId(), checkCommanderDamage); + } + + public void initCommanderEffects(Card commander, Player player, Ability commanderAbility) { + // all commander effects must be independent from sourceId or controllerId + commanderAbility.addEffect(new CommanderReplacementEffect(commander.getId(), alsoHand, alsoLibrary)); + commanderAbility.addEffect(new CommanderCostModification(commander.getId())); + } + //20130711 /*903.8. The Commander variant uses an alternate mulligan rule. * Each time a player takes a mulligan, rather than shuffling their entire hand of cards into their library, that player exiles any number of cards from their hand face down. @@ -207,4 +217,8 @@ public abstract class GameCommanderImpl extends GameImpl { this.checkCommanderDamage = checkCommanderDamage; } + public void addCommander(Card card, Player player) { + player.addCommanderId(card.getId()); + } + } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index a2d57f858c..5994ff0ca2 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -2871,7 +2871,7 @@ public abstract class GameImpl implements Game, Serializable { // as commander (only commander games, look at init code in GameCommanderImpl) if (this instanceof GameCommanderImpl) { for (Card card : command) { - player.addCommanderId(card.getId()); + ((GameCommanderImpl) this).addCommander(card, player); // no needs in initCommander call -- it's uses on game startup (init) } } else if (!command.isEmpty()) { diff --git a/Mage/src/main/java/mage/game/GameTinyLeadersImpl.java b/Mage/src/main/java/mage/game/GameTinyLeadersImpl.java index f89c542c84..83f17e0126 100644 --- a/Mage/src/main/java/mage/game/GameTinyLeadersImpl.java +++ b/Mage/src/main/java/mage/game/GameTinyLeadersImpl.java @@ -62,7 +62,7 @@ public abstract class GameTinyLeadersImpl extends GameImpl { ability.addEffect(new CommanderCostModification(commander.getId())); // Commander rule #4 was removed Jan. 18, 2016 // ability.addEffect(new CommanderManaReplacementEffect(player.getId(), CardUtil.getColorIdentity(commander))); - CommanderInfoWatcher watcher = new CommanderInfoWatcher(commander.getId(), false); + CommanderInfoWatcher watcher = new CommanderInfoWatcher("Commander", commander.getId(), false); getState().addWatcher(watcher); watcher.addCardInfoToCommander(this); this.getState().addAbility(ability, null); diff --git a/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java b/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java index d72635b400..0cfc776fc3 100644 --- a/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java @@ -26,17 +26,20 @@ public class CommanderInfoWatcher extends Watcher { private final Map damageToPlayer = new HashMap<>(); private final boolean checkCommanderDamage; + private final String commanderTypeName; - public CommanderInfoWatcher(UUID commander, boolean checkCommanderDamage) { + public CommanderInfoWatcher(String commanderTypeName, UUID commander, boolean checkCommanderDamage) { super(WatcherScope.CARD); this.sourceId = commander; this.checkCommanderDamage = checkCommanderDamage; + this.commanderTypeName = commanderTypeName; } public CommanderInfoWatcher(final CommanderInfoWatcher watcher) { super(watcher); this.damageToPlayer.putAll(watcher.damageToPlayer); this.checkCommanderDamage = watcher.checkCommanderDamage; + this.commanderTypeName = watcher.commanderTypeName; } @Override @@ -78,7 +81,7 @@ public class CommanderInfoWatcher extends Watcher { } if (object != null) { StringBuilder sb = new StringBuilder(); - sb.append("Commander"); + sb.append("" + commanderTypeName + ""); CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); int playsCount = watcher.getPlaysCount(sourceId); if (playsCount > 0) { @@ -89,9 +92,9 @@ public class CommanderInfoWatcher extends Watcher { if (checkCommanderDamage) { for (Map.Entry entry : damageToPlayer.entrySet()) { Player damagedPlayer = game.getPlayer(entry.getKey()); - sb.append("Commander did ").append(entry.getValue()).append(" combat damage to player ").append(damagedPlayer.getLogName()).append('.'); + sb.append("" + commanderTypeName + " did ").append(entry.getValue()).append(" combat damage to player ").append(damagedPlayer.getLogName()).append('.'); this.addInfo(object, "Commander" + entry.getKey(), - "Commander did " + entry.getValue() + " combat damage to player " + damagedPlayer.getLogName() + '.', game); + "" + commanderTypeName + " did " + entry.getValue() + " combat damage to player " + damagedPlayer.getLogName() + '.', game); } } }