Commanders improves:

* [KHM] fixed that some effects can't find mdf commanders on battlefield (example: Fierce Guardianship, #7504);
* Oathbreaker: fixed that some cards that refer to commander can affects signature spells too;
This commit is contained in:
Oleg Agafonov 2021-02-05 17:19:30 +04:00
parent 62cad8e850
commit dc0a29007c
34 changed files with 293 additions and 199 deletions

View file

@ -117,12 +117,12 @@ public class OathbreakerFreeForAll extends GameCommanderImpl {
} }
@Override @Override
public Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType) { public Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType, boolean returnAllCardParts) {
Set<UUID> res = new HashSet<>(); Set<UUID> res = new HashSet<>();
if (player != null) { if (player != null) {
Set<UUID> commanders = this.playerOathbreakers.getOrDefault(player.getId(), new HashSet<>()); Set<UUID> commanders = this.playerOathbreakers.getOrDefault(player.getId(), new HashSet<>());
Set<UUID> spells = this.playerSignatureSpells.getOrDefault(player.getId(), new HashSet<>()); Set<UUID> spells = this.playerSignatureSpells.getOrDefault(player.getId(), new HashSet<>());
for (UUID commanderId : super.getCommandersIds(player, commanderCardType)) { for (UUID commanderId : super.getCommandersIds(player, commanderCardType, returnAllCardParts)) {
switch (commanderCardType) { switch (commanderCardType) {
case ANY: case ANY:
res.add(commanderId); res.add(commanderId);
@ -133,6 +133,7 @@ public class OathbreakerFreeForAll extends GameCommanderImpl {
} }
break; break;
case SIGNATURE_SPELL: case SIGNATURE_SPELL:
// TODO: doesn't filter mdf cards with different sides (creature + spell)
if (spells.contains(commanderId)) { if (spells.contains(commanderId)) {
res.add(commanderId); res.add(commanderId);
} }
@ -142,6 +143,6 @@ public class OathbreakerFreeForAll extends GameCommanderImpl {
} }
} }
} }
return res; return super.filterCommandersBySearchZone(res, returnAllCardParts);
} }
} }

View file

@ -67,7 +67,7 @@ enum CaptainVargusWrathValue implements DynamicValue {
} }
CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class);
return watcher == null ? 0 : game return watcher == null ? 0 : game
.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER) .getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, false)
.stream() .stream()
.mapToInt(watcher::getPlaysCount) .mapToInt(watcher::getPlaysCount)
.sum(); .sum();

View file

@ -66,13 +66,7 @@ class CommandBeaconEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
List<Card> commandersInCommandZone = new ArrayList<>(1); List<Card> commandersInCommandZone = new ArrayList<>(game.getCommanderCardsFromCommandZone(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER));
for (UUID commanderId : game.getCommandersIds(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER)) {
Card commander = game.getCard(commanderId);
if (commander != null && game.getState().getZone(commander.getId()) == Zone.COMMAND) {
commandersInCommandZone.add(commander);
}
}
if (commandersInCommandZone.size() == 1) { if (commandersInCommandZone.size() == 1) {
controller.moveCards(commandersInCommandZone.get(0), Zone.HAND, source, game); controller.moveCards(commandersInCommandZone.get(0), Zone.HAND, source, game);
} else if (commandersInCommandZone.size() == 2) { } else if (commandersInCommandZone.size() == 2) {

View file

@ -52,7 +52,7 @@ enum CommandersInsigniaValue implements DynamicValue {
return 0; return 0;
} }
return game return game
.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER) .getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, false)
.stream() .stream()
.mapToInt(watcher::getPlaysCount) .mapToInt(watcher::getPlaysCount)
.sum(); .sum();

View file

@ -115,7 +115,7 @@ class CommandersPlateEffect extends ContinuousEffectImpl {
permanent = game.getPermanentOrLKIBattlefield(equipment.getAttachedTo()); permanent = game.getPermanentOrLKIBattlefield(equipment.getAttachedTo());
} }
} }
Set<UUID> commanders = game.getCommandersIds(player); Set<UUID> commanders = game.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, false);
if (commanders.isEmpty()) { if (commanders.isEmpty()) {
return false; return false;
} }

View file

