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
public Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType) {
public Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType, boolean returnAllCardParts) {
Set<UUID> res = new HashSet<>();
if (player != null) {
Set<UUID> commanders = this.playerOathbreakers.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) {
case ANY:
res.add(commanderId);
@ -133,6 +133,7 @@ public class OathbreakerFreeForAll extends GameCommanderImpl {
}
break;
case SIGNATURE_SPELL:
// TODO: doesn't filter mdf cards with different sides (creature + spell)
if (spells.contains(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);
return watcher == null ? 0 : game
.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER)
.getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, false)
.stream()
.mapToInt(watcher::getPlaysCount)
.sum();

View file

@ -66,13 +66,7 @@ class CommandBeaconEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
List<Card> commandersInCommandZone = new ArrayList<>(1);
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);
}
}
List<Card> commandersInCommandZone = new ArrayList<>(game.getCommanderCardsFromCommandZone(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER));
if (commandersInCommandZone.size() == 1) {
controller.moveCards(commandersInCommandZone.get(0), Zone.HAND, source, game);
} else if (commandersInCommandZone.size() == 2) {

View file

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

View file

@ -115,7 +115,7 @@ class CommandersPlateEffect extends ContinuousEffectImpl {
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()) {
return false;
}

View file

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

View file

@ -14,7 +14,6 @@ import mage.game.Game;
import mage.game.GameImpl;
import mage.game.command.Commander;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentImpl;
import mage.players.Player;
@ -109,7 +108,7 @@ class KarnLiberatedEffect extends OneShotEffect {
if (card.isOwnedBy(player.getId()) && !card.isCopy() // no copies
&& !player.getSideboard().contains(card.getId())
&& !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
// no needs in initCommander call -- it's used on game startup (init)
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.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;

View file

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

View file

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

View file

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

View file

@ -76,15 +76,7 @@ class RoadOfReturnEffect extends OneShotEffect {
if (controller == null) {
return false;
}
List<Card> commandersInCommandZone = new ArrayList<>(1);
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);
}
});
List<Card> commandersInCommandZone = new ArrayList<>(game.getCommanderCardsFromCommandZone(controller, CommanderCardType.COMMANDER_OR_OATHBREAKER));
if (commandersInCommandZone.size() == 1) {
controller.moveCards(commandersInCommandZone.get(0), Zone.HAND, source, game);
} else if (commandersInCommandZone.size() == 2) {

View file

@ -14,6 +14,7 @@ import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import java.util.UUID;
@ -62,9 +63,17 @@ class SkyfirePhoenixTriggeredAbility extends SpellCastControllerTriggeredAbility
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return super.checkTrigger(event, game) && game.getCommandersIds(
game.getPlayer(getControllerId()), CommanderCardType.COMMANDER_OR_OATHBREAKER
).contains(event.getSourceId());
if (!super.checkTrigger(event, game)) {
return false;
}
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

View file

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

View file

@ -14,6 +14,7 @@ import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.CommanderCardType;
import mage.filter.FilterMana;
import mage.game.Game;
import mage.players.Player;
@ -64,7 +65,7 @@ enum WarRoomValue implements DynamicValue {
ObjectColor color = new ObjectColor();
// if no commander then cost can't be paid
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);
if (commander == null) {
continue;

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.continuous;
import mage.abilities.dynamicvalue.common.CommanderCastCountValue;
import mage.abilities.keyword.FirstStrikeAbility;
import mage.cards.AdventureCard;
import mage.constants.CommanderCardType;
import mage.constants.PhaseStep;
import mage.constants.Zone;
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 creature spell for {G}{G} + {2} + {2}
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.getSpellCard().getSpellAbility(), null));
});
@ -474,7 +475,7 @@ public class CommandersCastTest extends CardTestCommander4Players {
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);
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.getSpellCard().getSpellAbility(), null));
});
@ -486,7 +487,7 @@ public class CommandersCastTest extends CardTestCommander4Players {
}
@Test
public void test_ModalDoubleFacesCard() {
public void test_ModalDoubleFacesCard_1() {
// Player order: A -> D -> C -> B
// use case:
@ -540,4 +541,41 @@ public class CommandersCastTest extends CardTestCommander4Players {
execute();
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
if (params[0].equals(SHOW_COMMAND_COMMAND) && params.length == 1) {
printStart(action.getActionName());
CardsImpl cards = new CardsImpl(game.getCommandersIds(computerPlayer));
CardsImpl cards = new CardsImpl(game.getCommandersIds(computerPlayer, CommanderCardType.ANY, false));
printCards(cards.getCards(game));
printEnd();
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) {
int realCount = 0;
for (UUID cardId : game.getCommandersIds(player)) {
for (UUID cardId : game.getCommandersIds(player, CommanderCardType.ANY, false)) {
Card card = game.getCard(cardId);
if (hasObjectTargetNameOrAlias(card, cardName) && Zone.COMMAND.equals(game.getState().getZone(cardId))) {
realCount++;
@ -1430,7 +1430,7 @@ public class TestPlayer implements Player {
if (realCount != count) {
printStart("Cards in command zone from " + player.getName());
printCards(game.getCommanderCardsFromCommandZone(player));
printCards(game.getCommanderCardsFromCommandZone(player, CommanderCardType.COMMANDER_OR_OATHBREAKER));
printEnd();
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.condition.Condition;
import mage.constants.CommanderCardType;
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
@ -20,16 +15,7 @@ public enum CommanderInPlayCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
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;
return ControlACommanderCondition.instance.apply(game, source);
}
@Override

View file

@ -21,7 +21,7 @@ public enum ControlACommanderCondition implements Condition {
.stream()
.map(game::getPlayer)
.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)
.map(game::getPermanent)
.filter(Objects::nonNull)

View file

@ -2,10 +2,7 @@ package mage.abilities.effects;
import mage.abilities.Ability;
import mage.cards.Card;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.constants.*;
import mage.filter.FilterObject;
import mage.game.Game;
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)
game.getCommanderCardsFromCommandZone(player).stream()
game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY).stream()
.filter(card -> filter.match(card, game))
.forEach(card -> {
game.getState().addOtherAbility(card, ability);

View file

@ -3,10 +3,7 @@ package mage.abilities.effects.common.continuous;
import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.cards.Card;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.game.Game;
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)
game.getCommanderCardsFromCommandZone(player).stream()
game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY).stream()
.filter(card -> filter.match(card, game))
.forEach(card -> {
game.getState().addOtherAbility(card, ability);

View file

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

View file

@ -1,6 +1,5 @@
package mage.abilities.keyword;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.costs.Cost;
@ -10,6 +9,7 @@ import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.CommanderCardType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.common.FilterControlledCreaturePermanent;
@ -20,6 +20,9 @@ import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetControlledPermanent;
import mage.util.CardUtil;
import java.util.UUID;
/**
* 702.47. Ninjutsu
@ -43,7 +46,7 @@ import mage.target.common.TargetControlledPermanent;
public class NinjutsuAbility extends ActivatedAbilityImpl {
private final boolean commander;
private static final FilterControlledCreaturePermanent filter =
private static final FilterControlledCreaturePermanent filter =
new FilterControlledCreaturePermanent("unblocked attacker you control");
static {
@ -150,7 +153,7 @@ class ReturnAttackerToHandTargetCost extends CostImpl {
for (UUID targetId : targets.get(0).getTargets()) {
Permanent permanent = game.getPermanent(targetId);
Player controller = game.getPlayer(controllerId);
if (permanent == null
if (permanent == null
|| controller == null) {
return false;
}
@ -194,16 +197,29 @@ class RevealNinjutsuCardCost extends CostImpl {
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Player player = game.getPlayer(controllerId);
// used from hand
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()) {
if (coj != null && coj.getId().equals(ability.getSourceId())) {
card = game.getCard(ability.getSourceId());
break;
if (CardUtil.getObjectParts(coj).contains(ability.getSourceId())) {
card = game.getCard(CardUtil.getMainCardId(game, ability.getSourceId()));
break;
}
}
}
}
if (card != null) {
Cards cards = new CardsImpl(card);
player.revealCards("Ninjutsu", cards, game);

View file

@ -9,6 +9,7 @@ import mage.cards.Card;
import mage.choices.Choice;
import mage.choices.ChoiceImpl;
import mage.constants.ColoredManaSymbol;
import mage.constants.CommanderCardType;
import mage.constants.Zone;
import mage.filter.FilterMana;
import mage.game.Game;
@ -71,7 +72,7 @@ class CommanderIdentityManaEffect extends ManaEffect {
}
Player controller = game.getPlayer(source.getControllerId());
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);
if (commander != null) {
FilterMana commanderMana = commander.getColorIdentity();
@ -106,7 +107,7 @@ class CommanderIdentityManaEffect extends ManaEffect {
if (controller != null) {
Choice choice = new ChoiceImpl();
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);
if (commander != null) {
FilterMana commanderMana = commander.getColorIdentity();

View file

@ -1,6 +1,12 @@
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
*/
public enum CommanderCardType {

View file

@ -507,27 +507,44 @@ public interface Game extends MageItem, Serializable {
Mulligan getMulligan();
Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType);
default Set<UUID> getCommandersIds(Player player) {
return getCommandersIds(player, CommanderCardType.ANY);
}
Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType, boolean returnAllCardParts);
/**
* Return not played commander cards from command zone
* Read comments for CommanderCardType for more info on commanderCardType usage
*
* @param player
* @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
return getCommandersIds(player).stream()
return getCommandersIds(player, commanderCardType, false).stream()
.map(this::getCard)
.filter(Objects::nonNull)
.filter(card -> Zone.COMMAND.equals(this.getState().getZone(card.getId())))
.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)
*
@ -546,7 +563,7 @@ public interface Game extends MageItem, Serializable {
if (object instanceof Card) {
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);

View file

@ -7,10 +7,7 @@ import mage.abilities.effects.common.continuous.CommanderReplacementEffect;
import mage.abilities.effects.common.cost.CommanderCostModification;
import mage.abilities.keyword.CompanionAbility;
import mage.cards.Card;
import mage.constants.MultiplayerAttackOption;
import mage.constants.PhaseStep;
import mage.constants.RangeOfInfluence;
import mage.constants.Zone;
import mage.constants.*;
import mage.game.mulligan.Mulligan;
import mage.game.turn.TurnMod;
import mage.players.Player;
@ -67,7 +64,7 @@ public abstract class GameCommanderImpl extends GameImpl {
}
// init commanders
for (UUID commanderId : this.getCommandersIds(player)) {
for (UUID commanderId : this.getCommandersIds(player, CommanderCardType.ANY, false)) {
Card commander = this.getCard(commanderId);
if (commander != null) {
initCommander(commander, player);
@ -193,7 +190,7 @@ public abstract class GameCommanderImpl extends GameImpl {
@Override
protected boolean checkStateBasedActions() {
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);
if (damageWatcher == null) {
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
// 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()) {
Set<UUID> commanderIds = getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER);
Set<UUID> commanderIds = getCommandersIds(player, CommanderCardType.COMMANDER_OR_OATHBREAKER, false);
if (commanderIds.isEmpty()) {
continue;
}
@ -3419,8 +3420,28 @@ public abstract class GameImpl implements Game, Serializable {
}
@Override
public Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType) {
return player.getCommandersIds();
public Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType, boolean returnAllCardParts) {
//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

View file

@ -43,7 +43,6 @@ import mage.filter.predicate.permanent.PermanentIdPredicate;
import mage.game.*;
import mage.game.combat.CombatGroup;
import mage.game.command.CommandObject;
import mage.game.command.Commander;
import mage.game.events.*;
import mage.game.match.MatchPlayer;
import mage.game.permanent.Permanent;
@ -314,7 +313,10 @@ public abstract class PlayerImpl implements Player, Serializable {
this.sideboard = player.getSideboard().copy();
this.hand = player.getHand().copy();
this.graveyard = player.getGraveyard().copy();
//noinspection deprecation - it's ok to use it in inner methods
this.commandersIds = new HashSet<>(player.getCommandersIds());
this.abilities = player.getAbilities().copy();
this.counters = player.getCounters().copy();
@ -1578,7 +1580,7 @@ public abstract class PlayerImpl implements Player, Serializable {
try {
// collect and filter playable activated abilities
// 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
List<ActivatedAbility> allPlayable = getPlayable(game, true, zone, false);
@ -1593,40 +1595,6 @@ public abstract class PlayerImpl implements Player, Serializable {
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) {
LinkedHashMap<UUID, ActivatedManaAbilityImpl> useable = new LinkedHashMap<>();
boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game);

View file

@ -4,10 +4,10 @@ import mage.MageItem;
import mage.abilities.Ability;
import mage.cards.Card;
import mage.cards.Cards;
import mage.constants.CommanderCardType;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.TargetEvent;
import mage.players.Player;
@ -115,6 +115,22 @@ public class TargetCard extends TargetObject {
}
}
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;
case COMMAND:
List<Card> possibleCards = game.getCommandersIds(player).stream()
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))

View file

@ -1177,4 +1177,50 @@ public final class CardUtil {
public static boolean checkCostWords(String text) {
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;
import mage.constants.CommanderCardType;
import mage.constants.WatcherScope;
import mage.constants.Zone;
import mage.game.Game;
@ -45,11 +46,12 @@ public class CommanderPlaysCountWatcher extends Watcher {
objectId = null;
}
// must calc all commanders and signature spell cause uses in commander tax
boolean isCommanderObject = game
.getPlayerList()
.stream()
.map(game::getPlayer)
.map(game::getCommandersIds)
.map(player -> game.getCommandersIds(player, CommanderCardType.ANY, false))
.flatMap(Collection::stream)
.anyMatch(id -> Objects.equals(id, objectId));
if (!isCommanderObject || event.getZone() != Zone.COMMAND) {