Reworking effects which allow casting spells from a selection of cards (ready for review) (#8136)

* added function for casting spells with specific attributes from a selection of cards

* updated cascade to use new method

* refactored various cards to use new methods

* added TestPlayer method

* fixed a small error

* text fix

* broke out some repeated code

* added missing notTarget setting

* add additional retain zone check

* some more cards refactored

* more refactoring

* added interface for split/modal cards

* reworked spell casting methods

* reworked multiple cast to prevent unnecessary dialogs

* fixed test failures due to change in functionality

* add AI code

* small nonfunctional change

* reworked Kaya, the Inexorable

* added currently failing test

* added more tests

* updated Geode Golem implementation

* fixed adventure/cascade interaction, added/updated tests

* some nonfunctional refactoring

* added interface for subcards

* [AFC] Implemented Fevered Suspicion

* [AFC] Implemented Extract Brain

* [AFC] updated Arcane Endeavor implementation

* [C17] reworked implementation of Izzet Chemister

* [ZEN] reworked implemented of Chandra Ablaze

* additional merge fix

* [SLD] updated Eleven, the Mage

* [NEO] Implemented Discover the Impossible

* [NEO] Implemented The Dragon-Kami Reborn / Dragon-Kami's Egg

* [NEO] Implemented Invoke Calamity

* [AFR] Implemented Rod of Absorption

* [VOC] Implemented Spectral Arcanist

* [VOC] added additional printings

* [NEO] added all variants

* [SLD] updated implementation of Ken, Burning Brawler
This commit is contained in:
Evan Kranzler 2022-03-09 08:03:54 -05:00 committed by GitHub
parent 7fb089db48
commit bbb9382150
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 2551 additions and 2059 deletions

View file

@ -7,6 +7,7 @@ import mage.abilities.costs.VariableCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.keyword.FlashAbility;
import mage.cards.AdventureCardSpell;
import mage.cards.Card;
import mage.cards.SplitCard;
import mage.constants.*;
@ -69,7 +70,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
// forced to cast (can be part id or main id)
Set<UUID> idsToCheck = new HashSet<>();
idsToCheck.add(object.getId());
if (object instanceof Card) {
if (object instanceof Card && !(object instanceof AdventureCardSpell)) {
idsToCheck.add(((Card) object).getMainCard().getId());
}
for (UUID idToCheck : idsToCheck) {

View file

@ -0,0 +1,48 @@
package mage.abilities.effects.common.cost;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.cards.CardsImpl;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
/**
* @author fireshoes - Original Code
* @author JRHerlehy - Implement as seperate class
* <p>
* Allows player to choose to cast as card from hand without paying its mana
* cost.
* </p>
*/
public class CastFromHandForFreeEffect extends OneShotEffect {
private final FilterCard filter;
public CastFromHandForFreeEffect(FilterCard filter) {
super(Outcome.PlayForFree);
this.filter = filter;
this.staticText = "you may cast " + filter.getMessage() + " from your hand without paying its mana cost";
}
public CastFromHandForFreeEffect(final CastFromHandForFreeEffect effect) {
super(effect);
this.filter = effect.filter;
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
return CardUtil.castSpellWithAttributesForFree(controller, source, game, new CardsImpl(controller.getHand()), filter);
}
@Override
public CastFromHandForFreeEffect copy() {
return new CastFromHandForFreeEffect(this);
}
}

View file

@ -1,115 +0,0 @@
package mage.abilities.effects.common.cost;
import mage.ApprovingObject;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.constants.ComparisonType;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.filter.common.FilterNonlandCard;
import mage.filter.predicate.mageobject.ManaValuePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetCardInHand;
import mage.util.CardUtil;
import org.apache.log4j.Logger;
/**
* @author fireshoes - Original Code
* @author JRHerlehy - Implement as seperate class
* <p>
* Allows player to choose to cast as card from hand without paying its mana
* cost.
* </p>
* TODO: this doesn't work correctly with MDFCs or Adventures (see https://github.com/magefree/mage/issues/7742)
*/
public class CastWithoutPayingManaCostEffect extends OneShotEffect {
private final DynamicValue manaCost;
private final FilterCard filter;
private static final FilterCard defaultFilter
= new FilterNonlandCard("card with mana value %mv or less from your hand");
/**
* @param maxCost Maximum converted mana cost for this effect to apply to
*/
public CastWithoutPayingManaCostEffect(int maxCost) {
this(StaticValue.get(maxCost));
}
public CastWithoutPayingManaCostEffect(DynamicValue maxCost) {
this(maxCost, defaultFilter);
}
public CastWithoutPayingManaCostEffect(DynamicValue maxCost, FilterCard filter) {
super(Outcome.PlayForFree);
this.manaCost = maxCost;
this.filter = filter;
this.staticText = "you may cast a spell with mana value "
+ maxCost + " or less from your hand without paying its mana cost";
}
public CastWithoutPayingManaCostEffect(final CastWithoutPayingManaCostEffect effect) {
super(effect);
this.manaCost = effect.manaCost;
this.filter = effect.filter;
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
int cmc = manaCost.calculate(game, source, this);
FilterCard filter = this.filter.copy();
filter.setMessage(filter.getMessage().replace("%mv", "" + cmc));
filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, cmc + 1));
Target target = new TargetCardInHand(filter);
if (!target.canChoose(
source.getSourceId(), controller.getId(), game
) || !controller.chooseUse(
Outcome.PlayForFree,
"Cast " + CardUtil.addArticle(filter.getMessage())
+ " without paying its mana cost?", source, game
)) {
return true;
}
Card cardToCast = null;
boolean cancel = false;
while (controller.canRespond()
&& !cancel) {
if (controller.chooseTarget(Outcome.PlayForFree, target, source, game)) {
cardToCast = game.getCard(target.getFirstTarget());
if (cardToCast != null) {
if (cardToCast.getSpellAbility() == null) {
Logger.getLogger(CastWithoutPayingManaCostEffect.class).fatal("Card: "
+ cardToCast.getName() + " is no land and has no spell ability!");
cancel = true;
}
if (cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) {
cancel = true;
}
}
} else {
cancel = true;
}
}
if (cardToCast != null) {
game.getState().setValue("PlayFromNotOwnHandZone" + cardToCast.getId(), Boolean.TRUE);
controller.cast(controller.chooseAbilityForCast(cardToCast, game, true),
game, true, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + cardToCast.getId(), null);
}
return true;
}
@Override
public CastWithoutPayingManaCostEffect copy() {
return new CastWithoutPayingManaCostEffect(this);
}
}

View file

@ -6,8 +6,7 @@ import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.ContinuousRuleModifyingEffectImpl;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.cards.Card;
import mage.cards.ModalDoubleFacesCardHalf;
import mage.cards.SplitCardHalf;
import mage.cards.SubCard;
import mage.constants.AsThoughEffectType;
import mage.constants.Duration;
import mage.constants.Outcome;
@ -156,12 +155,8 @@ class AftermathExileAsResolvesFromGraveyard extends ReplacementEffectImpl {
// wants to do that in the future.
UUID sourceId = source.getSourceId();
Card sourceCard = game.getCard(source.getSourceId());
if (sourceCard instanceof SplitCardHalf) {
sourceCard = ((SplitCardHalf) sourceCard).getParentCard();
sourceId = sourceCard.getId();
}
if (sourceCard instanceof ModalDoubleFacesCardHalf) {
sourceCard = ((ModalDoubleFacesCardHalf) sourceCard).getParentCard();
if (sourceCard instanceof SubCard) {
sourceCard = ((SubCard<?>) sourceCard).getParentCard();
sourceId = sourceCard.getId();
}
@ -178,11 +173,8 @@ class AftermathExileAsResolvesFromGraveyard extends ReplacementEffectImpl {
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Card sourceCard = game.getCard(source.getSourceId());
if (sourceCard instanceof SplitCardHalf) {
sourceCard = ((SplitCardHalf) sourceCard).getParentCard();
}
if (sourceCard instanceof ModalDoubleFacesCardHalf) {
sourceCard = ((ModalDoubleFacesCardHalf) sourceCard).getParentCard();
if (sourceCard instanceof SubCard) {
sourceCard = ((SubCard<?>) sourceCard).getParentCard();
}
if (sourceCard != null) {
Player player = game.getPlayer(sourceCard.getOwnerId());

View file

@ -1,23 +1,23 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.*;
import mage.cards.Card;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.ComparisonType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.filter.predicate.mageobject.ManaValuePredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.stack.Spell;
import mage.players.Player;
import mage.target.common.TargetCardInExile;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import mage.util.CardUtil;
/**
* Cascade A keyword ability that may let a player cast a random extra spell for
@ -152,45 +152,12 @@ class CascadeEffect extends OneShotEffect {
}
// You may cast that spell without paying its mana cost if its converted mana cost is less than this spell's converted mana cost.
List<Card> partsToCast = new ArrayList<>();
if (cardToCast != null) {
if (cardToCast instanceof SplitCard) {
partsToCast.add(((SplitCard) cardToCast).getLeftHalfCard());
partsToCast.add(((SplitCard) cardToCast).getRightHalfCard());
partsToCast.add(cardToCast);
} else if (cardToCast instanceof AdventureCard) {
partsToCast.add(((AdventureCard) cardToCast).getSpellCard());
partsToCast.add(cardToCast);
} else if (cardToCast instanceof ModalDoubleFacesCard) {
partsToCast.add(((ModalDoubleFacesCard) cardToCast).getLeftHalfCard());
partsToCast.add(((ModalDoubleFacesCard) cardToCast).getRightHalfCard());
} else {
partsToCast.add(cardToCast);
}
// remove too big cmc
partsToCast.removeIf(card -> card.getManaValue() >= sourceCost);
// remove non spells
partsToCast.removeIf(card -> card.getSpellAbility() == null);
}
String partsInfo = partsToCast.stream()
.map(MageObject::getIdName)
.collect(Collectors.joining(" or "));
if (cardToCast != null
&& partsToCast.size() > 0
&& controller.chooseUse(outcome, "Cast spell without paying its mana cost (" + partsInfo + ")?", source, game)) {
try {
// enable free cast for all compatible parts
partsToCast.forEach(card -> game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE));
controller.cast(controller.chooseAbilityForCast(cardToCast, game, true),
game, true, new ApprovingObject(source, game));
} finally {
partsToCast.forEach(card -> game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null));
}
}
FilterCard filter = new FilterCard();
filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, sourceCost + 1));
CardUtil.castSpellWithAttributesForFree(controller, source, game, new CardsImpl(cardToCast), filter);
// Then put all cards exiled this way that weren't cast on the bottom of your library in a random order.
cardsToExile.removeIf(uuid -> game.getState().getZone(uuid) != Zone.EXILED);
cardsToExile.retainZone(Zone.EXILED, game);
return controller.putCardsOnBottomOfLibrary(cardsToExile, game, source, false);
}

View file

@ -5,6 +5,7 @@ import mage.abilities.AbilitiesImpl;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.constants.CardType;
import mage.constants.SpellAbilityType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.ZoneChangeEvent;
@ -13,7 +14,7 @@ import java.util.List;
import java.util.UUID;
/**
* @author TheElk801
* @author phulin
*/
public abstract class AdventureCard extends CardImpl {
@ -90,13 +91,11 @@ public abstract class AdventureCard extends CardImpl {
@Override
public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
switch (ability.getSpellAbilityType()) {
case ADVENTURE_SPELL:
return this.getSpellCard().cast(game, fromZone, ability, controllerId);
default:
this.getSpellCard().getSpellAbility().setControllerId(controllerId);
return super.cast(game, fromZone, ability, controllerId);
if (ability.getSpellAbilityType() == SpellAbilityType.ADVENTURE_SPELL) {
return this.getSpellCard().cast(game, fromZone, ability, controllerId);
}
this.getSpellCard().getSpellAbility().setControllerId(controllerId);
return super.cast(game, fromZone, ability, controllerId);
}
@Override

View file

@ -1,15 +1,10 @@
package mage.cards;
/**
*
* @author phulin
*/
public interface AdventureCardSpell extends Card {
public interface AdventureCardSpell extends SubCard<AdventureCard> {
@Override
AdventureCardSpell copy();
void setParentCard(AdventureCard card);
AdventureCard getParentCard();
}

View file

@ -0,0 +1,11 @@
package mage.cards;
/**
* @author TheElk801
*/
public interface CardWithHalves extends Card {
Card getLeftHalfCard();
Card getRightHalfCard();
}

View file

@ -45,4 +45,6 @@ public interface Cards extends Set<UUID>, Serializable {
Cards copy();
void retainZone(Zone zone, Game game);
void removeZone(Zone zone, Game game);
}

View file

@ -210,4 +210,9 @@ public class CardsImpl extends LinkedHashSet<UUID> implements Cards, Serializabl
public void retainZone(Zone zone, Game game) {
removeIf(uuid -> game.getState().getZone(uuid) != zone);
}
@Override
public void removeZone(Zone zone, Game game) {
removeIf(uuid -> game.getState().getZone(uuid) == zone);
}
}

View file

@ -22,7 +22,7 @@ import java.util.UUID;
/**
* @author JayDi85
*/
public abstract class ModalDoubleFacesCard extends CardImpl {
public abstract class ModalDoubleFacesCard extends CardImpl implements CardWithHalves {
protected Card leftHalfCard; // main card in all zone
protected Card rightHalfCard; // second side card, can be only in stack and battlefield zones

View file

@ -5,15 +5,11 @@ import mage.MageInt;
/**
* @author JayDi85
*/
public interface ModalDoubleFacesCardHalf extends Card {
public interface ModalDoubleFacesCardHalf extends SubCard<ModalDoubleFacesCard> {
@Override
ModalDoubleFacesCardHalf copy();
void setParentCard(ModalDoubleFacesCard card);
ModalDoubleFacesCard getParentCard();
void setPT(int power, int toughness);
void setPT(MageInt power, MageInt toughness);

View file

@ -18,7 +18,7 @@ import java.util.UUID;
/**
* @author LevelX2
*/
public abstract class SplitCard extends CardImpl {
public abstract class SplitCard extends CardImpl implements CardWithHalves {
protected Card leftHalfCard;
protected Card rightHalfCard;

View file

@ -3,12 +3,8 @@ package mage.cards;
/**
* @author LevelX2
*/
public interface SplitCardHalf extends Card {
public interface SplitCardHalf extends SubCard<SplitCard> {
@Override
SplitCardHalf copy();
void setParentCard(SplitCard card);
SplitCard getParentCard();
}

View file

@ -0,0 +1,8 @@
package mage.cards;
public interface SubCard<T extends Card> extends Card {
void setParentCard(T card);
T getParentCard();
}

View file

@ -1,7 +1,7 @@
package mage.filter.predicate.mageobject;
import mage.MageObject;
import mage.cards.ModalDoubleFacesCard;
import mage.cards.CardWithHalves;
import mage.cards.SplitCard;
import mage.constants.SpellAbilityType;
import mage.filter.predicate.Predicate;
@ -34,13 +34,9 @@ public class NamePredicate implements Predicate<MageObject> {
// If a player names a card, the player may name either half of a split card, but not both.
// A split card has the chosen name if one of its two names matches the chosen name.
// Same for modal double faces cards
if (input instanceof SplitCard) {
return CardUtil.haveSameNames(name, ((SplitCard) input).getLeftHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, ((SplitCard) input).getRightHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, input.getName(), this.ignoreMtgRuleForEmptyNames);
} else if (input instanceof ModalDoubleFacesCard) {
return CardUtil.haveSameNames(name, ((ModalDoubleFacesCard) input).getLeftHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, ((ModalDoubleFacesCard) input).getRightHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
if (input instanceof CardWithHalves) {
return CardUtil.haveSameNames(name, ((CardWithHalves) input).getLeftHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, ((CardWithHalves) input).getRightHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, input.getName(), this.ignoreMtgRuleForEmptyNames);
} else if (input instanceof Spell && ((Spell) input).getSpellAbility().getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) {
SplitCard card = (SplitCard) ((Spell) input).getCard();

View file

@ -1,48 +1,51 @@
package mage.game.command.emblems;
import mage.ApprovingObject;
import mage.abilities.Ability;
import mage.abilities.common.BeginningOfUpkeepTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.choices.Choice;
import mage.choices.ChoiceImpl;
import mage.constants.*;
import mage.constants.Outcome;
import mage.constants.SuperType;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.common.FilterOwnedCard;
import mage.filter.predicate.Predicates;
import mage.game.Game;
import mage.game.command.Emblem;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.common.TargetCardInExile;
import mage.target.common.TargetCardInHand;
import mage.target.common.TargetCardInYourGraveyard;
import mage.util.CardUtil;
import java.util.LinkedHashSet;
import java.util.Set;
/**
*
* @author weirddan455
*/
public class KayaTheInexorableEmblem extends Emblem {
public class KayaTheInexorableEmblem extends Emblem {
// 7: You get an emblem with "At the beginning of your upkeep, you may cast a legendary spell from your hand, from your graveyard, or from among cards you own in exile without paying its mana cost."
public KayaTheInexorableEmblem() {
this.setName("Emblem Kaya");
this.setExpansionSetCodeForImage("KHM");
this.getAbilities().add(new BeginningOfUpkeepTriggeredAbility(Zone.COMMAND, new KayaTheInexorableEmblemEffect(), TargetController.YOU, true, false));
this.getAbilities().add(new BeginningOfUpkeepTriggeredAbility(
Zone.COMMAND, new KayaTheInexorableEmblemEffect(),
TargetController.YOU, true, false
));
}
}
class KayaTheInexorableEmblemEffect extends OneShotEffect {
private static final FilterOwnedCard filter = new FilterOwnedCard();
private static final FilterCard filter = new FilterOwnedCard();
private static final FilterCard filter2 = new FilterCard();
private static final Set<String> choices = new LinkedHashSet<>();
static {
filter.add(SuperType.LEGENDARY.getPredicate());
filter.add(Predicates.not(CardType.LAND.getPredicate()));
filter2.add(SuperType.LEGENDARY.getPredicate());
choices.add("Hand");
choices.add("Graveyard");
choices.add("Exile");
@ -50,7 +53,8 @@ class KayaTheInexorableEmblemEffect extends OneShotEffect {
public KayaTheInexorableEmblemEffect() {
super(Outcome.PlayForFree);
this.staticText = "cast a legendary spell from your hand, from your graveyard, or from among cards you own in exile without paying its mana cost";
this.staticText = "cast a legendary spell from your hand, from your graveyard, " +
"or from among cards you own in exile without paying its mana cost";
}
private KayaTheInexorableEmblemEffect(final KayaTheInexorableEmblemEffect effect) {
@ -65,40 +69,26 @@ class KayaTheInexorableEmblemEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player != null) {
Choice zoneChoice = new ChoiceImpl(true);
zoneChoice.setMessage("Cast a legendary spell from hand, graveyard, or exile");
zoneChoice.setChoices(choices);
zoneChoice.clearChoice();
if (player.choose(Outcome.PlayForFree, zoneChoice, game)) {
TargetCard target = null;
switch (zoneChoice.getChoice()) {
case "Hand":
target = new TargetCardInHand(0, 1, filter);
target.setTargetName("legendary spell from your hand");
break;
case "Graveyard":
target = new TargetCardInYourGraveyard(0, 1, filter, true);
target.setTargetName("legendary spell from your graveyard");
break;
case "Exile":
target = new TargetCardInExile(0, 1, filter, null, true);
target.setNotTarget(true);
target.setTargetName("legendary spell you own in exile");
break;
}
if (target != null && player.chooseTarget(Outcome.PlayForFree, target, source, game)) {
Card card = game.getCard(target.getFirstTarget());
if (card != null) {
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
boolean cardWasCast = player.cast(player.chooseAbilityForCast(card, game, true),
game, true, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
return cardWasCast;
}
}
}
if (player == null) {
return false;
}
return false;
Choice zoneChoice = new ChoiceImpl(true);
zoneChoice.setMessage("Cast a legendary spell from hand, graveyard, or exile");
zoneChoice.setChoices(choices);
zoneChoice.clearChoice();
player.choose(Outcome.PlayForFree, zoneChoice, game);
Cards cards = new CardsImpl();
switch (zoneChoice.getChoice()) {
case "Hand":
cards.addAll(player.getHand());
break;
case "Graveyard":
cards.addAll(player.getGraveyard());
break;
case "Exile":
cards.addAll(game.getExile().getCards(filter, game));
break;
}
return CardUtil.castSpellWithAttributesForFree(player, source, game, cards, filter2);
}
}

View file

@ -1571,72 +1571,75 @@ public abstract class PlayerImpl implements Player, Serializable {
* @param noMana
* @return
*/
public static LinkedHashMap<UUID, ActivatedAbility> getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) {
public static LinkedHashMap<UUID, SpellAbility> getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) {
// it uses simple check from spellCanBeActivatedRegularlyNow
// reason: no approved info here (e.g. forced to choose spell ability from cast card)
LinkedHashMap<UUID, ActivatedAbility> useable = new LinkedHashMap<>();
LinkedHashMap<UUID, SpellAbility> useable = new LinkedHashMap<>();
Abilities<Ability> allAbilities;
if (object instanceof Card) {
allAbilities = ((Card) object).getAbilities(game);
} else {
allAbilities = object.getAbilities();
}
for (Ability ability : allAbilities) {
if (ability instanceof SpellAbility) {
SpellAbility spellAbility = (SpellAbility) ability;
switch (spellAbility.getSpellAbilityType()) {
case BASE_ALTERNATE:
// rules:
// If you cast a spell without paying its mana cost, you cant choose to cast it for
// any alternative costs. You can, however, pay additional costs, such as kicker costs.
// If the card has any mandatory additional costs, those must be paid to cast the spell.
// (2021-02-05)
if (!noMana) {
if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) {
useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability
}
return useable;
for (SpellAbility spellAbility : allAbilities
.stream()
.filter(SpellAbility.class::isInstance)
.map(SpellAbility.class::cast)
.collect(Collectors.toList())) {
switch (spellAbility.getSpellAbilityType()) {
case BASE_ALTERNATE:
// rules:
// If you cast a spell without paying its mana cost, you cant choose to cast it for
// any alternative costs. You can, however, pay additional costs, such as kicker costs.
// If the card has any mandatory additional costs, those must be paid to cast the spell.
// (2021-02-05)
if (!noMana) {
if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) {
useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability
}
break;
case SPLIT_FUSED:
// rules:
// If you cast a split card with fuse from your hand without paying its mana cost,
// you can choose to use its fuse ability and cast both halves without paying their mana costs.
if (zone == Zone.HAND) {
if (spellAbility.canChooseTarget(game, playerId)) {
useable.put(spellAbility.getId(), spellAbility);
}
}
case SPLIT:
if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(),
((SplitCard) object).getLeftHalfCard().getSpellAbility());
return useable;
}
break;
case SPLIT_FUSED:
// rules:
// If you cast a split card with fuse from your hand without paying its mana cost,
// you can choose to use its fuse ability and cast both halves without paying their mana costs.
if (zone == Zone.HAND) {
if (spellAbility.canChooseTarget(game, playerId)) {
useable.put(spellAbility.getId(), spellAbility);
}
}
case SPLIT:
if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(
((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(),
((SplitCard) object).getLeftHalfCard().getSpellAbility()
);
}
if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(
((SplitCard) object).getRightHalfCard().getSpellAbility().getId(),
((SplitCard) object).getRightHalfCard().getSpellAbility()
);
}
return useable;
case SPLIT_AFTERMATH:
if (zone == Zone.GRAVEYARD) {
if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(),
((SplitCard) object).getRightHalfCard().getSpellAbility());
}
return useable;
case SPLIT_AFTERMATH:
if (zone == Zone.GRAVEYARD) {
if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(),
((SplitCard) object).getRightHalfCard().getSpellAbility());
}
} else {
if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(),
((SplitCard) object).getLeftHalfCard().getSpellAbility());
}
} else {
if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(),
((SplitCard) object).getLeftHalfCard().getSpellAbility());
}
return useable;
default:
if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) {
useable.put(spellAbility.getId(), spellAbility);
}
}
}
return useable;
default:
if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) {
useable.put(spellAbility.getId(), spellAbility);
}
}
}
return useable;

View file

@ -1,6 +1,7 @@
package mage.util;
import com.google.common.collect.ImmutableList;
import mage.ApprovingObject;
import mage.MageObject;
import mage.Mana;
import mage.abilities.Abilities;
@ -23,6 +24,7 @@ import mage.cards.*;
import mage.constants.*;
import mage.counters.Counter;
import mage.filter.Filter;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.CardState;
import mage.game.Game;
@ -36,6 +38,7 @@ import mage.game.permanent.token.Token;
import mage.game.stack.Spell;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetCard;
import mage.target.targetpointer.FixedTarget;
import mage.util.functions.CopyTokenFunction;
import org.apache.log4j.Logger;
@ -1196,6 +1199,126 @@ public final class CardUtil {
}
}
public interface SpellCastTracker {
boolean checkCard(Card card, Game game);
void addCard(Card card, Ability source, Game game);
}
private static List<Card> getCastableComponents(Card cardToCast, FilterCard filter, UUID sourceId, UUID playerId, Game game, SpellCastTracker spellCastTracker) {
List<Card> cards = new ArrayList<>();
if (cardToCast instanceof CardWithHalves) {
cards.add(((CardWithHalves) cardToCast).getLeftHalfCard());
cards.add(((CardWithHalves) cardToCast).getRightHalfCard());
} else if (cardToCast instanceof AdventureCard) {
cards.add(cardToCast);
cards.add(((AdventureCard) cardToCast).getSpellCard());
} else {
cards.add(cardToCast);
}
cards.removeIf(Objects::isNull);
cards.removeIf(card -> !filter.match(card, sourceId, playerId, game));
if (spellCastTracker != null) {
cards.removeIf(card -> spellCastTracker.checkCard(card, game));
}
return cards;
}
private static final FilterCard defaultFilter = new FilterCard("card to cast");
public static boolean castSpellWithAttributesForFree(Player player, Ability source, Game game, Cards cards, FilterCard filter) {
return castSpellWithAttributesForFree(player, source, game, cards, filter, null);
}
public static boolean castSpellWithAttributesForFree(Player player, Ability source, Game game, Cards cards, FilterCard filter, SpellCastTracker spellCastTracker) {
Map<UUID, List<Card>> cardMap = new HashMap<>();
for (Card card : cards.getCards(game)) {
List<Card> castableComponents = getCastableComponents(card, filter, source.getSourceId(), player.getId(), game, spellCastTracker);
if (!castableComponents.isEmpty()) {
cardMap.put(card.getId(), castableComponents);
}
}
Card cardToCast;
switch (cardMap.size()) {
case 0:
return false;
case 1:
cardToCast = cards.get(cardMap.keySet().stream().findFirst().orElse(null), game);
break;
default:
Cards castableCards = new CardsImpl(cardMap.keySet());
TargetCard target = new TargetCard(0, 1, Zone.ALL, defaultFilter);
target.setNotTarget(true);
player.choose(Outcome.PlayForFree, castableCards, target, game);
cardToCast = castableCards.get(target.getFirstTarget(), game);
}
if (cardToCast == null) {
return false;
}
List<Card> partsToCast = cardMap.get(cardToCast.getId());
String partsInfo = partsToCast
.stream()
.map(MageObject::getIdName)
.collect(Collectors.joining(" or "));
if (cardToCast == null
|| partsToCast.size() < 1
|| !player.chooseUse(
Outcome.PlayForFree, "Cast spell without paying its mana cost (" + partsInfo + ")?", source, game
)) {
return false;
}
partsToCast.forEach(card -> game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE));
boolean result = player.cast(
player.chooseAbilityForCast(cardToCast, game, true),
game, true, new ApprovingObject(source, game)
);
partsToCast.forEach(card -> game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null));
if (result && spellCastTracker != null) {
spellCastTracker.addCard(cardToCast, source, game);
}
if (player.isComputer() && !result) {
cards.remove(cardToCast);
}
return result;
}
private static boolean checkForPlayable(Cards cards, FilterCard filter, UUID sourceId, UUID playerId, Game game, SpellCastTracker spellCastTracker) {
return cards
.getCards(game)
.stream()
.anyMatch(card -> !getCastableComponents(card, filter, sourceId, playerId, game, spellCastTracker).isEmpty());
}
public static void castMultipleWithAttributeForFree(Player player, Ability source, Game game, Cards cards, FilterCard filter) {
castMultipleWithAttributeForFree(player, source, game, cards, filter, Integer.MAX_VALUE);
}
public static void castMultipleWithAttributeForFree(Player player, Ability source, Game game, Cards cards, FilterCard filter, int maxSpells) {
castMultipleWithAttributeForFree(player, source, game, cards, filter, maxSpells, null);
}
public static void castMultipleWithAttributeForFree(Player player, Ability source, Game game, Cards cards, FilterCard filter, int maxSpells, SpellCastTracker spellCastTracker) {
if (maxSpells == 1) {
CardUtil.castSpellWithAttributesForFree(player, source, game, cards, filter);
return;
}
int spellsCast = 0;
cards.removeZone(Zone.STACK, game);
while (player.canRespond() && spellsCast < maxSpells && !cards.isEmpty()) {
if (CardUtil.castSpellWithAttributesForFree(player, source, game, cards, filter, spellCastTracker)) {
spellsCast++;
cards.removeZone(Zone.STACK, game);
} else if (!checkForPlayable(
cards, filter, source.getSourceId(), player.getId(), game, spellCastTracker
) || !player.chooseUse(
Outcome.PlayForFree, "Continue casting spells?", source, game
)) {
break;
}
}
}
/**
* Pay life in effects
*

View file

@ -13,6 +13,7 @@ import mage.game.stack.Spell;
import mage.watchers.Watcher;
import java.util.*;
import java.util.stream.Stream;
/**
* @author LevelX2
@ -59,6 +60,10 @@ public class SpellsCastWatcher extends Watcher {
spellsCastFromGraveyard.clear();
}
public Stream<Spell> getAllSpellsCastThisTurn() {
return spellsCast.values().stream().flatMap(Collection::stream);
}
public List<Spell> getSpellsCastThisTurn(UUID playerId) {
return spellsCast.computeIfAbsent(playerId, x -> new ArrayList<>());
}