@ -1,16 +1,17 @@
package mage.cards.g; package mage.cards.g;
import mage.ApprovingObject;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility;
import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCost;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.TrampleAbility; import mage.abilities.keyword.TrampleAbility;
import mage.cards.*; import mage.cards.Card;
import mage.constants.CardType; import mage.cards.CardImpl;
import mage.constants.Outcome; import mage.cards.CardSetInfo;
import mage.constants.SubType; import mage.cards.CardsImpl;
import mage.constants.Zone; import mage.constants.*;
import mage.filter.FilterCard; import mage.filter.FilterCard;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
@ -18,10 +19,8 @@ import mage.target.TargetCard;
import mage.util.ManaUtil; import mage.util.ManaUtil;
import mage.watchers.common.CommanderPlaysCountWatcher; import mage.watchers.common.CommanderPlaysCountWatcher;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import mage.ApprovingObject;
/** /**
* @author spjspj, JayDi85 * @author spjspj, JayDi85
@ -69,36 +68,31 @@ class GeodeGolemEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
UUID selectedCommanderId = null; Card selectedCommander = null;
Set<UUID> possibleCommanders = new HashSet<>();
for (UUID id : game.getCommandersIds(controller)) {
if (game.getState().getZone(id) == Zone.COMMAND) {
possibleCommanders.add(id);
}
}
if (possibleCommanders.isEmpty()) { Set<Card> commandersInCommandZone = game.getCommanderCardsFromCommandZone(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER);
if (commandersInCommandZone.isEmpty()) {
return false; return false;
} }
// select from commanders // select from commanders
if (possibleCommanders.size() == 1) { if (commandersInCommandZone.size() == 1) {
selectedCommanderId = possibleCommanders.iterator().next(); selectedCommander = commandersInCommandZone.stream().findFirst().get();
} else { } else {
TargetCard target = new TargetCard(Zone.COMMAND, new FilterCard( TargetCard target = new TargetCard(Zone.COMMAND, new FilterCard("commander to cast without mana cost"));
"commander to cast without mana cost"));
Cards cards = new CardsImpl(possibleCommanders);
target.setNotTarget(true); target.setNotTarget(true);
if (controller.canRespond() if (controller.canRespond()
&& controller.choose(Outcome.PlayForFree, cards, target, game)) { && controller.choose(Outcome.PlayForFree, new CardsImpl(commandersInCommandZone), target, game)) {
if (target.getFirstTarget() != null) { if (target.getFirstTarget() != null) {
selectedCommanderId = target.getFirstTarget(); selectedCommander = commandersInCommandZone.stream()
.filter(c -> c.getId().equals(target.getFirstTarget()))
.findFirst()
.orElse(null);
} }
} }
} }
Card commander = game.getCard(selectedCommanderId); if (selectedCommander == null) {
if (commander == null) {
return false; return false;
} }
@ -106,7 +100,7 @@ class GeodeGolemEffect extends OneShotEffect {
// TODO: this is broken with the commander cost reduction effect // TODO: this is broken with the commander cost reduction effect
ManaCost cost = null; ManaCost cost = null;
CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class);
int castCount = watcher.getPlaysCount(commander.getId()); int castCount = watcher.getPlaysCount(selectedCommander.getId());
if (castCount > 0) { if (castCount > 0) {
cost = ManaUtil.createManaCost(castCount * 2, false); cost = ManaUtil.createManaCost(castCount * 2, false);
} }
@ -114,14 +108,14 @@ class GeodeGolemEffect extends OneShotEffect {
// CAST: as spell or as land // CAST: as spell or as land
if (cost == null if (cost == null
|| cost.pay(source, game, source, controller.getId(), false, null)) { || cost.pay(source, game, source, controller.getId(), false, null)) {
if (commander.getSpellAbility() != null) { if (selectedCommander.getSpellAbility() != null) { // TODO: can be broken with mdf cards (one side creature, one side land)?
game.getState().setValue("PlayFromNotOwnHandZone" + commander.getId(), Boolean.TRUE); game.getState().setValue("PlayFromNotOwnHandZone" + selectedCommander.getId(), Boolean.TRUE);
Boolean commanderWasCast = controller.cast(controller.chooseAbilityForCast(commander, game, true), Boolean commanderWasCast = controller.cast(controller.chooseAbilityForCast(selectedCommander, game, true),
game, true, new ApprovingObject(source, game)); game, true, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + commander.getId(), null); game.getState().setValue("PlayFromNotOwnHandZone" + selectedCommander.getId(), null);
return commanderWasCast; return commanderWasCast;
} else { } else {
return controller.playLand(commander, game, true); return controller.playLand(selectedCommander, game, true);
} }
} }
} }

View file

@ -88,7 +88,7 @@ enum JeskaThriceRebornValue implements DynamicValue {
} }
CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class);
return watcher == null ? 0 : game return watcher == null ? 0 : game
.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER) .getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, false)
.stream() .stream()
.mapToInt(watcher::getPlaysCount) .mapToInt(watcher::getPlaysCount)
.sum(); .sum();

View file

@ -14,7 +14,6 @@ import mage.game.Game;
import mage.game.GameImpl; import mage.game.GameImpl;
import mage.game.command.Commander; import mage.game.command.Commander;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentImpl; import mage.game.permanent.PermanentImpl;
import mage.players.Player; import mage.players.Player;
@ -109,7 +108,7 @@ class KarnLiberatedEffect extends OneShotEffect {
if (card.isOwnedBy(player.getId()) && !card.isCopy() // no copies if (card.isOwnedBy(player.getId()) && !card.isCopy() // no copies
&& !player.getSideboard().contains(card.getId()) && !player.getSideboard().contains(card.getId())
&& !cards.contains(card)) { // not the exiled cards && !cards.contains(card)) { // not the exiled cards
if (game.getCommandersIds(player).contains(card.getId())) { if (game.getCommandersIds(player, CommanderCardType.ANY, false).contains(card.getId())) {
game.addCommander(new Commander(card)); // TODO: check restart and init game.addCommander(new Commander(card)); // TODO: check restart and init
// no needs in initCommander call -- it's used on game startup (init) // no needs in initCommander call -- it's used on game startup (init)
game.setZone(card.getId(), Zone.COMMAND); game.setZone(card.getId(), Zone.COMMAND);

View file

@ -92,7 +92,8 @@ class MythUnboundCostReductionEffect extends CostModificationEffectImpl {
if (abilityToModify instanceof SpellAbility || abilityToModify instanceof PlayLandAbility) { if (abilityToModify instanceof SpellAbility || abilityToModify instanceof PlayLandAbility) {
if (abilityToModify.isControlledBy(source.getControllerId())) { if (abilityToModify.isControlledBy(source.getControllerId())) {
return game.getCommandersIds(player).contains(abilityToModify.getSourceId()); // must check all card parts (example: mdf commander)
return game.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, true).contains(abilityToModify.getSourceId());
} }
} }
return false; return false;

View file

@ -17,10 +17,9 @@ import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
/** /**
* @author TheElk801 * @author TheElk801
@ -68,13 +67,8 @@ class NetherbornAltarEffect extends OneShotEffect {
if (controller == null) { if (controller == null) {
return false; return false;
} }
List<Card> commandersInCommandZone = game
.getCommandersIds(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER) List<Card> commandersInCommandZone = new ArrayList<>(game.getCommanderCardsFromCommandZone(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER));
.stream()
.map(game::getCard)
.filter(Objects::nonNull)
.filter(commander -> game.getState().getZone(commander.getId()) == Zone.COMMAND)
.collect(Collectors.toList());
if (commandersInCommandZone.size() == 1) { if (commandersInCommandZone.size() == 1) {
controller.moveCards(commandersInCommandZone.get(0), Zone.HAND, source, game); controller.moveCards(commandersInCommandZone.get(0), Zone.HAND, source, game);
} else if (commandersInCommandZone.size() == 2) { } else if (commandersInCommandZone.size() == 2) {

View file

@ -58,7 +58,7 @@ public final class OpalPalace extends CardImpl {
class OpalPalaceWatcher extends Watcher { class OpalPalaceWatcher extends Watcher {
private List<UUID> commanderId = new ArrayList<>(); private final List<UUID> commanderPartsId = new ArrayList<>();
private final String originalId; private final String originalId;
public OpalPalaceWatcher(String originalId) { public OpalPalaceWatcher(String originalId) {
@ -66,8 +66,8 @@ class OpalPalaceWatcher extends Watcher {
this.originalId = originalId; this.originalId = originalId;
} }
public boolean manaUsedToCastCommander(UUID id){ public boolean manaUsedToCastCommanderPart(UUID id) {
return commanderId.contains(id); return commanderPartsId.contains(id);
} }
@Override @Override
@ -81,8 +81,9 @@ class OpalPalaceWatcher extends Watcher {
for (UUID playerId : game.getPlayerList()) { for (UUID playerId : game.getPlayerList()) {
Player player = game.getPlayer(playerId); Player player = game.getPlayer(playerId);
if (player != null) { if (player != null) {
if (game.getCommandersIds(player).contains(card.getId())) { // need check all card parts (example: mdf cards)
commanderId.add(card.getId()); if (game.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, true).contains(card.getId())) {
commanderPartsId.add(card.getId());
break; break;
} }
} }
@ -96,7 +97,7 @@ class OpalPalaceWatcher extends Watcher {
@Override @Override
public void reset() { public void reset() {
super.reset(); super.reset();
commanderId.clear(); commanderPartsId.clear();
} }
} }
@ -119,7 +120,7 @@ class OpalPalaceEntersBattlefieldEffect extends ReplacementEffectImpl {
@Override @Override
public boolean applies(GameEvent event, Ability source, Game game) { public boolean applies(GameEvent event, Ability source, Game game) {
OpalPalaceWatcher watcher = game.getState().getWatcher(OpalPalaceWatcher.class, source.getSourceId()); OpalPalaceWatcher watcher = game.getState().getWatcher(OpalPalaceWatcher.class, source.getSourceId());
return watcher != null && watcher.manaUsedToCastCommander(event.getTargetId()); return watcher != null && watcher.manaUsedToCastCommanderPart(event.getTargetId());
} }
@Override @Override

View file

@ -10,6 +10,7 @@ import mage.cards.Card;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.CommanderCardType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
@ -90,14 +91,9 @@ class PathOfAncestryTriggeredAbility extends DelayedTriggeredAbility {
if (player == null) { if (player == null) {
return false; return false;
} }
for (UUID commanderId : game.getCommandersIds(player)) {
Card commander = game.getPermanent(commanderId); // share creature type with commander
if (commander == null) { for (Card commander : game.getCommanderCardsFromAnyZones(player, CommanderCardType.COMMANDER_OR_OATHBREAKER)) {
commander = game.getCard(commanderId);
}
if (commander == null) {
continue;
}
if (spell.getCard().shareCreatureTypes(game, commander)) { if (spell.getCard().shareCreatureTypes(game, commander)) {
return true; return true;
} }

View file

@ -76,15 +76,7 @@ class RoadOfReturnEffect extends OneShotEffect {
if (controller == null) { if (controller == null) {
return false; return false;
} }
List<Card> commandersInCommandZone = new ArrayList<>(1); List<Card> commandersInCommandZone = new ArrayList<>(game.getCommanderCardsFromCommandZone(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER));
game.getCommandersIds(
controller, CommanderCardType.COMMANDER_OR_OATHBREAKER
).stream().forEach(commanderId -> {
Card commander = game.getCard(commanderId);
if (commander != null && game.getState().getZone(commander.getId()) == Zone.COMMAND) {
commandersInCommandZone.add(commander);
}
});
if (commandersInCommandZone.size() == 1) { if (commandersInCommandZone.size() == 1) {
controller.moveCards(commandersInCommandZone.get(0), Zone.HAND, source, game); controller.moveCards(commandersInCommandZone.get(0), Zone.HAND, source, game);
} else if (commandersInCommandZone.size() == 2) { } else if (commandersInCommandZone.size() == 2) {

View file

@ -14,6 +14,7 @@ import mage.constants.Zone;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.players.Player;
import java.util.UUID; import java.util.UUID;
@ -62,9 +63,17 @@ class SkyfirePhoenixTriggeredAbility extends SpellCastControllerTriggeredAbility
@Override @Override
public boolean checkTrigger(GameEvent event, Game game) { public boolean checkTrigger(GameEvent event, Game game) {
return super.checkTrigger(event, game) && game.getCommandersIds( if (!super.checkTrigger(event, game)) {
game.getPlayer(getControllerId()), CommanderCardType.COMMANDER_OR_OATHBREAKER return false;
).contains(event.getSourceId()); }
Player controller = game.getPlayer(getControllerId());
if (controller == null) {
return false;
}
// must check all parts (example: cast one from from mdf/split card)
return game.getCommandersIds(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER, true).contains(event.getSourceId());
} }
@Override @Override

View file

@ -98,8 +98,8 @@ class TeferiMageOfZhalfirAddFlashEffect extends ContinuousEffectImpl {
game.getState().addOtherAbility(card, FlashAbility.getInstance()); game.getState().addOtherAbility(card, FlashAbility.getInstance());
} }
} }
// commander in command zone // cards in command zone
game.getCommanderCardsFromCommandZone(controller).stream() game.getCommanderCardsFromCommandZone(controller, CommanderCardType.ANY).stream()
.filter(MageObject::isCreature) .filter(MageObject::isCreature)
.forEach(card -> { .forEach(card -> {
game.getState().addOtherAbility(card, FlashAbility.getInstance()); game.getState().addOtherAbility(card, FlashAbility.getInstance());

View file

@ -8,6 +8,7 @@ import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.continuous.GainControlTargetEffect; import mage.abilities.effects.common.continuous.GainControlTargetEffect;
import mage.abilities.keyword.PartnerAbility; import mage.abilities.keyword.PartnerAbility;
import mage.cards.Card;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.cards.CardsImpl; import mage.cards.CardsImpl;
@ -23,11 +24,10 @@ import mage.players.Player;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import java.util.Collection; import java.util.HashSet;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import static mage.constants.Outcome.Benefit; import static mage.constants.Outcome.Benefit;
@ -110,10 +110,12 @@ class TeveshSzatDoomOfFoolsSacrificeEffect extends OneShotEffect {
if (permanent == null) { if (permanent == null) {
return false; return false;
} }
boolean isCommander = game.getCommandersIds(
game.getPlayer(permanent.getControllerId()), // must check all card parts (example: mdf commander)
CommanderCardType.COMMANDER_OR_OATHBREAKER Player permanentController = game.getPlayer(permanent.getControllerId());
).contains(permanent.getId()); boolean isCommander = permanentController != null
&& game.getCommandersIds(permanentController, CommanderCardType.COMMANDER_OR_OATHBREAKER, true).contains(permanent.getId());
if (!permanent.sacrifice(source, game)) { if (!permanent.sacrifice(source, game)) {
return false; return false;
} }
@ -154,6 +156,8 @@ class TeveshSzatDoomOfFoolsCommanderEffect extends OneShotEffect {
if (controller == null) { if (controller == null) {
return false; return false;
} }
// gain control of all commanders
for (Permanent permanent : game.getBattlefield().getActivePermanents( for (Permanent permanent : game.getBattlefield().getActivePermanents(
filter, source.getControllerId(), source.getSourceId(), game filter, source.getControllerId(), source.getSourceId(), game
)) { )) {
@ -161,16 +165,17 @@ class TeveshSzatDoomOfFoolsCommanderEffect extends OneShotEffect {
Duration.Custom, true Duration.Custom, true
).setTargetPointer(new FixedTarget(permanent, game)), source); ).setTargetPointer(new FixedTarget(permanent, game)), source);
} }
Set<UUID> commanders = game
.getPlayerList() // put all commanders to battlefield under control
.stream() // TODO: doesn't support range of influence (e.g. take control of all commanders)
Set<Card> commandersToPut = new HashSet<>();
game.getPlayerList().stream()
.map(game::getPlayer) .map(game::getPlayer)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.map(player -> game.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER)) .forEach(player -> {
.flatMap(Collection::stream) commandersToPut.addAll(game.getCommanderCardsFromCommandZone(player, CommanderCardType.COMMANDER_OR_OATHBREAKER));
.filter(uuid -> game.getState().getZone(uuid) == Zone.COMMAND) });
.collect(Collectors.toSet()); controller.moveCards(new CardsImpl(commandersToPut), Zone.BATTLEFIELD, source, game);
controller.moveCards(new CardsImpl(commanders), Zone.BATTLEFIELD, source, game);
return true; return true;
} }
} }

View file

@ -14,6 +14,7 @@ import mage.cards.Card;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.CommanderCardType;
import mage.filter.FilterMana; import mage.filter.FilterMana;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
@ -64,7 +65,7 @@ enum WarRoomValue implements DynamicValue {
ObjectColor color = new ObjectColor(); ObjectColor color = new ObjectColor();
// if no commander then cost can't be paid // if no commander then cost can't be paid
boolean hasCommander = false; boolean hasCommander = false;
for (UUID commanderId : game.getCommandersIds(controller)) { for (UUID commanderId : game.getCommandersIds(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER, false)) {
Card commander = game.getCard(commanderId); Card commander = game.getCard(commanderId);
if (commander == null) { if (commander == null) {
continue; continue;

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.continuous;
import mage.abilities.dynamicvalue.common.CommanderCastCountValue; import mage.abilities.dynamicvalue.common.CommanderCastCountValue;
import mage.abilities.keyword.FirstStrikeAbility; import mage.abilities.keyword.FirstStrikeAbility;
import mage.cards.AdventureCard; import mage.cards.AdventureCard;
import mage.constants.CommanderCardType;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import org.junit.Assert; import org.junit.Assert;
@ -448,7 +449,7 @@ public class CommandersCastTest extends CardTestCommander4Players {
// can't cast adventure spell for {G} + {2} + {2} // can't cast adventure spell for {G} + {2} + {2}
// can't cast creature spell for {G}{G} + {2} + {2} // can't cast creature spell for {G}{G} + {2} + {2}
runCode("check commander tax 2x", 9, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { runCode("check commander tax 2x", 9, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
AdventureCard card = (AdventureCard) game.getCommanderCardsFromCommandZone(player).stream().findFirst().get(); AdventureCard card = (AdventureCard) game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY).stream().findFirst().get();
Assert.assertEquals(2, CommanderCastCountValue.instance.calculate(game, card.getSpellAbility(), null)); Assert.assertEquals(2, CommanderCastCountValue.instance.calculate(game, card.getSpellAbility(), null));
Assert.assertEquals(2, CommanderCastCountValue.instance.calculate(game, card.getSpellCard().getSpellAbility(), null)); Assert.assertEquals(2, CommanderCastCountValue.instance.calculate(game, card.getSpellCard().getSpellAbility(), null));
}); });
@ -474,7 +475,7 @@ public class CommandersCastTest extends CardTestCommander4Players {
checkPermanentCount("after last cast", 13, PhaseStep.PRECOMBAT_MAIN, playerA, "Curious Pair", 1); checkPermanentCount("after last cast", 13, PhaseStep.PRECOMBAT_MAIN, playerA, "Curious Pair", 1);
checkPermanentTapped("after last cast", 13, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest", true, 2 + 2 + 2); checkPermanentTapped("after last cast", 13, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest", true, 2 + 2 + 2);
runCode("check commander tax 3x", 13, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { runCode("check commander tax 3x", 13, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
AdventureCard card = (AdventureCard) game.getCard(game.getCommandersIds(player).stream().findFirst().get()); AdventureCard card = (AdventureCard) game.getCard(game.getCommandersIds(player, CommanderCardType.ANY, false).stream().findFirst().get());
Assert.assertEquals(3, CommanderCastCountValue.instance.calculate(game, card.getSpellAbility(), null)); Assert.assertEquals(3, CommanderCastCountValue.instance.calculate(game, card.getSpellAbility(), null));
Assert.assertEquals(3, CommanderCastCountValue.instance.calculate(game, card.getSpellCard().getSpellAbility(), null)); Assert.assertEquals(3, CommanderCastCountValue.instance.calculate(game, card.getSpellCard().getSpellAbility(), null));
}); });
@ -486,7 +487,7 @@ public class CommandersCastTest extends CardTestCommander4Players {
} }
@Test @Test
public void test_ModalDoubleFacesCard() { public void test_ModalDoubleFacesCard_1() {
// Player order: A -> D -> C -> B // Player order: A -> D -> C -> B
// use case: // use case:
@ -540,4 +541,41 @@ public class CommandersCastTest extends CardTestCommander4Players {
execute(); execute();
assertAllCommandsUsed(); assertAllCommandsUsed();
} }
@Test
public void test_ModalDoubleFacesCard_CanReturnAfterKillAndCommanderControlCondition() {
// Player order: A -> D -> C -> B
// Cosima, God of the Voyage, {2}{U}, creature, 2/4
// The Omenkeel, {1}{U}, artifact, vehicle
addCard(Zone.COMMAND, playerA, "Cosima, God of the Voyage", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
//
addCard(Zone.HAND, playerA, "Lightning Bolt", 2);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
//
// If you control a commander, you may cast this spell without paying its mana cost.
// Counter target noncreature spell.
addCard(Zone.HAND, playerA, "Fierce Guardianship", 1); // {2}{U}
// prepare commander on battlefield
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cosima, God of the Voyage");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cosima, God of the Voyage", 1);
// kill and return to command zone
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Cosima, God of the Voyage");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Cosima, God of the Voyage");
// check what commander control condition works with mdf parts
checkStackSize("must have 2 bolts on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2);
checkPlayableAbility("must see commander on battle for free cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Fierce Guardianship", true);
//
setChoice(playerA, "Yes"); // return to command zone after kill
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
}
} }

View file

@ -943,7 +943,7 @@ public class TestPlayer implements Player {
// show command // show command
if (params[0].equals(SHOW_COMMAND_COMMAND) && params.length == 1) { if (params[0].equals(SHOW_COMMAND_COMMAND) && params.length == 1) {
printStart(action.getActionName()); printStart(action.getActionName());
CardsImpl cards = new CardsImpl(game.getCommandersIds(computerPlayer)); CardsImpl cards = new CardsImpl(game.getCommandersIds(computerPlayer, CommanderCardType.ANY, false));
printCards(cards.getCards(game)); printCards(cards.getCards(game));
printEnd(); printEnd();
actions.remove(action); actions.remove(action);
@ -1421,7 +1421,7 @@ public class TestPlayer implements Player {
private void assertCommandCardCount(PlayerAction action, Game game, Player player, String cardName, int count) { private void assertCommandCardCount(PlayerAction action, Game game, Player player, String cardName, int count) {
int realCount = 0; int realCount = 0;
for (UUID cardId : game.getCommandersIds(player)) { for (UUID cardId : game.getCommandersIds(player, CommanderCardType.ANY, false)) {
Card card = game.getCard(cardId); Card card = game.getCard(cardId);
if (hasObjectTargetNameOrAlias(card, cardName) && Zone.COMMAND.equals(game.getState().getZone(cardId))) { if (hasObjectTargetNameOrAlias(card, cardName) && Zone.COMMAND.equals(game.getState().getZone(cardId))) {
realCount++; realCount++;
@ -1430,7 +1430,7 @@ public class TestPlayer implements Player {
if (realCount != count) { if (realCount != count) {
printStart("Cards in command zone from " + player.getName()); printStart("Cards in command zone from " + player.getName());
printCards(game.getCommanderCardsFromCommandZone(player)); printCards(game.getCommanderCardsFromCommandZone(player, CommanderCardType.COMMANDER_OR_OATHBREAKER));
printEnd(); printEnd();
Assert.fail(action.getActionName() + " - must have " + count + " cards with name " + cardName + ", but found " + realCount); Assert.fail(action.getActionName() + " - must have " + count + " cards with name " + cardName + ", but found " + realCount);
} }

View file

@ -2,12 +2,7 @@ package mage.abilities.condition.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.constants.CommanderCardType;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.UUID;
/** /**
* Checks if the player has its commander in play and controls it * Checks if the player has its commander in play and controls it
@ -20,16 +15,7 @@ public enum CommanderInPlayCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); return ControlACommanderCondition.instance.apply(game, source);
if (controller != null) {
for (UUID commanderId : game.getCommandersIds(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER)) {
Permanent commander = game.getPermanent(commanderId);
if (commander != null && commander.isControlledBy(source.getControllerId())) {
return true;
}
}
}
return false;
} }
@Override @Override

View file

@ -21,7 +21,7 @@ public enum ControlACommanderCondition implements Condition {
.stream() .stream()
.map(game::getPlayer) .map(game::getPlayer)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.map(player -> game.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER)) .map(player -> game.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, true)) // must search all card parts (example: mdf commander on battlefield)
.flatMap(Collection::stream) .flatMap(Collection::stream)
.map(game::getPermanent) .map(game::getPermanent)
.filter(Objects::nonNull) .filter(Objects::nonNull)

View file

@ -2,10 +2,7 @@ package mage.abilities.effects;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.Duration; import mage.constants.*;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.filter.FilterObject; import mage.filter.FilterObject;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
@ -62,7 +59,7 @@ public class GainAbilitySpellsEffect extends ContinuousEffectImpl {
} }
// workaround to gain cost reduction abilities to commanders before cast (make it playable) // workaround to gain cost reduction abilities to commanders before cast (make it playable)
game.getCommanderCardsFromCommandZone(player).stream() game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY).stream()
.filter(card -> filter.match(card, game)) .filter(card -> filter.match(card, game))
.forEach(card -> { .forEach(card -> {
game.getState().addOtherAbility(card, ability); game.getState().addOtherAbility(card, ability);

View file

@ -3,10 +3,7 @@ package mage.abilities.effects.common.continuous;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.ContinuousEffectImpl;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.Duration; import mage.constants.*;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.filter.FilterCard; import mage.filter.FilterCard;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
@ -68,7 +65,7 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl {
} }
// workaround to gain cost reduction abilities to commanders before cast (make it playable) // workaround to gain cost reduction abilities to commanders before cast (make it playable)
game.getCommanderCardsFromCommandZone(player).stream() game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY).stream()
.filter(card -> filter.match(card, game)) .filter(card -> filter.match(card, game))
.forEach(card -> { .forEach(card -> {
game.getState().addOtherAbility(card, ability); game.getState().addOtherAbility(card, ability);

View file

@ -10,7 +10,6 @@ import mage.constants.Outcome;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject; import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
@ -89,7 +88,7 @@ class CommanderStormEffect extends OneShotEffect {
return false; return false;
} }
int stormCount = game int stormCount = game
.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER) .getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, false)
.stream() .stream()
.mapToInt(watcher::getPlaysCount) .mapToInt(watcher::getPlaysCount)
.sum(); .sum();

View file

@ -1,6 +1,5 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.ActivatedAbilityImpl; import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
@ -10,6 +9,7 @@ import mage.abilities.effects.OneShotEffect;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.Cards; import mage.cards.Cards;
import mage.cards.CardsImpl; import mage.cards.CardsImpl;
import mage.constants.CommanderCardType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterControlledCreaturePermanent;
@ -20,6 +20,9 @@ import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetControlledPermanent; import mage.target.common.TargetControlledPermanent;
import mage.util.CardUtil;
import java.util.UUID;
/** /**
* 702.47. Ninjutsu * 702.47. Ninjutsu
@ -194,16 +197,29 @@ class RevealNinjutsuCardCost extends CostImpl {
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Player player = game.getPlayer(controllerId); Player player = game.getPlayer(controllerId);
// used from hand
Card card = player.getHand().get(ability.getSourceId(), game); Card card = player.getHand().get(ability.getSourceId(), game);
if (card == null && commander
&& game.getCommandersIds(player).contains(ability.getSourceId())) { // rules:
// Commander ninjutsu is a variant of ninjutsu that can be activated from the command zone as
// well as from your hand. Just as with regular ninjutsu, the Ninja enters attacking the player
// or planeswalker that the returned creature was attacking.
// used from command zone
// must search all card sides for ability (example: mdf card with Ninjutsu in command zone)
if (card == null
&& commander
&& game.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, true).contains(ability.getSourceId())) {
for (CommandObject coj : game.getState().getCommand()) { for (CommandObject coj : game.getState().getCommand()) {
if (coj != null && coj.getId().equals(ability.getSourceId())) { if (coj != null && coj.getId().equals(ability.getSourceId())) {
card = game.getCard(ability.getSourceId()); if (CardUtil.getObjectParts(coj).contains(ability.getSourceId())) {
break; card = game.getCard(CardUtil.getMainCardId(game, ability.getSourceId()));
break;
}
} }
} }
} }
if (card != null) { if (card != null) {
Cards cards = new CardsImpl(card); Cards cards = new CardsImpl(card);
player.revealCards("Ninjutsu", cards, game); player.revealCards("Ninjutsu", cards, game);

View file

@ -9,6 +9,7 @@ import mage.cards.Card;
import mage.choices.Choice; import mage.choices.Choice;
import mage.choices.ChoiceImpl; import mage.choices.ChoiceImpl;
import mage.constants.ColoredManaSymbol; import mage.constants.ColoredManaSymbol;
import mage.constants.CommanderCardType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.FilterMana; import mage.filter.FilterMana;
import mage.game.Game; import mage.game.Game;
@ -71,7 +72,7 @@ class CommanderIdentityManaEffect extends ManaEffect {
} }
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
for (UUID commanderId : game.getCommandersIds(controller)) { for (UUID commanderId : game.getCommandersIds(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER, false)) {
Card commander = game.getCard(commanderId); Card commander = game.getCard(commanderId);
if (commander != null) { if (commander != null) {
FilterMana commanderMana = commander.getColorIdentity(); FilterMana commanderMana = commander.getColorIdentity();
@ -106,7 +107,7 @@ class CommanderIdentityManaEffect extends ManaEffect {
if (controller != null) { if (controller != null) {
Choice choice = new ChoiceImpl(); Choice choice = new ChoiceImpl();
choice.setMessage("Pick a mana color"); choice.setMessage("Pick a mana color");
for (UUID commanderId : game.getCommandersIds(controller)) { for (UUID commanderId : game.getCommandersIds(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER, false)) {
Card commander = game.getCard(commanderId); Card commander = game.getCard(commanderId);
if (commander != null) { if (commander != null) {
FilterMana commanderMana = commander.getColorIdentity(); FilterMana commanderMana = commander.getColorIdentity();

View file

@ -1,6 +1,12 @@
package mage.constants; package mage.constants;
/** /**
* rules:
* Cards that reference "your commander" instead reference "your Oathbreaker."
* <p>
* So in card rules text contains "commander" then you must use COMMANDER_OR_OATHBREAKER.
* If you card must look to command zone (e.g. target any card) then you must use ANY
*
* @author JayDi85 * @author JayDi85
*/ */
public enum CommanderCardType { public enum CommanderCardType {

View file

@ -507,27 +507,44 @@ public interface Game extends MageItem, Serializable {
Mulligan getMulligan(); Mulligan getMulligan();
Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType); Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType, boolean returnAllCardParts);
default Set<UUID> getCommandersIds(Player player) {
return getCommandersIds(player, CommanderCardType.ANY);
}
/** /**
* Return not played commander cards from command zone * Return not played commander cards from command zone
* Read comments for CommanderCardType for more info on commanderCardType usage
* *
* @param player * @param player
* @return * @return
*/ */
default Set<Card> getCommanderCardsFromCommandZone(Player player) { default Set<Card> getCommanderCardsFromCommandZone(Player player, CommanderCardType commanderCardType) {
// commanders in command zone aren't cards so you must call getCard instead getObject // commanders in command zone aren't cards so you must call getCard instead getObject
return getCommandersIds(player).stream() return getCommandersIds(player, commanderCardType, false).stream()
.map(this::getCard) .map(this::getCard)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(card -> Zone.COMMAND.equals(this.getState().getZone(card.getId()))) .filter(card -> Zone.COMMAND.equals(this.getState().getZone(card.getId())))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
/**
* Return commander cards from any zones (main card from command and permanent card from battlefield)
* Read comments for CommanderCardType for more info on commanderCardType usage
*
* @param player
* @param commanderCardType commander or signature spell
* @return
*/
default Set<Card> getCommanderCardsFromAnyZones(Player player, CommanderCardType commanderCardType) {
// from command zone
Set<Card> res = getCommanderCardsFromCommandZone(player, commanderCardType);
// from battlefield
this.getCommandersIds(player, commanderCardType, true).stream()
.map(this::getPermanent)
.filter(Objects::nonNull)
.forEach(res::add);
return res;
}
/** /**
* Finds is it a commander card/object (use it in conditional and other things) * Finds is it a commander card/object (use it in conditional and other things)
* *
@ -546,7 +563,7 @@ public interface Game extends MageItem, Serializable {
if (object instanceof Card) { if (object instanceof Card) {
idToCheck = ((Card) object).getMainCard().getId(); idToCheck = ((Card) object).getMainCard().getId();
} }
return idToCheck != null && this.getCommandersIds(player).contains(idToCheck); return idToCheck != null && this.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, false).contains(idToCheck);
} }
void setGameStopped(boolean gameStopped); void setGameStopped(boolean gameStopped);

View file

@ -7,10 +7,7 @@ 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.abilities.keyword.CompanionAbility;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.MultiplayerAttackOption; import mage.constants.*;
import mage.constants.PhaseStep;
import mage.constants.RangeOfInfluence;
import mage.constants.Zone;
import mage.game.mulligan.Mulligan; import mage.game.mulligan.Mulligan;
import mage.game.turn.TurnMod; import mage.game.turn.TurnMod;
import mage.players.Player; import mage.players.Player;
@ -67,7 +64,7 @@ public abstract class GameCommanderImpl extends GameImpl {
} }
// init commanders // init commanders
for (UUID commanderId : this.getCommandersIds(player)) { for (UUID commanderId : this.getCommandersIds(player, CommanderCardType.ANY, false)) {
Card commander = this.getCard(commanderId); Card commander = this.getCard(commanderId);
if (commander != null) { if (commander != null) {
initCommander(commander, player); initCommander(commander, player);
@ -193,7 +190,7 @@ public abstract class GameCommanderImpl extends GameImpl {
@Override @Override
protected boolean checkStateBasedActions() { protected boolean checkStateBasedActions() {
for (Player player : getPlayers().values()) { for (Player player : getPlayers().values()) {
for (UUID commanderId : this.getCommandersIds(player)) { for (UUID commanderId : this.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, false)) {
CommanderInfoWatcher damageWatcher = getState().getWatcher(CommanderInfoWatcher.class, commanderId); CommanderInfoWatcher damageWatcher = getState().getWatcher(CommanderInfoWatcher.class, commanderId);
if (damageWatcher == null) { if (damageWatcher == null) {
continue; continue;

View file

@ -1893,8 +1893,9 @@ public abstract class GameImpl implements Game, Serializable {
// If a commander is in a graveyard or in exile and that card was put into that zone // If a commander is in a graveyard or in exile and that card was put into that zone
// since the last time state-based actions were checked, its owner may put it into the command zone. // since the last time state-based actions were checked, its owner may put it into the command zone.
// signature spells goes to command zone all the time
for (Player player : state.getPlayers().values()) { for (Player player : state.getPlayers().values()) {
Set<UUID> commanderIds = getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER); Set<UUID> commanderIds = getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, false);
if (commanderIds.isEmpty()) { if (commanderIds.isEmpty()) {
continue; continue;
} }
@ -3419,8 +3420,28 @@ public abstract class GameImpl implements Game, Serializable {
} }
@Override @Override
public Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType) { public Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType, boolean returnAllCardParts) {
return player.getCommandersIds(); //noinspection deprecation - it's ok to use it in inner method
Set<UUID> mainCards = player.getCommandersIds();
return filterCommandersBySearchZone(mainCards, returnAllCardParts);
}
final protected Set<UUID> filterCommandersBySearchZone(Set<UUID> commanderMainCards, boolean returnAllCardParts) {
// filter by zone search (example: if you search commanders on battlefield then must see all sides of mdf cards)
Set<UUID> filteredCards = new HashSet<>();
if (returnAllCardParts) {
// need all card parts
commanderMainCards.stream()
.map(this::getCard)
.filter(Objects::nonNull)
.forEach(card -> {
filteredCards.addAll(CardUtil.getObjectParts(card));
});
} else {
filteredCards.addAll(commanderMainCards);
}
return filteredCards;
} }
@Override @Override

View file

@ -43,7 +43,6 @@ import mage.filter.predicate.permanent.PermanentIdPredicate;
import mage.game.*; import mage.game.*;
import mage.game.combat.CombatGroup; import mage.game.combat.CombatGroup;
import mage.game.command.CommandObject; import mage.game.command.CommandObject;
import mage.game.command.Commander;
import mage.game.events.*; import mage.game.events.*;
import mage.game.match.MatchPlayer; import mage.game.match.MatchPlayer;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
@ -314,7 +313,10 @@ public abstract class PlayerImpl implements Player, Serializable {
this.sideboard = player.getSideboard().copy(); this.sideboard = player.getSideboard().copy();
this.hand = player.getHand().copy(); this.hand = player.getHand().copy();
this.graveyard = player.getGraveyard().copy(); this.graveyard = player.getGraveyard().copy();
//noinspection deprecation - it's ok to use it in inner methods
this.commandersIds = new HashSet<>(player.getCommandersIds()); this.commandersIds = new HashSet<>(player.getCommandersIds());
this.abilities = player.getAbilities().copy(); this.abilities = player.getAbilities().copy();
this.counters = player.getCounters().copy(); this.counters = player.getCounters().copy();
@ -1578,7 +1580,7 @@ public abstract class PlayerImpl implements Player, Serializable {
try { try {
// collect and filter playable activated abilities // collect and filter playable activated abilities
// GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right) // GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right)
Set<UUID> needIds = getObjectParts(object); Set<UUID> needIds = CardUtil.getObjectParts(object);
// workaround to find all abilities first and filter it for one object // workaround to find all abilities first and filter it for one object
List<ActivatedAbility> allPlayable = getPlayable(game, true, zone, false); List<ActivatedAbility> allPlayable = getPlayable(game, true, zone, false);
@ -1593,40 +1595,6 @@ public abstract class PlayerImpl implements Player, Serializable {
return useable; return useable;
} }
protected Set<UUID> getObjectParts(MageObject object) {
// collect all possible object's parts (example: all sides in mdf/split cards)
Set<UUID> res = new HashSet<>();
if (object instanceof SplitCard || object instanceof SplitCardHalf) {
SplitCard mainCard = (SplitCard) ((Card) object).getMainCard();
res.add(object.getId());
res.add(mainCard.getId());
res.add(mainCard.getLeftHalfCard().getId());
res.add(mainCard.getRightHalfCard().getId());
} else if (object instanceof ModalDoubleFacesCard || object instanceof ModalDoubleFacesCardHalf) {
ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) ((Card) object).getMainCard();
res.add(object.getId());
res.add(mainCard.getId());
res.add(mainCard.getLeftHalfCard().getId());
res.add(mainCard.getRightHalfCard().getId());
} else if (object instanceof AdventureCard || object instanceof AdventureCardSpell) {
AdventureCard mainCard = (AdventureCard) ((Card) object).getMainCard();
res.add(object.getId());
res.add(mainCard.getId());
res.add(mainCard.getSpellCard().getId());
} else if (object instanceof Spell) {
// example: activate Lightning Storm's ability from the spell on the stack
res.add(object.getId());
res.add(((Spell) object).getCard().getId()); // only single side goes to the stack
} else if (object instanceof Commander) {
// commander can contains double sides
res.add(object.getId());
res.addAll(getObjectParts(((Commander) object).getSourceObject()));
} else {
res.add(object.getId());
}
return res;
}
protected LinkedHashMap<UUID, ActivatedManaAbilityImpl> getUseableManaAbilities(MageObject object, Zone zone, Game game) { protected LinkedHashMap<UUID, ActivatedManaAbilityImpl> getUseableManaAbilities(MageObject object, Zone zone, Game game) {
LinkedHashMap<UUID, ActivatedManaAbilityImpl> useable = new LinkedHashMap<>(); LinkedHashMap<UUID, ActivatedManaAbilityImpl> useable = new LinkedHashMap<>();
boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game); boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game);

View file

@ -4,10 +4,10 @@ import mage.MageItem;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.Cards; import mage.cards.Cards;
import mage.constants.CommanderCardType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.FilterCard; import mage.filter.FilterCard;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.TargetEvent; import mage.game.events.TargetEvent;
import mage.players.Player; import mage.players.Player;
@ -115,6 +115,22 @@ public class TargetCard extends TargetObject {
} }
} }
break; break;
case COMMAND:
List<Card> possibleCards = game.getCommandersIds(player, CommanderCardType.ANY, false).stream()
.map(game::getCard)
.filter(Objects::nonNull)
.filter(card -> game.getState().getZone(card.getId()).equals(Zone.COMMAND))
.filter(card -> filter.match(card, sourceId, sourceControllerId, game))
.collect(Collectors.toList());
for (Card card : possibleCards) {
if (sourceId == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, sourceId, sourceControllerId))) {
possibleTargets++;
if (possibleTargets >= this.minNumberOfTargets) {
return true;
}
}
}
break;
} }
} }
} }
@ -173,7 +189,7 @@ public class TargetCard extends TargetObject {
} }
break; break;
case COMMAND: case COMMAND:
List<Card> possibleCards = game.getCommandersIds(player).stream() List<Card> possibleCards = game.getCommandersIds(player, CommanderCardType.ANY, false).stream()
.map(game::getCard) .map(game::getCard)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(card -> game.getState().getZone(card.getId()).equals(Zone.COMMAND)) .filter(card -> game.getState().getZone(card.getId()).equals(Zone.COMMAND))

View file

@ -1177,4 +1177,50 @@ public final class CardUtil {
public static boolean checkCostWords(String text) { public static boolean checkCostWords(String text) {
return text != null && costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith); return text != null && costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith);
} }
/**
* Collect all possible object's parts (example: all sides in mdf/split cards)
* <p>
* Works with any objects, so commander object can return four ids: commander + main card + left card + right card
* If you pass Card object then it return main card + all parts
*
* @param object
* @return
*/
public static Set<UUID> getObjectParts(MageObject object) {
Set<UUID> res = new HashSet<>();
if (object == null) {
return res;
}
if (object instanceof SplitCard || object instanceof SplitCardHalf) {
SplitCard mainCard = (SplitCard) ((Card) object).getMainCard();
res.add(object.getId());
res.add(mainCard.getId());
res.add(mainCard.getLeftHalfCard().getId());
res.add(mainCard.getRightHalfCard().getId());
} else if (object instanceof ModalDoubleFacesCard || object instanceof ModalDoubleFacesCardHalf) {
ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) ((Card) object).getMainCard();
res.add(object.getId());
res.add(mainCard.getId());
res.add(mainCard.getLeftHalfCard().getId());
res.add(mainCard.getRightHalfCard().getId());
} else if (object instanceof AdventureCard || object instanceof AdventureCardSpell) {
AdventureCard mainCard = (AdventureCard) ((Card) object).getMainCard();
res.add(object.getId());
res.add(mainCard.getId());
res.add(mainCard.getSpellCard().getId());
} else if (object instanceof Spell) {
// example: activate Lightning Storm's ability from the spell on the stack
res.add(object.getId());
res.addAll(getObjectParts(((Spell) object).getCard()));
} else if (object instanceof Commander) {
// commander can contains double sides
res.add(object.getId());
res.addAll(getObjectParts(((Commander) object).getSourceObject()));
} else {
res.add(object.getId());
}
return res;
}
} }

View file

@ -1,5 +1,6 @@
package mage.watchers.common; package mage.watchers.common;
import mage.constants.CommanderCardType;
import mage.constants.WatcherScope; import mage.constants.WatcherScope;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
@ -45,11 +46,12 @@ public class CommanderPlaysCountWatcher extends Watcher {
objectId = null; objectId = null;
} }
// must calc all commanders and signature spell cause uses in commander tax
boolean isCommanderObject = game boolean isCommanderObject = game
.getPlayerList() .getPlayerList()
.stream() .stream()
.map(game::getPlayer) .map(game::getPlayer)
.map(game::getCommandersIds) .map(player -> game.getCommandersIds(player, CommanderCardType.ANY, false))
.flatMap(Collection::stream) .flatMap(Collection::stream)
.anyMatch(id -> Objects.equals(id, objectId)); .anyMatch(id -> Objects.equals(id, objectId));
if (!isCommanderObject || event.getZone() != Zone.COMMAND) { if (!isCommanderObject || event.getZone() != Zone.COMMAND) {