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

@ -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) {