Rework AsThough handling to allow choosing/affecting a specific alternate cast (#11114)

* Rework AsThoughEffect

* some cleanup of MageIdentifer

* refactor ActivationStatus

* fix bolas's citadel

* fix a couple of the Alternative Cost being applied too broadly.

* fix Risen Executioneer

* allow cancellation of AsThough choice.

* fix One with the Multiverse

* cleanup cards needing their own MageIdentifier

* last couple of fixes

* apply reviews for cleaner code.

* some more cleanup
This commit is contained in:
Susucre 2023-10-03 00:42:54 +02:00 committed by GitHub
parent ba135abc78
commit 7c454fb24c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 1176 additions and 395 deletions

View file

@ -1513,7 +1513,9 @@ public class ComputerPlayer extends PlayerImpl implements Player {
protected boolean playManaHandling(Ability ability, ManaCost unpaid, final Game game) { protected boolean playManaHandling(Ability ability, ManaCost unpaid, final Game game) {
// log.info("paying for " + unpaid.getText()); // log.info("paying for " + unpaid.getText());
ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(), AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(ability.getSourceId(), AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game);
boolean hasApprovingObject = !approvingObjects.isEmpty();
ManaCost cost; ManaCost cost;
List<MageObject> producers; List<MageObject> producers;
if (unpaid instanceof ManaCosts) { if (unpaid instanceof ManaCosts) {
@ -1543,7 +1545,7 @@ public class ComputerPlayer extends PlayerImpl implements Player {
if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) {
continue; continue;
} }
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue; continue;
} }
if (activateAbility(manaAbility, game)) { if (activateAbility(manaAbility, game)) {
@ -1560,11 +1562,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof ColoredManaCost) { if (cost instanceof ColoredManaCost) {
for (Mana netMana : manaAbility.getNetMana(game)) { for (Mana netMana : manaAbility.getNetMana(game)) {
if (cost.testPay(netMana) || approvingObject != null) { if (cost.testPay(netMana) || hasApprovingObject) {
if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) {
continue; continue;
} }
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue; continue;
} }
if (activateAbility(manaAbility, game)) { if (activateAbility(manaAbility, game)) {
@ -1578,11 +1580,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof SnowManaCost) { if (cost instanceof SnowManaCost) {
for (Mana netMana : manaAbility.getNetMana(game)) { for (Mana netMana : manaAbility.getNetMana(game)) {
if (cost.testPay(netMana) || approvingObject != null) { if (cost.testPay(netMana) || hasApprovingObject) {
if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) {
continue; continue;
} }
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue; continue;
} }
if (activateAbility(manaAbility, game)) { if (activateAbility(manaAbility, game)) {
@ -1596,11 +1598,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof HybridManaCost) { if (cost instanceof HybridManaCost) {
for (Mana netMana : manaAbility.getNetMana(game)) { for (Mana netMana : manaAbility.getNetMana(game)) {
if (cost.testPay(netMana) || approvingObject != null) { if (cost.testPay(netMana) || hasApprovingObject) {
if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) {
continue; continue;
} }
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue; continue;
} }
if (activateAbility(manaAbility, game)) { if (activateAbility(manaAbility, game)) {
@ -1614,11 +1616,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof MonoHybridManaCost) { if (cost instanceof MonoHybridManaCost) {
for (Mana netMana : manaAbility.getNetMana(game)) { for (Mana netMana : manaAbility.getNetMana(game)) {
if (cost.testPay(netMana) || approvingObject != null) { if (cost.testPay(netMana) || hasApprovingObject) {
if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) {
continue; continue;
} }
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue; continue;
} }
if (activateAbility(manaAbility, game)) { if (activateAbility(manaAbility, game)) {
@ -1632,11 +1634,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof ColorlessManaCost) { if (cost instanceof ColorlessManaCost) {
for (Mana netMana : manaAbility.getNetMana(game)) { for (Mana netMana : manaAbility.getNetMana(game)) {
if (cost.testPay(netMana) || approvingObject != null) { if (cost.testPay(netMana) || hasApprovingObject) {
if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) {
continue; continue;
} }
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue; continue;
} }
if (activateAbility(manaAbility, game)) { if (activateAbility(manaAbility, game)) {
@ -1650,11 +1652,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof GenericManaCost) { if (cost instanceof GenericManaCost) {
for (Mana netMana : manaAbility.getNetMana(game)) { for (Mana netMana : manaAbility.getNetMana(game)) {
if (cost.testPay(netMana) || approvingObject != null) { if (cost.testPay(netMana) || hasApprovingObject) {
if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) {
continue; continue;
} }
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue; continue;
} }
if (activateAbility(manaAbility, game)) { if (activateAbility(manaAbility, game)) {
@ -1673,7 +1675,7 @@ public class ComputerPlayer extends PlayerImpl implements Player {
// pay phyrexian life costs // pay phyrexian life costs
if (cost.isPhyrexian()) { if (cost.isPhyrexian()) {
alreadyTryingToPayPhyrexian = true; alreadyTryingToPayPhyrexian = true;
boolean paidPhyrexian = cost.pay(ability, game, ability, playerId, false, null) || approvingObject != null; boolean paidPhyrexian = cost.pay(ability, game, ability, playerId, false, null) || hasApprovingObject;
alreadyTryingToPayPhyrexian = false; alreadyTryingToPayPhyrexian = false;
return paidPhyrexian; return paidPhyrexian;
} }
@ -1688,7 +1690,7 @@ public class ComputerPlayer extends PlayerImpl implements Player {
ManaOptions specialMana = specialAction == null ? null : specialAction.getManaOptions(ability, game, unpaid); ManaOptions specialMana = specialAction == null ? null : specialAction.getManaOptions(ability, game, unpaid);
if (specialMana != null) { if (specialMana != null) {
for (Mana netMana : specialMana) { for (Mana netMana : specialMana) {
if (cost.testPay(netMana) || approvingObject != null) { if (cost.testPay(netMana) || hasApprovingObject) {
if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) {
continue; continue;
} }

View file

@ -1,5 +1,6 @@
package mage.player.human; package mage.player.human;
import mage.MageIdentifier;
import mage.MageObject; import mage.MageObject;
import mage.abilities.*; import mage.abilities.*;
import mage.abilities.costs.VariableCost; import mage.abilities.costs.VariableCost;
@ -16,6 +17,8 @@ import mage.cards.decks.Deck;
import mage.choices.Choice; import mage.choices.Choice;
import mage.choices.ChoiceImpl; import mage.choices.ChoiceImpl;
import mage.constants.*; import mage.constants.*;
import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL;
import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.filter.common.FilterAttackingCreature; import mage.filter.common.FilterAttackingCreature;
import mage.filter.common.FilterBlockingCreature; import mage.filter.common.FilterBlockingCreature;
@ -51,9 +54,6 @@ import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL;
import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -2260,7 +2260,7 @@ public class HumanPlayer extends PlayerImpl {
} }
// hide on alternative cost activated // hide on alternative cost activated
if (!getCastSourceIdWithAlternateMana().contains(ability.getSourceId()) if (!getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(MageIdentifier.Default)
&& ability.getManaCostsToPay().manaValue() > 0) { && ability.getManaCostsToPay().manaValue() > 0) {
return true; return true;
} }

View file

@ -1,26 +1,13 @@
package mage.cards.a; package mage.cards.a;
import java.util.EnumSet;
import java.util.Set;
import java.util.UUID;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.cards.Card; import mage.cards.*;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.cards.ModalDoubleFacedCard;
import mage.cards.ModalDoubleFacedCardHalf;
import mage.choices.Choice; import mage.choices.Choice;
import mage.choices.ChoiceImpl; import mage.choices.ChoiceImpl;
import mage.constants.AsThoughEffectType; import mage.constants.*;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.ExileZone; import mage.game.ExileZone;
import mage.game.Game; import mage.game.Game;
@ -29,6 +16,10 @@ import mage.target.TargetCard;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil; import mage.util.CardUtil;
import java.util.EnumSet;
import java.util.Set;
import java.util.UUID;
/** /**
* *
* @author credman0 * @author credman0
@ -218,7 +209,7 @@ class AminatousAuguryCastFromExileEffect extends AsThoughEffectImpl {
usedCardTypes.addAll(unusedCardTypes); usedCardTypes.addAll(unusedCardTypes);
game.getState().setValue(source.getSourceId().toString() + "cardTypes", usedCardTypes); game.getState().setValue(source.getSourceId().toString() + "cardTypes", usedCardTypes);
} }
player.setCastSourceIdWithAlternateMana(objectId, null, card.getSpellAbility().getCosts()); allowCardToPlayWithoutMana(objectId, source, player.getId(), game);
return true; return true;
} }
} }

View file

@ -1,5 +1,6 @@
package mage.cards.b; package mage.cards.b;
import mage.MageIdentifier;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
@ -44,7 +45,7 @@ public final class BolassCitadel extends CardImpl {
this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect()));
// You may play the top card of your library. If you cast a spell this way, pay life equal to its converted mana cost rather than pay its mana cost. // You may play the top card of your library. If you cast a spell this way, pay life equal to its converted mana cost rather than pay its mana cost.
this.addAbility(new SimpleStaticAbility(new BolassCitadelPlayTheTopCardEffect())); this.addAbility(new SimpleStaticAbility(new BolassCitadelPlayTheTopCardEffect()).setIdentifier(MageIdentifier.BolassCitadelAlternateCast));
// {T}, Sacrifice ten nonland permanents: Each opponent loses 10 life. // {T}, Sacrifice ten nonland permanents: Each opponent loses 10 life.
Ability ability = new SimpleActivatedAbility(new LoseLifeOpponentsEffect(10), new TapSourceCost()); Ability ability = new SimpleActivatedAbility(new LoseLifeOpponentsEffect(10), new TapSourceCost());
@ -118,7 +119,7 @@ class BolassCitadelPlayTheTopCardEffect extends AsThoughEffectImpl {
Costs newCosts = new CostsImpl(); Costs newCosts = new CostsImpl();
newCosts.add(lifeCost); newCosts.add(lifeCost);
newCosts.addAll(cardToCheck.getSpellAbility().getCosts()); newCosts.addAll(cardToCheck.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts); player.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts, MageIdentifier.BolassCitadelAlternateCast);
} }
return true; return true;
} }

View file

@ -104,7 +104,7 @@ class CemeteryIlluminatorExileEffect extends OneShotEffect {
class CemeteryIlluminatorPlayTopEffect extends AsThoughEffectImpl { class CemeteryIlluminatorPlayTopEffect extends AsThoughEffectImpl {
public CemeteryIlluminatorPlayTopEffect() { public CemeteryIlluminatorPlayTopEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
staticText = "Once each turn, you may cast a spell from the top of your library if it shares a card type with a card exiled with {this}"; staticText = "Once each turn, you may cast a spell from the top of your library if it shares a card type with a card exiled with {this}";
} }

View file

@ -63,7 +63,7 @@ public final class DanithaNewBenaliasLight extends CardImpl {
class DanithaNewBenaliasLightCastFromGraveyardEffect extends AsThoughEffectImpl { class DanithaNewBenaliasLightCastFromGraveyardEffect extends AsThoughEffectImpl {
DanithaNewBenaliasLightCastFromGraveyardEffect() { DanithaNewBenaliasLightCastFromGraveyardEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCardInPlay, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCardInPlay);
staticText = "once during each of your turns, you may cast an Aura or Equipment spell from your graveyard"; staticText = "once during each of your turns, you may cast an Aura or Equipment spell from your graveyard";
} }

View file

@ -1,7 +1,6 @@
package mage.cards.d; package mage.cards.d;
import java.util.UUID; import mage.MageIdentifier;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.common.AttacksTriggeredAbility;
@ -17,9 +16,9 @@ import mage.abilities.effects.Effect;
import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect;
import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect;
import mage.abilities.hint.ValueHint; import mage.abilities.hint.ValueHint;
import mage.constants.*;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.Game; import mage.game.Game;
import mage.game.stack.Spell; import mage.game.stack.Spell;
@ -27,6 +26,8 @@ import mage.players.Player;
import mage.target.common.TargetCardInYourGraveyard; import mage.target.common.TargetCardInYourGraveyard;
import mage.watchers.common.SpellsCastWatcher; import mage.watchers.common.SpellsCastWatcher;
import java.util.UUID;
/** /**
* *
* @author weirddan455 * @author weirddan455
@ -53,7 +54,7 @@ public final class Demilich extends CardImpl {
this.addAbility(ability); this.addAbility(ability);
// You may cast Demilich from your graveyard by exiling four instants and/or sorcery cards from your graveyard in addition to paying its other costs. // You may cast Demilich from your graveyard by exiling four instants and/or sorcery cards from your graveyard in addition to paying its other costs.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemilichPlayEffect())); this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemilichPlayEffect()).setIdentifier(MageIdentifier.DemilichAlternateCast));
} }
private Demilich(final Demilich card) { private Demilich(final Demilich card) {
@ -123,7 +124,7 @@ class DemilichPlayEffect extends AsThoughEffectImpl {
if (controller != null) { if (controller != null) {
Costs<Cost> costs = new CostsImpl<>(); Costs<Cost> costs = new CostsImpl<>();
costs.add(new ExileFromGraveCost(new TargetCardInYourGraveyard(4, StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD))); costs.add(new ExileFromGraveCost(new TargetCardInYourGraveyard(4, StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD)));
controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{U}{U}{U}{U}"), costs); controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{U}{U}{U}{U}"), costs, MageIdentifier.DemilichAlternateCast);
return true; return true;
} }
} }

View file

@ -1,7 +1,8 @@
package mage.cards.d; package mage.cards.d;
import java.util.UUID; import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs; import mage.abilities.costs.Costs;
import mage.abilities.costs.CostsImpl; import mage.abilities.costs.CostsImpl;
@ -9,22 +10,22 @@ import mage.abilities.costs.common.DiscardCardCost;
import mage.abilities.costs.common.PayLifeCost; import mage.abilities.costs.common.PayLifeCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.AsThoughEffectImpl;
import mage.constants.*;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetCreaturePermanent;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.continuous.AddCardSubtypeAttachedEffect; import mage.abilities.effects.common.continuous.AddCardSubtypeAttachedEffect;
import mage.abilities.effects.common.continuous.BoostEnchantedEffect; import mage.abilities.effects.common.continuous.BoostEnchantedEffect;
import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect;
import mage.target.TargetPermanent;
import mage.abilities.keyword.EnchantAbility; import mage.abilities.keyword.EnchantAbility;
import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/** /**
* @author arcox * @author arcox
@ -54,7 +55,7 @@ public final class DemonicEmbrace extends CardImpl {
this.addAbility(ability); this.addAbility(ability);
// You may cast Demonic Embrace from your graveyard by paying 3 life and discarding a card in addition to paying its other costs. // You may cast Demonic Embrace from your graveyard by paying 3 life and discarding a card in addition to paying its other costs.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemonicEmbracePlayEffect())); this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemonicEmbracePlayEffect()).setIdentifier(MageIdentifier.DemonicEmbraceAlternateCast));
} }
private DemonicEmbrace(final DemonicEmbrace card) { private DemonicEmbrace(final DemonicEmbrace card) {
@ -98,7 +99,10 @@ class DemonicEmbracePlayEffect extends AsThoughEffectImpl {
Costs<Cost> costs = new CostsImpl<>(); Costs<Cost> costs = new CostsImpl<>();
costs.add(new PayLifeCost(3)); costs.add(new PayLifeCost(3));
costs.add(new DiscardCardCost()); costs.add(new DiscardCardCost());
player.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{1}{B}{B}"), costs); player.setCastSourceIdWithAlternateMana(
sourceId, new ManaCostsImpl<>("{1}{B}{B}"), costs,
MageIdentifier.DemonicEmbraceAlternateCast
);
return true; return true;
} }
} }

View file

@ -1,5 +1,6 @@
package mage.cards.f; package mage.cards.f;
import mage.MageIdentifier;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.EntersBattlefieldAbility;
@ -18,12 +19,12 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.*; import mage.constants.*;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetControlledCreaturePermanent;
import java.util.UUID; import java.util.UUID;
import mage.filter.common.FilterControlledCreaturePermanent;
/** /**
* @author TheElk801 * @author TheElk801
@ -56,7 +57,10 @@ public final class FalcoSparaPactweaver extends CardImpl {
this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect()));
// You may cast spells from the top of your library by removing a counter from a creature you control in addition to paying their other costs. // You may cast spells from the top of your library by removing a counter from a creature you control in addition to paying their other costs.
this.addAbility(new SimpleStaticAbility(new FalcoSparaPactweaverEffect())); this.addAbility(
new SimpleStaticAbility(new FalcoSparaPactweaverEffect())
.setIdentifier(MageIdentifier.FalcoSparaPactweaverAlternateCast)
);
} }
private FalcoSparaPactweaver(final FalcoSparaPactweaver card) { private FalcoSparaPactweaver(final FalcoSparaPactweaver card) {
@ -113,7 +117,10 @@ class FalcoSparaPactweaverEffect extends AsThoughEffectImpl {
Costs<Cost> newCosts = new CostsImpl<>(); Costs<Cost> newCosts = new CostsImpl<>();
newCosts.add(new RemoveCounterCost(new TargetControlledCreaturePermanent(1, 1, new FilterControlledCreaturePermanent(), true))); newCosts.add(new RemoveCounterCost(new TargetControlledCreaturePermanent(1, 1, new FilterControlledCreaturePermanent(), true)));
newCosts.addAll(cardToCheck.getSpellAbility().getCosts()); newCosts.addAll(cardToCheck.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(cardToCheck.getId(), cardToCheck.getManaCost(), newCosts); player.setCastSourceIdWithAlternateMana(
cardToCheck.getId(), cardToCheck.getManaCost(), newCosts,
MageIdentifier.FalcoSparaPactweaverAlternateCast
);
return true; return true;
} }
} }

View file

@ -57,7 +57,7 @@ public final class GisaAndGeralf extends CardImpl {
class GisaAndGeralfCastFromGraveyardEffect extends AsThoughEffectImpl { class GisaAndGeralfCastFromGraveyardEffect extends AsThoughEffectImpl {
GisaAndGeralfCastFromGraveyardEffect() { GisaAndGeralfCastFromGraveyardEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCreatureInPlay, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCreatureInPlay);
staticText = "During each of your turns, you may cast a Zombie creature spell from your graveyard"; staticText = "During each of your turns, you may cast a Zombie creature spell from your graveyard";
} }

View file

@ -1,9 +1,12 @@
package mage.cards.g; package mage.cards.g;
import java.util.HashSet; import mage.MageIdentifier;
import java.util.Set;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.decorator.ConditionalAsThoughEffect;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.ReplacementEffectImpl;
import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; import mage.abilities.effects.common.LookLibraryAndPickControllerEffect;
import mage.cards.Card; import mage.cards.Card;
@ -15,14 +18,12 @@ import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent; import mage.game.events.ZoneChangeEvent;
import mage.players.Player; import mage.players.Player;
import java.util.UUID;
import mage.MageIdentifier;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.decorator.ConditionalAsThoughEffect;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.watchers.Watcher; import mage.watchers.Watcher;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/** /**
* *
* @author jeffwadsworth * @author jeffwadsworth
@ -36,11 +37,16 @@ public class GlimpseTheCosmos extends CardImpl {
this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect(3, 1, PutCards.HAND, PutCards.BOTTOM_ANY)); this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect(3, 1, PutCards.HAND, PutCards.BOTTOM_ANY));
//As long as you control a Giant, you may cast Glimpse the Cosmos from your graveyard by paying {U} rather than paying its mana cost. If you cast Glimpse the Cosmos this way and it would be put into your graveyard, exile it instead. //As long as you control a Giant, you may cast Glimpse the Cosmos from your graveyard by paying {U} rather than paying its mana cost. If you cast Glimpse the Cosmos this way and it would be put into your graveyard, exile it instead.
this.addAbility(new SimpleStaticAbility(Zone.GRAVEYARD, this.addAbility(
new ConditionalAsThoughEffect( new SimpleStaticAbility(
new GlimpseTheCosmosPlayEffect(), Zone.GRAVEYARD,
new PermanentsOnTheBattlefieldCondition(new FilterControlledPermanent(SubType.GIANT)))).setIdentifier(MageIdentifier.GlimpseTheCosmosWatcher), new ConditionalAsThoughEffect(
new GlimpseTheCosmosWatcher()); new GlimpseTheCosmosPlayEffect(),
new PermanentsOnTheBattlefieldCondition(new FilterControlledPermanent(SubType.GIANT))
)
).setIdentifier(MageIdentifier.GlimpseTheCosmosWatcher),
new GlimpseTheCosmosWatcher()
);
this.addAbility(new SimpleStaticAbility(Zone.ALL, new GlimpseTheCosmosReplacementEffect())); this.addAbility(new SimpleStaticAbility(Zone.ALL, new GlimpseTheCosmosReplacementEffect()));
@ -85,7 +91,10 @@ class GlimpseTheCosmosPlayEffect extends AsThoughEffectImpl {
if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) {
Player controller = game.getPlayer(affectedControllerId); Player controller = game.getPlayer(affectedControllerId);
if (controller != null) { if (controller != null) {
controller.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{U}"), null); controller.setCastSourceIdWithAlternateMana(
sourceId, new ManaCostsImpl<>("{U}"), null,
MageIdentifier.GlimpseTheCosmosWatcher
);
return true; return true;
} }
} }

View file

@ -136,7 +136,7 @@ class HaukensInsightLookEffect extends AsThoughEffectImpl {
class HaukensInsightPlayEffect extends AsThoughEffectImpl { class HaukensInsightPlayEffect extends AsThoughEffectImpl {
public HaukensInsightPlayEffect() { public HaukensInsightPlayEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PlayForFree, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PlayForFree);
staticText = "Once during each of your turns, you may play a land or cast a spell from among the cards exiled with this permanent without paying its mana cost"; staticText = "Once during each of your turns, you may play a land or cast a spell from among the cards exiled with this permanent without paying its mana cost";
} }
@ -164,7 +164,7 @@ class HaukensInsightPlayEffect extends AsThoughEffectImpl {
UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), game.getState().getZoneChangeCounter(source.getSourceId())); UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), game.getState().getZoneChangeCounter(source.getSourceId()));
ExileZone exileZone = game.getExile().getExileZone(exileId); ExileZone exileZone = game.getExile().getExileZone(exileId);
if (exileZone != null && exileZone.contains(CardUtil.getMainCardId(game, objectId))) { if (exileZone != null && exileZone.contains(CardUtil.getMainCardId(game, objectId))) {
allowCardToPlayWithoutMana(objectId, source, affectedControllerId, game); allowCardToPlayWithoutMana(objectId, source, affectedControllerId, MageIdentifier.HaukensInsightWatcher, game);
return true; return true;
} }
} }

View file

@ -1,5 +1,6 @@
package mage.cards.h; package mage.cards.h;
import mage.MageIdentifier;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
@ -37,7 +38,9 @@ public final class Helbrute extends CardImpl {
this.addAbility(HasteAbility.getInstance()); this.addAbility(HasteAbility.getInstance());
// Sarcophagus You may cast Helbrute from your graveyard by exiling another creature card from your graveyard in addition to paying its other costs. // Sarcophagus You may cast Helbrute from your graveyard by exiling another creature card from your graveyard in addition to paying its other costs.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new HelbruteEffect()).withFlavorWord("Sarcophagus")); this.addAbility(new SimpleStaticAbility(Zone.ALL, new HelbruteEffect())
.withFlavorWord("Sarcophagus")
.setIdentifier(MageIdentifier.HelbruteAlternateCast));
} }
private Helbrute(final Helbrute card) { private Helbrute(final Helbrute card) {
@ -91,7 +94,10 @@ class HelbruteEffect extends AsThoughEffectImpl {
} }
Costs<Cost> costs = new CostsImpl<>(); Costs<Cost> costs = new CostsImpl<>();
costs.add(new ExileFromGraveCost(new TargetCardInYourGraveyard(filter))); costs.add(new ExileFromGraveCost(new TargetCardInYourGraveyard(filter)));
controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{3}{B}{R}"), costs); controller.setCastSourceIdWithAlternateMana(
objectId, new ManaCostsImpl<>("{3}{B}{R}"), costs,
MageIdentifier.HelbruteAlternateCast
);
return true; return true;
} }
} }

View file

@ -84,7 +84,7 @@ enum JohannApprenticeSorcererHint implements Hint {
class JohannApprenticeSorcererPlayTopEffect extends AsThoughEffectImpl { class JohannApprenticeSorcererPlayTopEffect extends AsThoughEffectImpl {
public JohannApprenticeSorcererPlayTopEffect() { public JohannApprenticeSorcererPlayTopEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
staticText = "Once each turn, you may cast an instant or sorcery spell from the top of your library"; staticText = "Once each turn, you may cast an instant or sorcery spell from the top of your library";
} }

View file

@ -72,7 +72,7 @@ public final class KaghaShadowArchdruid extends CardImpl {
class KaghaShadowArchdruidEffect extends AsThoughEffectImpl { class KaghaShadowArchdruidEffect extends AsThoughEffectImpl {
KaghaShadowArchdruidEffect() { KaghaShadowArchdruidEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
this.staticText = "Once during each of your turns, you may play a land or cast a permanent spell from among cards in your graveyard that were put there from your library this turn."; this.staticText = "Once during each of your turns, you may play a land or cast a permanent spell from among cards in your graveyard that were put there from your library this turn.";
} }

View file

@ -98,7 +98,7 @@ class KaradorGhostChieftainCostReductionEffect extends CostModificationEffectImp
class KaradorGhostChieftainCastFromGraveyardEffect extends AsThoughEffectImpl { class KaradorGhostChieftainCastFromGraveyardEffect extends AsThoughEffectImpl {
KaradorGhostChieftainCastFromGraveyardEffect() { KaradorGhostChieftainCastFromGraveyardEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCreatureInPlay, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCreatureInPlay);
staticText = "During each of your turns, you may cast a creature spell from your graveyard"; staticText = "During each of your turns, you may cast a creature spell from your graveyard";
} }

View file

@ -60,7 +60,7 @@ public final class KessDissidentMage extends CardImpl {
class KessDissidentMageCastFromGraveyardEffect extends AsThoughEffectImpl { class KessDissidentMageCastFromGraveyardEffect extends AsThoughEffectImpl {
KessDissidentMageCastFromGraveyardEffect() { KessDissidentMageCastFromGraveyardEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
staticText = "During each of your turns, you may cast an instant or sorcery card from your graveyard"; staticText = "During each of your turns, you may cast an instant or sorcery card from your graveyard";
} }

View file

@ -1,5 +1,6 @@
package mage.cards.m; package mage.cards.m;
import mage.MageIdentifier;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
@ -31,7 +32,8 @@ public final class MaestrosAscendancy extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{U}{B}{R}"); super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{U}{B}{R}");
// Once during each of your turns, you may cast an instant or sorcery spell from your graveyard by sacrificing a creature in addition to paying its other costs. If a spell cast this way would be put into your graveyard, exile it instead. // Once during each of your turns, you may cast an instant or sorcery spell from your graveyard by sacrificing a creature in addition to paying its other costs. If a spell cast this way would be put into your graveyard, exile it instead.
Ability ability = new SimpleStaticAbility(new MaestrosAscendancyCastEffect()); Ability ability = new SimpleStaticAbility(new MaestrosAscendancyCastEffect())
.setIdentifier(MageIdentifier.MaestrosAscendencyAlternateCast);
ability.addEffect(new MaestrosAscendancyExileEffect()); ability.addEffect(new MaestrosAscendancyExileEffect());
this.addAbility(ability, new MaestrosAscendancyWatcher()); this.addAbility(ability, new MaestrosAscendancyWatcher());
} }
@ -87,7 +89,10 @@ class MaestrosAscendancyCastEffect extends AsThoughEffectImpl {
Costs<Cost> newCosts = new CostsImpl<>(); Costs<Cost> newCosts = new CostsImpl<>();
newCosts.addAll(card.getSpellAbility().getCosts()); newCosts.addAll(card.getSpellAbility().getCosts());
newCosts.add(new SacrificeTargetCost(StaticFilters.FILTER_CONTROLLED_CREATURE_SHORT_TEXT)); newCosts.add(new SacrificeTargetCost(StaticFilters.FILTER_CONTROLLED_CREATURE_SHORT_TEXT));
player.setCastSourceIdWithAlternateMana(card.getId(), card.getManaCost(), newCosts); player.setCastSourceIdWithAlternateMana(
card.getId(), card.getManaCost(), newCosts,
MageIdentifier.MaestrosAscendencyAlternateCast
);
return true; return true;
} }
} }

View file

@ -66,7 +66,7 @@ public final class MuldrothaTheGravetide extends CardImpl {
class MuldrothaTheGravetideCastFromGraveyardEffect extends AsThoughEffectImpl { class MuldrothaTheGravetideCastFromGraveyardEffect extends AsThoughEffectImpl {
public MuldrothaTheGravetideCastFromGraveyardEffect() { public MuldrothaTheGravetideCastFromGraveyardEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
staticText = "During each of your turns, you may play a land and cast a permanent spell of each permanent type from your graveyard. " staticText = "During each of your turns, you may play a land and cast a permanent spell of each permanent type from your graveyard. "
+ "<i>(If a card has multiple permanent types, choose one as you play it.)</i>"; + "<i>(If a card has multiple permanent types, choose one as you play it.)</i>";
} }

View file

@ -1,5 +1,6 @@
package mage.cards.n; package mage.cards.n;
import mage.MageIdentifier;
import mage.MageInt; import mage.MageInt;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -43,7 +44,7 @@ public final class NashiMoonSagesScion extends CardImpl {
// Whenever Nashi, Moon Sage's Scion deals combat damage to a player, exile the top card of each player's library. Until end of turn, you may play one of those cards. If you cast a spell this way, pay life equal to its mana value rather than paying its mana cost. // Whenever Nashi, Moon Sage's Scion deals combat damage to a player, exile the top card of each player's library. Until end of turn, you may play one of those cards. If you cast a spell this way, pay life equal to its mana value rather than paying its mana cost.
this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility( this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(
new NashiMoonSagesScionEffect(), false new NashiMoonSagesScionEffect(), false
), new NashiMoonSagesScionWatcher()); ).setIdentifier(MageIdentifier.NashiMoonSagesScionAlternateCast), new NashiMoonSagesScionWatcher());
} }
private NashiMoonSagesScion(final NashiMoonSagesScion card) { private NashiMoonSagesScion(final NashiMoonSagesScion card) {
@ -194,7 +195,10 @@ class NashiMoonSagesScionPlayEffect extends CanPlayCardControllerEffect {
Costs<Cost> newCosts = new CostsImpl<>(); Costs<Cost> newCosts = new CostsImpl<>();
newCosts.add(lifeCost); newCosts.add(lifeCost);
newCosts.addAll(cardToCheck.getSpellAbility().getCosts()); newCosts.addAll(cardToCheck.getSpellAbility().getCosts());
controller.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts); controller.setCastSourceIdWithAlternateMana(
cardToCheck.getId(), null, newCosts,
MageIdentifier.NashiMoonSagesScionAlternateCast
);
return true; return true;
} }
} }

View file

@ -4,15 +4,18 @@ import mage.MageIdentifier;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect;
import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; import mage.abilities.effects.common.continuous.PlayTheTopCardEffect;
import mage.abilities.hint.common.ConditionPermanentHint;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.*; import mage.constants.*;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.watchers.Watcher; import mage.watchers.Watcher;
@ -22,7 +25,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
/** /**
* @author TheElk801 * @author TheElk801, Susucr
*/ */
public final class OneWithTheMultiverse extends CardImpl { public final class OneWithTheMultiverse extends CardImpl {
@ -36,8 +39,15 @@ public final class OneWithTheMultiverse extends CardImpl {
this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect())); this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect()));
// Once during each of your turns, you may cast a spell from your hand or the top of your library without paying its mana cost. // Once during each of your turns, you may cast a spell from your hand or the top of your library without paying its mana cost.
this.addAbility(new SimpleStaticAbility(new OneWithTheMultiverseEffect()) this.addAbility(
.setIdentifier(MageIdentifier.OneWithTheMultiverseWatcher), new OneWithTheMultiverseWatcher()); new SimpleStaticAbility(new OneWithTheMultiverseEffect())
.setIdentifier(MageIdentifier.OneWithTheMultiverseWatcher)
.addHint(new ConditionPermanentHint(
OneWithTheMultiverseCondition.instance,
"Can cast a spell without paying mana cost this turn"
)),
new OneWithTheMultiverseWatcher()
);
} }
private OneWithTheMultiverse(final OneWithTheMultiverse card) { private OneWithTheMultiverse(final OneWithTheMultiverse card) {
@ -50,10 +60,24 @@ public final class OneWithTheMultiverse extends CardImpl {
} }
} }
enum OneWithTheMultiverseCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
OneWithTheMultiverseWatcher watcher = game.getState().getWatcher(OneWithTheMultiverseWatcher.class);
Permanent sourceObject = game.getPermanent(source.getSourceId());
return watcher != null
&& sourceObject != null
&& game.isActivePlayer(source.getControllerId())
&& !watcher.isAbilityUsed(new MageObjectReference(sourceObject, game));
}
}
class OneWithTheMultiverseEffect extends AsThoughEffectImpl { class OneWithTheMultiverseEffect extends AsThoughEffectImpl {
public OneWithTheMultiverseEffect() { public OneWithTheMultiverseEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PlayForFree, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PlayForFree);
staticText = "once during each of your turns, you may cast a spell from your hand " + staticText = "once during each of your turns, you may cast a spell from your hand " +
"or the top of your library without paying its mana cost."; "or the top of your library without paying its mana cost.";
} }
@ -78,13 +102,14 @@ class OneWithTheMultiverseEffect extends AsThoughEffectImpl {
return false; return false;
} }
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
Permanent sourceObject = game.getPermanent(source.getSourceId());
Card card = game.getCard(CardUtil.getMainCardId(game, objectId)); Card card = game.getCard(CardUtil.getMainCardId(game, objectId));
OneWithTheMultiverseWatcher watcher = game.getState().getWatcher(OneWithTheMultiverseWatcher.class); OneWithTheMultiverseWatcher watcher = game.getState().getWatcher(OneWithTheMultiverseWatcher.class);
if (controller == null if (controller == null
|| card == null || card == null
|| watcher == null || watcher == null
|| watcher.isAbilityUsed(new MageObjectReference(source)) || sourceObject == null
|| !card.isOwnedBy(source.getControllerId())) { || watcher.isAbilityUsed(new MageObjectReference(sourceObject, game))) {
return false; return false;
} }
Zone zone = game.getState().getZone(card.getId()); Zone zone = game.getState().getZone(card.getId());
@ -92,7 +117,8 @@ class OneWithTheMultiverseEffect extends AsThoughEffectImpl {
(!Zone.LIBRARY.match(zone) || !controller.getLibrary().getFromTop(game).getId().equals(card.getId()))) { (!Zone.LIBRARY.match(zone) || !controller.getLibrary().getFromTop(game).getId().equals(card.getId()))) {
return false; return false;
} }
allowCardToPlayWithoutMana(objectId, source, affectedControllerId, game);
allowCardToPlayWithoutMana(objectId, source, affectedControllerId, MageIdentifier.OneWithTheMultiverseWatcher, game);
return true; return true;
} }
} }

View file

@ -1,5 +1,6 @@
package mage.cards.r; package mage.cards.r;
import mage.MageIdentifier;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
@ -39,8 +40,9 @@ public final class RaffinesGuidance extends CardImpl {
// Enchanted creature gets +1/+1. // Enchanted creature gets +1/+1.
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostEnchantedEffect(1, 1, Duration.WhileOnBattlefield))); this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostEnchantedEffect(1, 1, Duration.WhileOnBattlefield)));
// You may cast Raffines Guidance from your graveyard by paying {2}{W} instead of its mana cost. // You may cast Raffine's Guidance from your graveyard by paying {2}{W} instead of its mana cost.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new RafinnesGuidancePlayEffect())); this.addAbility(new SimpleStaticAbility(Zone.ALL, new RafinnesGuidancePlayEffect())
.setIdentifier(MageIdentifier.RafinnesGuidanceAlternateCast));
} }
private RaffinesGuidance(final RaffinesGuidance card) { private RaffinesGuidance(final RaffinesGuidance card) {
@ -71,7 +73,10 @@ class RafinnesGuidancePlayEffect extends AsThoughEffectImpl {
Player player = game.getPlayer(affectedControllerId); Player player = game.getPlayer(affectedControllerId);
if (player != null) { if (player != null) {
Costs<Cost> costs = new CostsImpl<>(); Costs<Cost> costs = new CostsImpl<>();
player.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{2}{W}"), costs); player.setCastSourceIdWithAlternateMana(
sourceId, new ManaCostsImpl<>("{2}{W}"), costs,
MageIdentifier.RafinnesGuidanceAlternateCast
);
return true; return true;
} }
} }

View file

@ -2,10 +2,14 @@
package mage.cards.r; package mage.cards.r;
import java.util.UUID; import java.util.UUID;
import mage.MageIdentifier;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.CantBlockAbility; import mage.abilities.common.CantBlockAbility;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.common.continuous.BoostControlledEffect; import mage.abilities.effects.common.continuous.BoostControlledEffect;
import mage.abilities.effects.common.cost.CostModificationEffectImpl; import mage.abilities.effects.common.cost.CostModificationEffectImpl;
@ -23,7 +27,7 @@ import mage.util.CardUtil;
/** /**
* *
* @author LevelX2 * @author LevelX2, Susucr
*/ */
public final class RisenExecutioner extends CardImpl { public final class RisenExecutioner extends CardImpl {
@ -47,9 +51,8 @@ public final class RisenExecutioner extends CardImpl {
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostControlledEffect(1, 1, Duration.WhileOnBattlefield, filter, true))); this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostControlledEffect(1, 1, Duration.WhileOnBattlefield, filter, true)));
// You may cast Risen Executioner from your graveyard if you pay {1} more to cast it for each other creature card in your graveyard. // You may cast Risen Executioner from your graveyard if you pay {1} more to cast it for each other creature card in your graveyard.
// TODO: cost increase does not happen if Risen Executioner is cast grom graveyard because of other effects Ability ability = new SimpleStaticAbility(Zone.ALL, new RisenExecutionerCastEffect())
Ability ability = new SimpleStaticAbility(Zone.ALL, new RisenExecutionerCastEffect()); .setIdentifier(MageIdentifier.RisenExectutionerAlternateCast);
ability.addEffect(new RisenExecutionerCostIncreasingEffect());
this.addAbility(ability); this.addAbility(ability);
} }
@ -66,6 +69,12 @@ public final class RisenExecutioner extends CardImpl {
class RisenExecutionerCastEffect extends AsThoughEffectImpl { class RisenExecutionerCastEffect extends AsThoughEffectImpl {
protected static final FilterCreatureCard filter = new FilterCreatureCard();
static {
filter.add(AnotherPredicate.instance);
}
RisenExecutionerCastEffect() { RisenExecutionerCastEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit);
staticText = "You may cast {this} from your graveyard if you pay {1} more to cast it for each other creature card in your graveyard"; staticText = "You may cast {this} from your graveyard if you pay {1} more to cast it for each other creature card in your graveyard";
@ -87,56 +96,23 @@ class RisenExecutionerCastEffect extends AsThoughEffectImpl {
@Override @Override
public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) {
if (sourceId.equals(source.getSourceId())) { if (!sourceId.equals(source.getSourceId())) {
Card card = game.getCard(source.getSourceId()); return false;
if (card != null }
&& card.isOwnedBy(affectedControllerId) Card card = game.getCard(source.getSourceId());
&& game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { if(card == null
return true; || !card.isOwnedBy(affectedControllerId)
} || game.getState().getZone(source.getSourceId()) != Zone.GRAVEYARD) {
return false;
} }
return false;
}
}
class RisenExecutionerCostIncreasingEffect extends CostModificationEffectImpl {
protected static final FilterCreatureCard filter = new FilterCreatureCard();
static {
filter.add(AnotherPredicate.instance);
}
RisenExecutionerCostIncreasingEffect() {
super(Duration.EndOfGame, Outcome.Benefit, CostModificationType.INCREASE_COST);
staticText = "";
}
private RisenExecutionerCostIncreasingEffect(final RisenExecutionerCostIncreasingEffect effect) {
super(effect);
}
@Override
public boolean apply(Game game, Ability source, Ability abilityToModify) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if(controller == null) {
CardUtil.increaseCost(abilityToModify, controller.getGraveyard().count(filter, source.getControllerId(), source, game)); return false;
} }
int costIncrease = controller.getGraveyard().count(filter, source.getControllerId(), source, game);
ManaCosts<ManaCost> adjustedCost = CardUtil.adjustCost(card.getSpellAbility().getManaCostsToPay(), -costIncrease);
controller.setCastSourceIdWithAlternateMana(card.getId(), adjustedCost, null, MageIdentifier.RisenExectutionerAlternateCast);
return true; return true;
} }
@Override
public boolean applies(Ability abilityToModify, Ability source, Game game) {
if (abilityToModify.getSourceId().equals(source.getSourceId())) {
Spell spell = game.getStack().getSpell(abilityToModify.getSourceId());
return spell != null && spell.getFromZone() == Zone.GRAVEYARD;
}
return false;
}
@Override
public RisenExecutionerCostIncreasingEffect copy() {
return new RisenExecutionerCostIncreasingEffect(this);
}
} }

View file

@ -1,5 +1,6 @@
package mage.cards.r; package mage.cards.r;
import mage.MageIdentifier;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
@ -42,7 +43,8 @@ public final class RonaSheoldredsFaithful extends CardImpl {
)); ));
// You may cast Rona, Sheoldred's Faithful from your graveyard by discarding two cards in addition to paying its other costs. // You may cast Rona, Sheoldred's Faithful from your graveyard by discarding two cards in addition to paying its other costs.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new RonaSheoldredsFaithfulEffect())); this.addAbility(new SimpleStaticAbility(Zone.ALL, new RonaSheoldredsFaithfulEffect())
.setIdentifier(MageIdentifier.RonaSheoldredsFaithfulAlternateCast));
} }
private RonaSheoldredsFaithful(final RonaSheoldredsFaithful card) { private RonaSheoldredsFaithful(final RonaSheoldredsFaithful card) {
@ -90,7 +92,10 @@ class RonaSheoldredsFaithfulEffect extends AsThoughEffectImpl {
} }
Costs<Cost> costs = new CostsImpl<>(); Costs<Cost> costs = new CostsImpl<>();
costs.add(new DiscardTargetCost(new TargetCardInHand(2, StaticFilters.FILTER_CARD_CARDS))); costs.add(new DiscardTargetCost(new TargetCardInHand(2, StaticFilters.FILTER_CARD_CARDS)));
controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{1}{U}{B}{B}"), costs); controller.setCastSourceIdWithAlternateMana(
objectId, new ManaCostsImpl<>("{1}{U}{B}{B}"), costs,
MageIdentifier.RonaSheoldredsFaithfulAlternateCast
);
return true; return true;
} }
} }

View file

@ -1,7 +1,7 @@
package mage.cards.s; package mage.cards.s;
import java.util.UUID; import mage.MageIdentifier;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
@ -14,16 +14,13 @@ import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.AsThoughEffectType; import mage.constants.*;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetControlledCreaturePermanent;
import java.util.UUID;
/** /**
* *
* @author LevelX2 * @author LevelX2
@ -40,7 +37,8 @@ public final class ScourgeOfNelToth extends CardImpl {
// Flying // Flying
this.addAbility(FlyingAbility.getInstance()); this.addAbility(FlyingAbility.getInstance());
// You may cast Scourge of Nel Toth from your graveyard by paying {B}{B} and sacrificing two creatures rather than paying its mana cost. // You may cast Scourge of Nel Toth from your graveyard by paying {B}{B} and sacrificing two creatures rather than paying its mana cost.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new ScourgeOfNelTothPlayEffect())); this.addAbility(new SimpleStaticAbility(Zone.ALL, new ScourgeOfNelTothPlayEffect())
.setIdentifier(MageIdentifier.ScourgeOfNelTothAlternateCast));
} }
private ScourgeOfNelToth(final ScourgeOfNelToth card) { private ScourgeOfNelToth(final ScourgeOfNelToth card) {
@ -80,10 +78,12 @@ class ScourgeOfNelTothPlayEffect extends AsThoughEffectImpl {
if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) {
Player player = game.getPlayer(affectedControllerId); Player player = game.getPlayer(affectedControllerId);
if (player != null) { if (player != null) {
// can sometimes be cast with base mana cost from grave????
Costs<Cost> costs = new CostsImpl<>(); Costs<Cost> costs = new CostsImpl<>();
costs.add(new SacrificeTargetCost(new TargetControlledCreaturePermanent(2))); costs.add(new SacrificeTargetCost(new TargetControlledCreaturePermanent(2)));
player.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{B}{B}"), costs); player.setCastSourceIdWithAlternateMana(
sourceId, new ManaCostsImpl<>("{B}{B}"), costs,
MageIdentifier.ScourgeOfNelTothAlternateCast
);
return true; return true;
} }
} }

View file

@ -1,5 +1,6 @@
package mage.cards.s; package mage.cards.s;
import mage.MageIdentifier;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.common.AttacksTriggeredAbility;
@ -47,7 +48,8 @@ public final class SqueeDubiousMonarch extends CardImpl {
))); )));
// You may cast Squee, Dubious Monarch from your graveyard by paying {3}{R} and exiling four other cards from your graveyard rather than paying its mana cost. // You may cast Squee, Dubious Monarch from your graveyard by paying {3}{R} and exiling four other cards from your graveyard rather than paying its mana cost.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SqueeDubiousMonarchEffect())); this.addAbility(new SimpleStaticAbility(Zone.ALL, new SqueeDubiousMonarchEffect())
.setIdentifier(MageIdentifier.SqueeDubiousMonarchAlternateCast));
} }
private SqueeDubiousMonarch(final SqueeDubiousMonarch card) { private SqueeDubiousMonarch(final SqueeDubiousMonarch card) {
@ -101,7 +103,10 @@ class SqueeDubiousMonarchEffect extends AsThoughEffectImpl {
} }
Costs<Cost> costs = new CostsImpl<>(); Costs<Cost> costs = new CostsImpl<>();
costs.add(new ExileFromGraveCost(new TargetCardInYourGraveyard(4, filter))); costs.add(new ExileFromGraveCost(new TargetCardInYourGraveyard(4, filter)));
controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{3}{R}"), costs); controller.setCastSourceIdWithAlternateMana(
objectId, new ManaCostsImpl<>("{3}{R}"), costs,
MageIdentifier.SqueeDubiousMonarchAlternateCast
);
return true; return true;
} }
} }

View file

@ -81,7 +81,7 @@ class CantBeBlockedUnlessAllEffect extends RestrictionEffect {
// check if all creatures of defender are able to block this permanent // check if all creatures of defender are able to block this permanent
// permanent.canBlock() can't be used because causing recursive call // permanent.canBlock() can't be used because causing recursive call
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, blocker.getControllerId(), game)) { for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, blocker.getControllerId(), game)) {
if (permanent.isTapped() && null == game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, blocker.getControllerId(), game)) { if (permanent.isTapped() && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, blocker.getControllerId(), game).isEmpty()) {
return false; return false;
} }
// check blocker restrictions // check blocker restrictions

View file

@ -1,5 +1,6 @@
package mage.cards.w; package mage.cards.w;
import mage.ApprovingObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.ActivatedAbilityImpl; import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.condition.common.IsStepCondition; import mage.abilities.condition.common.IsStepCondition;
@ -63,7 +64,7 @@ class WellOfKnowledgeConditionalActivatedAbility extends ActivatedAbilityImpl {
&& getCosts().canPay(this, this, playerId, game) && getCosts().canPay(this, this, playerId, game)
&& game.isActivePlayer(playerId)) { && game.isActivePlayer(playerId)) {
this.activatorId = playerId; this.activatorId = playerId;
return ActivationStatus.getTrue(this, game); return new ActivationStatus(new ApprovingObject(this, game));
} }
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();

View file

@ -77,7 +77,7 @@ class WishEffect extends OneShotEffect {
class WishPlayFromSideboardEffect extends AsThoughEffectImpl { class WishPlayFromSideboardEffect extends AsThoughEffectImpl {
public WishPlayFromSideboardEffect() { public WishPlayFromSideboardEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit);
} }
private WishPlayFromSideboardEffect(final WishPlayFromSideboardEffect effect) { private WishPlayFromSideboardEffect(final WishPlayFromSideboardEffect effect) {

View file

@ -1,7 +1,7 @@
package mage.cards.w; package mage.cards.w;
import java.util.UUID; import mage.MageIdentifier;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
@ -13,17 +13,14 @@ import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.AsThoughEffectType; import mage.constants.*;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import java.util.UUID;
/** /**
* *
* @author LevelX2 * @author LevelX2
@ -41,7 +38,8 @@ public final class WorldheartPhoenix extends CardImpl {
// You may cast Worldheart Phoenix from your graveyard by paying {W}{U}{B}{R}{G} rather than paying its mana cost. // You may cast Worldheart Phoenix from your graveyard by paying {W}{U}{B}{R}{G} rather than paying its mana cost.
// If you do, it enters the battlefield with two +1/+1 counters on it. // If you do, it enters the battlefield with two +1/+1 counters on it.
Ability ability = new SimpleStaticAbility(Zone.ALL, new WorldheartPhoenixPlayEffect()); Ability ability = new SimpleStaticAbility(Zone.ALL, new WorldheartPhoenixPlayEffect())
.setIdentifier(MageIdentifier.WorldheartPhoenixAlternateCast);
ability.addEffect(new EntersBattlefieldEffect(new WorldheartPhoenixEntersBattlefieldEffect(), ability.addEffect(new EntersBattlefieldEffect(new WorldheartPhoenixEntersBattlefieldEffect(),
"If you do, it enters the battlefield with two +1/+1 counters on it")); "If you do, it enters the battlefield with two +1/+1 counters on it"));
this.addAbility(ability); this.addAbility(ability);
@ -84,8 +82,10 @@ public final class WorldheartPhoenix extends CardImpl {
if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) {
Player player = game.getPlayer(affectedControllerId); Player player = game.getPlayer(affectedControllerId);
if (player != null) { if (player != null) {
// can sometimes be cast with base mana cost from grave???? player.setCastSourceIdWithAlternateMana(
player.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{W}{U}{B}{R}{G}"), null); sourceId, new ManaCostsImpl<>("{W}{U}{B}{R}{G}"), null,
MageIdentifier.WorldheartPhoenixAlternateCast
);
return true; return true;
} }
} }

View file

@ -1,5 +1,6 @@
package mage.cards.x; package mage.cards.x;
import mage.MageIdentifier;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs; import mage.abilities.costs.Costs;
@ -36,6 +37,7 @@ public final class XandersPact extends CardImpl {
// Each opponent exiles the top card of their library. You may cast spells from among those cards this turn. If you cast a spell this way, pay life equal to that spell's mana value rather than pay its mana cost. // Each opponent exiles the top card of their library. You may cast spells from among those cards this turn. If you cast a spell this way, pay life equal to that spell's mana value rather than pay its mana cost.
this.getSpellAbility().addEffect(new XandersPactExileEffect()); this.getSpellAbility().addEffect(new XandersPactExileEffect());
this.getSpellAbility().setIdentifier(MageIdentifier.XandersPactAlternateCast);
} }
private XandersPact(final XandersPact card) { private XandersPact(final XandersPact card) {
@ -120,7 +122,10 @@ class XandersPactCastEffect extends CanPlayCardControllerEffect {
Costs<Cost> newCosts = new CostsImpl<>(); Costs<Cost> newCosts = new CostsImpl<>();
newCosts.add(new PayLifeCost(cardToCheck.getManaValue())); newCosts.add(new PayLifeCost(cardToCheck.getManaValue()));
newCosts.addAll(cardToCheck.getSpellAbility().getCosts()); newCosts.addAll(cardToCheck.getSpellAbility().getCosts());
controller.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts); controller.setCastSourceIdWithAlternateMana(
cardToCheck.getId(), null, newCosts,
MageIdentifier.XandersPactAlternateCast
);
return true; return true;
} }
} }

View file

@ -0,0 +1,81 @@
package org.mage.test.cards.abilities.other;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Alex-Vasile, Susucr
*/
public class MultipleAsThoughEffects extends CardTestPlayerBase {
/**
* Reported bug: https://github.com/magefree/mage/issues/8584
*
* If there are multiple effects which allow a player to cast a spell,
* they should be able to choose which one they whish to use.
*/
@Test
public void ChoosingAlternateCastingMethod() {
setStrictChooseMode(true);
skipInitShuffling();
// You may cast creature spells from the top of your library.
addCard(Zone.HAND, playerA, "Vivien, Monsters' Advocate");
// You may play lands and cast spells from the top of your library.
// If you cast a spell this way, pay life equal to its mana value rather than pay its mana cost.
addCard(Zone.BATTLEFIELD, playerA, "Bolas's Citadel");
// Random creature card to play with mana value of 3
addCard(Zone.LIBRARY, playerA, "Abzan Beastmaster",2);
addCard(Zone.LIBRARY, playerA, "Grizzly Bears",1); // This one is drawn.
addCard(Zone.BATTLEFIELD, playerA, "Forest",5);
// For the "cast from the top" abilities to work, Vivien or Bolas's Citadel
// must be played and not be in battlefield as start. Or else the top of the library will
// not be able to be cast during the test.
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Vivien, Monsters' Advocate");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Abzan Beastmaster",true);
setChoice(playerA, "Vivien");
checkLife("Vivien not making pay life", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 20);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Abzan Beastmaster");
setChoice(playerA, "Bolas's");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Abzan Beastmaster", 2);
assertTappedCount("Forest", true, 3);
assertLife(playerA, 20 - 3); // 3 from casting Abzan Beastmaster with Bolas Citadel
}
/**
* Reported bug: https://github.com/magefree/mage/issues/2087
*
* If there are multiple effects which allow a player to cast a spell,
* they should be able to choose which one they whish to use, even if one is single-use.
*/
@Test
public void RisenExecutioner() {
setStrictChooseMode(true);
// You may cast Risen Executioner from your graveyard if you pay {1} more to cast it for each other creature card in your graveyard.
addCard(Zone.GRAVEYARD, playerA, "Risen Executioner", 2);
addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 1);
// During each of your turns, you may cast a Zombie creature spell from your graveyard.
addCard(Zone.BATTLEFIELD, playerA, "Gisa and Geralf");
addCard(Zone.BATTLEFIELD, playerA, "Swamp",9); // Only enough mana to cast
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Risen Executioner", true); // Should cost {2}{B}{B} since cast with Gisa
setChoice(playerA, "Gisa");
checkPermanentTapped("Swamp tapped after cast with Gisa and Geralf", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", true, 4);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Risen Executioner"); // Should cost {3}{B}{B} when cast with own ability, there is another creature in the graveyard
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Risen Executioner", 2);
assertTappedCount("Swamp", true, 4 + 5);
}
}

View file

@ -734,6 +734,7 @@ public class AdventureCardsTest extends CardTestPlayerBase {
checkExileCount("after exile 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Curious Pair", 1); checkExileCount("after exile 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Curious Pair", 1);
// play as adventure spell // play as adventure spell
castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Treats to Share"); castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Treats to Share");
setChoice(playerA, "Hostage Taker"); // Not sure why there is an alternative there. No issue with using either. TODO: investigate?
waitStackResolved(3, PhaseStep.POSTCOMBAT_MAIN); waitStackResolved(3, PhaseStep.POSTCOMBAT_MAIN);
checkPermanentCount("after play 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Food Token", 1); checkPermanentCount("after play 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Food Token", 1);

View file

@ -96,11 +96,12 @@ public class KaradorGhostChieftainTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5);
addCard(Zone.BATTLEFIELD, playerA, "Island", 3); addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
// //
// {1}{B}: Target attacking Zombie gains indestructible until end of turn. // {1}{B}: Target attacking Zombie gains indestructible until end of turn.
addCard(Zone.LIBRARY, playerA, "Accursed Horde", 1); // Creature Zombie {3}{B} addCard(Zone.LIBRARY, playerA, "Accursed Horde", 1); // Creature Zombie {3}{B}
addCard(Zone.LIBRARY, playerA, "Carrion Screecher", 1); // Creature Zombie {3}{B}
// //
addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion", 5); // Creature {1}{W} addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion", 5); // Creature {1}{W}
// //
@ -119,15 +120,64 @@ public class KaradorGhostChieftainTest extends CardTestPlayerBase {
// you play any creatures due to two approve objects // you play any creatures due to two approve objects
checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", true); checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", true);
checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Accursed Horde", true); checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Accursed Horde", true);
checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Carrion Screecher", true);
// cast zombie creature and approves by Karagar // cast zombie creature and approves by Karador
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Accursed Horde"); castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Accursed Horde");
setChoice(playerA, "Karador, Ghost Chieftain"); // choose the permitting object setChoice(playerA, "Karador, Ghost Chieftain"); // choose the permitting object
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN);
// you can't cast lion due to approving object (Gisa needs zombie) // you can't cast lion due to approving object (Gisa needs zombie)
checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", false); checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", false);
checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Accursed Horde", false); checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Accursed Horde", false);
checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Carrion Screecher", true);
setStrictChooseMode(true);
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
}
@Test
public void test_castFromGraveyardWithDifferentApproversOtherCast() {
skipInitShuffling();
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 6);
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
//
// {1}{B}: Target attacking Zombie gains indestructible until end of turn.
addCard(Zone.LIBRARY, playerA, "Accursed Horde", 1); // Creature Zombie {3}{B}
addCard(Zone.LIBRARY, playerA, "Carrion Screecher", 1); // Creature Zombie {3}{B}
//
addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion", 5); // Creature {1}{W}
//
// Karador, Ghost Chieftain costs {1} less to cast for each creature card in your graveyard.
// During each of your turns, you may cast one creature card from your graveyard.
addCard(Zone.HAND, playerA, "Karador, Ghost Chieftain");// {5}{B}{G}{W}
//
// When Gisa and Geralf enters the battlefield, put the top four cards of your library into your graveyard.
// During each of your turns, you may cast a Zombie creature card from your graveyard.
addCard(Zone.HAND, playerA, "Gisa and Geralf"); // CREATURE {2}{U}{B} (4/4)
// prepare spels with same AsThough effects and puts creature to graveyard
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Karador, Ghost Chieftain", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gisa and Geralf");
// you play any creatures due to two approve objects
checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", true);
checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Accursed Horde", true);
checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Carrion Screecher", true);
// cast zombie creature and approves by Gisa and Geralf
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Accursed Horde");
setChoice(playerA, "Gisa and Geralf"); // choose the permitting object
waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN);
// you can't cast lion due to approving object (Gisa needs zombie)
checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", true);
checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Accursed Horde", false);
checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Carrion Screecher", true);
setStrictChooseMode(true); setStrictChooseMode(true);
setStopAt(3, PhaseStep.BEGIN_COMBAT); setStopAt(3, PhaseStep.BEGIN_COMBAT);

View file

@ -0,0 +1,370 @@
package org.mage.test.cards.single.bro;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class OneWithTheMultiverseTest extends CardTestPlayerBase {
/** One with the Multiverse
* {6}{U}{U}
* Enchantment
*
* You may look at the top card of your library any time.
* You may play lands and cast spells from the top of your library.
* Once during each of your turns, you may cast a spell from your hand or the top of your library without paying its mana cost.
*/
private final String owtm = "One with the Multiverse";
private final String ogre = "Gray Ogre"; // 2/2 {2}{R}
private final String piker = "Goblin Piker"; // 2/1 {1}{R}
@Test
public void castFromTopForFree() {
setStrictChooseMode(true);
skipInitShuffling();
// The "You may look at the top card of your library any time."
// is not set up properly if starting directly on the battlefield.
// So we do cast it in those tests.
addCard(Zone.HAND, playerA, owtm);
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 8);
addCard(Zone.LIBRARY, playerA, ogre, 3);
addCard(Zone.HAND, playerA, piker, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true);
setChoice(playerA, "Without paying manacost");
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, ogre, 1);
assertPermanentCount(playerA, piker, 0);
assertLibraryCount(playerA, ogre, 2);
assertHandCount(playerA, piker, 2);
}
@Test
public void castFromHandForFree() {
setStrictChooseMode(true);
skipInitShuffling();
// The "You may look at the top card of your library any time."
// is not set up properly if starting directly on the battlefield.
// So we do cast it in those tests.
addCard(Zone.HAND, playerA, owtm);
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 8);
addCard(Zone.LIBRARY, playerA, ogre, 3);
addCard(Zone.HAND, playerA, piker, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true);
setChoice(playerA, "Without paying manacost");
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, ogre, 0);
assertPermanentCount(playerA, piker, 1);
assertLibraryCount(playerA, ogre, 3);
assertHandCount(playerA, piker, 1);
}
@Test
public void castFromTopForFreeThenNormalFromTop() {
setStrictChooseMode(true);
skipInitShuffling();
// The "You may look at the top card of your library any time."
// is not set up properly if starting directly on the battlefield.
// So we do cast it in those tests.
addCard(Zone.HAND, playerA, owtm);
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11);
addCard(Zone.LIBRARY, playerA, ogre, 3);
addCard(Zone.HAND, playerA, piker, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true);
setChoice(playerA, "Without paying manacost");
checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, ogre, 2);
assertPermanentCount(playerA, piker, 0);
assertLibraryCount(playerA, ogre, 1);
assertHandCount(playerA, piker, 2);
assertTappedCount("Volcanic Island", true, 8 + 3);
}
@Test
public void castFromTopForFreeThenNormalFromHand() {
setStrictChooseMode(true);
skipInitShuffling();
// The "You may look at the top card of your library any time."
// is not set up properly if starting directly on the battlefield.
// So we do cast it in those tests.
addCard(Zone.HAND, playerA, owtm);
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11);
addCard(Zone.LIBRARY, playerA, ogre, 3);
addCard(Zone.HAND, playerA, piker, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true);
setChoice(playerA, "Without paying manacost");
checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, ogre, 1);
assertPermanentCount(playerA, piker, 1);
assertLibraryCount(playerA, ogre, 2);
assertHandCount(playerA, piker, 1);
assertTappedCount("Volcanic Island", true, 8 + 2);
}
@Test
public void castFromHandForFreeThenNormalFromHand() {
setStrictChooseMode(true);
skipInitShuffling();
// The "You may look at the top card of your library any time."
// is not set up properly if starting directly on the battlefield.
// So we do cast it in those tests.
addCard(Zone.HAND, playerA, owtm);
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11);
addCard(Zone.LIBRARY, playerA, ogre, 3);
addCard(Zone.HAND, playerA, piker, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true);
setChoice(playerA, "Without paying manacost");
checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, ogre, 0);
assertPermanentCount(playerA, piker, 2);
assertLibraryCount(playerA, ogre, 3);
assertHandCount(playerA, piker, 0);
assertTappedCount("Volcanic Island", true, 8 + 2);
}
@Test
public void castFromHandForFreeThenNormalFromTop() {
setStrictChooseMode(true);
skipInitShuffling();
// The "You may look at the top card of your library any time."
// is not set up properly if starting directly on the battlefield.
// So we do cast it in those tests.
addCard(Zone.HAND, playerA, owtm);
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11);
addCard(Zone.LIBRARY, playerA, ogre, 3);
addCard(Zone.HAND, playerA, piker, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true);
setChoice(playerA, "Without paying manacost");
checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, ogre, 1);
assertPermanentCount(playerA, piker, 1);
assertLibraryCount(playerA, ogre, 2);
assertHandCount(playerA, piker, 1);
assertTappedCount("Volcanic Island", true, 8 + 3);
}
@Test
public void castNormalFromTopThenFreeFromHand() {
setStrictChooseMode(true);
skipInitShuffling();
// The "You may look at the top card of your library any time."
// is not set up properly if starting directly on the battlefield.
// So we do cast it in those tests.
addCard(Zone.HAND, playerA, owtm);
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11);
addCard(Zone.LIBRARY, playerA, ogre, 3);
addCard(Zone.HAND, playerA, piker, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true);
setChoice(playerA, owtm);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true);
setChoice(playerA, "Without paying manacost");
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, ogre, 1);
assertPermanentCount(playerA, piker, 1);
assertLibraryCount(playerA, ogre, 2);
assertHandCount(playerA, piker, 1);
assertTappedCount("Volcanic Island", true, 8 + 3);
}
@Test
public void castNormalFromTopThenFreeFromTop() {
setStrictChooseMode(true);
skipInitShuffling();
// The "You may look at the top card of your library any time."
// is not set up properly if starting directly on the battlefield.
// So we do cast it in those tests.
addCard(Zone.HAND, playerA, owtm);
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11);
addCard(Zone.LIBRARY, playerA, ogre, 3);
addCard(Zone.HAND, playerA, piker, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true);
setChoice(playerA, owtm);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true);
setChoice(playerA, "Without paying manacost");
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, ogre, 2);
assertPermanentCount(playerA, piker, 0);
assertLibraryCount(playerA, ogre, 1);
assertHandCount(playerA, piker, 2);
assertTappedCount("Volcanic Island", true, 8 + 3);
}
@Test
public void castNormalFromHandThenFreeFromHand() {
setStrictChooseMode(true);
skipInitShuffling();
// The "You may look at the top card of your library any time."
// is not set up properly if starting directly on the battlefield.
// So we do cast it in those tests.
addCard(Zone.HAND, playerA, owtm);
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11);
addCard(Zone.LIBRARY, playerA, ogre, 3);
addCard(Zone.HAND, playerA, piker, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true);
setChoice(playerA, piker);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true);
setChoice(playerA, "Without paying manacost");
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, ogre, 0);
assertPermanentCount(playerA, piker, 2);
assertLibraryCount(playerA, ogre, 3);
assertHandCount(playerA, piker, 0);
assertTappedCount("Volcanic Island", true, 8 + 2);
}
@Test
public void castNormalFromHandThenFreeFromTop() {
setStrictChooseMode(true);
skipInitShuffling();
// The "You may look at the top card of your library any time."
// is not set up properly if starting directly on the battlefield.
// So we do cast it in those tests.
addCard(Zone.HAND, playerA, owtm);
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11);
addCard(Zone.LIBRARY, playerA, ogre, 3);
addCard(Zone.HAND, playerA, piker, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true);
setChoice(playerA, piker);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true);
checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true);
setChoice(playerA, "Without paying manacost");
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false);
checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, ogre, 1);
assertPermanentCount(playerA, piker, 1);
assertLibraryCount(playerA, ogre, 2);
assertHandCount(playerA, piker, 1);
assertTappedCount("Volcanic Island", true, 8 + 2);
}
}

View file

@ -46,6 +46,7 @@ public class ValkiGodOfLiesTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2: Exile the top card of each player's library."); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2: Exile the top card of each player's library.");
playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Plains"); playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Plains");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Ephemerate", "Grizzly Bears"); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Ephemerate", "Grizzly Bears");
setChoice(playerA, "Emblem Tibalt");
setStrictChooseMode(true); setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);

View file

@ -3161,22 +3161,22 @@ public class TestPlayer implements Player {
} }
@Override @Override
public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs, MageIdentifier identifier) {
computerPlayer.setCastSourceIdWithAlternateMana(sourceId, manaCosts, costs); computerPlayer.setCastSourceIdWithAlternateMana(sourceId, manaCosts, costs, identifier);
} }
@Override @Override
public Set<UUID> getCastSourceIdWithAlternateMana() { public Map<UUID, Set<MageIdentifier>> getCastSourceIdWithAlternateMana() {
return computerPlayer.getCastSourceIdWithAlternateMana(); return computerPlayer.getCastSourceIdWithAlternateMana();
} }
@Override @Override
public Map<UUID, ManaCosts<ManaCost>> getCastSourceIdManaCosts() { public Map<UUID, Map<MageIdentifier,ManaCosts<ManaCost>>> getCastSourceIdManaCosts() {
return computerPlayer.getCastSourceIdManaCosts(); return computerPlayer.getCastSourceIdManaCosts();
} }
@Override @Override
public Map<UUID, Costs<Cost>> getCastSourceIdCosts() { public Map<UUID, Map<MageIdentifier,Costs<Cost>>> getCastSourceIdCosts() {
return computerPlayer.getCastSourceIdCosts(); return computerPlayer.getCastSourceIdCosts();
} }

View file

@ -1,6 +1,7 @@
package org.mage.test.stub; package org.mage.test.stub;
import mage.ApprovingObject; import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject; import mage.MageObject;
import mage.Mana; import mage.Mana;
import mage.abilities.*; import mage.abilities.*;
@ -1270,22 +1271,22 @@ public class PlayerStub implements Player {
} }
@Override @Override
public Set<UUID> getCastSourceIdWithAlternateMana() { public Map<UUID, Set<MageIdentifier>> getCastSourceIdWithAlternateMana() {
return null; return null;
} }
@Override @Override
public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs) { public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs, MageIdentifier identifier) {
} }
@Override @Override
public Map<UUID, Costs<Cost>> getCastSourceIdCosts() { public Map<UUID, Map<MageIdentifier,Costs<Cost>>> getCastSourceIdCosts() {
return null; return null;
} }
@Override @Override
public Map<UUID, ManaCosts<ManaCost>> getCastSourceIdManaCosts() { public Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> getCastSourceIdManaCosts() {
return null; return null;
} }

View file

@ -4,9 +4,21 @@ package mage;
* Used to identify specific actions/events and to be able to assign them to the * Used to identify specific actions/events and to be able to assign them to the
* correct watcher or other processing. * correct watcher or other processing.
* *
* @author LevelX2 * @author LevelX2, Susucr
*/ */
public enum MageIdentifier { public enum MageIdentifier {
// No special behavior. Cleaner than null as a default.
Default,
// -------------------------------- //
// spell cast watchers //
// -------------------------------- //
//
// All those are used by a watcher to track spells cast using a matching MageIdentifier way.
//
// e.g. [[Johann, Apprentice Sorcerer]]
// "Once each turn, you may cast an instant or sorcery spell from the top of your library."
//
CastFromGraveyardOnceWatcher, CastFromGraveyardOnceWatcher,
CemeteryIlluminatorWatcher, CemeteryIlluminatorWatcher,
GisaAndGeralfWatcher, GisaAndGeralfWatcher,
@ -19,7 +31,65 @@ public enum MageIdentifier {
WishWatcher, WishWatcher,
GlimpseTheCosmosWatcher, GlimpseTheCosmosWatcher,
SerraParagonWatcher, SerraParagonWatcher,
OneWithTheMultiverseWatcher, OneWithTheMultiverseWatcher("Without paying manacost"),
JohannApprenticeSorcererWatcher, JohannApprenticeSorcererWatcher,
KaghaShadowArchdruidWatcher KaghaShadowArchdruidWatcher,
CourtOfLocthwainWatcher("Without paying manacost"),
// ----------------------------//
// alternate casts //
// ----------------------------//
//
// All those are used to link (cost) modification only when cast
// using an AsThough with the matching MageIdentifier.
//
// e.g. [[Bolas's Citadel]]
// """
// You may look at the top card of your library any time.
//
// You may play lands and cast spells from the top of your library.
// If you cast a spell this way, pay life equal to its mana value rather than pay its mana cost.
// """
//
// If there are other ways to cast from the top of the library, then the MageIdentifier being different
// means that the alternate cast won't apply to the other ways to cast.
BolassCitadelAlternateCast,
RisenExectutionerAlternateCast,
DemilichAlternateCast,
DemonicEmbraceAlternateCast,
FalcoSparaPactweaverAlternateCast,
HelbruteAlternateCast,
MaestrosAscendencyAlternateCast,
NashiMoonSagesScionAlternateCast,
RafinnesGuidanceAlternateCast,
RonaSheoldredsFaithfulAlternateCast,
ScourgeOfNelTothAlternateCast,
SqueeDubiousMonarchAlternateCast,
WorldheartPhoenixAlternateCast,
XandersPactAlternateCast;
/**
* Additional text if there is need to differentiate two very similar effects
* from the same source in the UI.
* See [[Court of Lochtwain]] for an example.
* """
* At the beginning of your upkeep, exile the top card of target opponents library.
* You may play that card for as long as it remains exiled, and mana of any type can be spent to cast it.
* If you're the monarch, until end of turn, you may cast a spell from among cards exiled with
* Court of Locthwain without paying its mana cost.
* """
*/
private final String additionalText;
MageIdentifier() {
this("");
}
MageIdentifier(String additionalText) {
this.additionalText = additionalText;
}
public String getAdditionalText() {
return this.additionalText;
}
} }

View file

@ -79,7 +79,7 @@ public abstract class AbilityImpl implements Ability {
protected List<Hint> hints = new ArrayList<>(); protected List<Hint> hints = new ArrayList<>();
protected List<CardIcon> icons = new ArrayList<>(); protected List<CardIcon> icons = new ArrayList<>();
protected Outcome customOutcome = null; // uses for AI decisions instead effects protected Outcome customOutcome = null; // uses for AI decisions instead effects
protected MageIdentifier identifier; // used to identify specific ability (e.g. to match with corresponding watcher) protected MageIdentifier identifier = MageIdentifier.Default; // used to identify specific ability (e.g. to match with corresponding watcher)
protected String appendToRule = null; protected String appendToRule = null;
protected int sourcePermanentTransformCount = 0; protected int sourcePermanentTransformCount = 0;

View file

@ -7,41 +7,57 @@ import mage.constants.TargetController;
import mage.constants.TimingRule; import mage.constants.TimingRule;
import mage.game.Game; import mage.game.Game;
import java.util.UUID; import java.util.*;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, Susucr
*/ */
public interface ActivatedAbility extends Ability { public interface ActivatedAbility extends Ability {
final class ActivationStatus { final class ActivationStatus {
private final boolean canActivate; // Expected to not be modified after creation.
private final ApprovingObject approvingObject; private final Set<ApprovingObject> approvingObjects;
public ActivationStatus(boolean canActivate, ApprovingObject approvingObject) { // If true, the Activation Status will not check if there is an approvingObject.
this.canActivate = canActivate; private final boolean forcedCanActivate;
this.approvingObject = approvingObject;
public ActivationStatus(ApprovingObject approvingObject) {
this.forcedCanActivate = false;
this.approvingObjects = Collections.singleton(approvingObject);
}
public ActivationStatus(Set<ApprovingObject> approvingObjects) {
this(false, approvingObjects);
}
private ActivationStatus(boolean forcedCanActivate, Set<ApprovingObject> approvingObjects) {
this.forcedCanActivate = forcedCanActivate;
this.approvingObjects = new HashSet<>();
this.approvingObjects.addAll(approvingObjects);
} }
public boolean canActivate() { public boolean canActivate() {
return canActivate; return forcedCanActivate || !approvingObjects.isEmpty();
}
public ApprovingObject getApprovingObject() {
return approvingObject;
}
public static ActivationStatus getFalse() {
return new ActivationStatus(false, null);
} }
/** /**
* @param approvingObjectAbility ability that allows to activate/use current ability * @return the set of all approving objects for that ActivationStatus.
* That Set is readonly in spirit, as there might be different parts
* of the engine retrieving info from it.
*/ */
public static ActivationStatus getTrue(Ability approvingObjectAbility, Game game) { public Set<ApprovingObject> getApprovingObjects() {
ApprovingObject approvingObject = approvingObjectAbility == null ? null : new ApprovingObject(approvingObjectAbility, game); return approvingObjects;
return new ActivationStatus(true, approvingObject); }
private static final ActivationStatus falseInstance = new ActivationStatus(Collections.emptySet());
public static ActivationStatus getFalse() {
return falseInstance;
}
public static ActivationStatus withoutApprovingObject(boolean forcedCanActivate) {
return new ActivationStatus(forcedCanActivate, Collections.emptySet());
} }
} }

View file

@ -17,6 +17,7 @@ import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil; import mage.util.CardUtil;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
/** /**
@ -179,15 +180,16 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
// timing check // timing check
//20091005 - 602.5d/602.5e //20091005 - 602.5d/602.5e
boolean asInstant; Set<ApprovingObject> approvingObjects = game
ApprovingObject approvingObject = game.getContinuousEffects() .getContinuousEffects()
.asThough(sourceId, .asThough(sourceId,
AsThoughEffectType.ACTIVATE_AS_INSTANT, AsThoughEffectType.ACTIVATE_AS_INSTANT,
this, this,
controllerId, controllerId,
game); game
asInstant = approvingObject != null; );
asInstant |= (timing == TimingRule.INSTANT); boolean asInstant = !approvingObjects.isEmpty()
|| (timing == TimingRule.INSTANT);
if (!asInstant && !game.canPlaySorcery(playerId)) { if (!asInstant && !game.canPlaySorcery(playerId)) {
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();
} }
@ -204,7 +206,13 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
// game.inCheckPlayableState() can't be a help here cause some cards checking activating status, // game.inCheckPlayableState() can't be a help here cause some cards checking activating status,
// activatorId must be removed // activatorId must be removed
this.activatorId = playerId; this.activatorId = playerId;
return new ActivationStatus(true, approvingObject);
if (approvingObjects.isEmpty()) {
return ActivationStatus.withoutApprovingObject(true);
}
else {
return new ActivationStatus(approvingObjects);
}
} }
@Override @Override

View file

@ -1,11 +1,13 @@
package mage.abilities; package mage.abilities;
import mage.ApprovingObject; import mage.ApprovingObject;
import mage.cards.Card;
import mage.constants.AbilityType; import mage.constants.AbilityType;
import mage.constants.AsThoughEffectType; import mage.constants.AsThoughEffectType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
/** /**
@ -33,15 +35,35 @@ public class PlayLandAbility extends ActivatedAbilityImpl {
// no super.canActivate() call // no super.canActivate() call
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (!controlsAbility(playerId, game) && null == approvingObject) { if (!controlsAbility(playerId, game) && approvingObjects.isEmpty()) {
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();
} }
//20091005 - 114.2a //20091005 - 114.2a
return new ActivationStatus(game.isActivePlayer(playerId) if(!game.isActivePlayer(playerId)
&& game.getPlayer(playerId).canPlayLand() || !game.getPlayer(playerId).canPlayLand()
&& game.canPlaySorcery(playerId), || !game.canPlaySorcery(playerId)) {
approvingObject); return ActivationStatus.getFalse();
}
// TODO: this check may not be required, but removing it require more investigation.
// As of now it is only a way for One with the Multiverse to work.
if (!approvingObjects.isEmpty()) {
Card card = game.getCard(sourceId);
Zone zone = game.getState().getZone(sourceId);
if(card != null && card.isOwnedBy(playerId) && Zone.HAND.match(zone)) {
// Regular casting, to be an alternative to the AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE from hand (e.g. One with the Multiverse):
approvingObjects.add(new ApprovingObject(this, game));
}
}
if(approvingObjects.isEmpty()) {
return ActivationStatus.withoutApprovingObject(true);
}
else {
return new ActivationStatus(approvingObjects);
}
} }
@Override @Override

View file

@ -1,6 +1,7 @@
package mage.abilities; package mage.abilities;
import mage.ApprovingObject; import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject; import mage.MageObject;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCost; import mage.abilities.costs.VariableCost;
@ -45,6 +46,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
this.spellAbilityType = spellAbilityType; this.spellAbilityType = spellAbilityType;
this.spellAbilityCastMode = spellAbilityCastMode; this.spellAbilityCastMode = spellAbilityCastMode;
this.addManaCost(cost); this.addManaCost(cost);
this.setIdentifier(MageIdentifier.Default);
setSpellName(); setSpellName();
} }
@ -97,7 +99,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
} }
} }
return null != game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game) // check this first to allow Offering in main phase return !game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game).isEmpty() // check this first to allow Offering in main phase
|| timing == TimingRule.INSTANT || timing == TimingRule.INSTANT
|| object.isInstant(game) || object.isInstant(game)
|| object.hasAbility(FlashAbility.getInstance(), game) || object.hasAbility(FlashAbility.getInstance(), game)
@ -116,13 +118,13 @@ public class SpellAbility extends ActivatedAbilityImpl {
} }
// play from not own hand // play from not own hand
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (approvingObject == null && getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) { if (approvingObjects.isEmpty() && getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) {
// allowed to cast adventures from non-hand? // allowed to cast adventures from non-hand?
approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
} }
if (approvingObject == null) { if (approvingObjects.isEmpty()) {
Card card = game.getCard(sourceId); Card card = game.getCard(sourceId);
if (!(card != null && card.isOwnedBy(playerId))) { if (!(card != null && card.isOwnedBy(playerId))) {
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();
@ -141,12 +143,26 @@ public class SpellAbility extends ActivatedAbilityImpl {
} }
} }
// TODO: this check may not be required, but removing it require more investigation.
// As of now it is only a way for One with the Multiverse to work.
if (!approvingObjects.isEmpty()) {
Card card = game.getCard(sourceId);
Zone zone = game.getState().getZone(sourceId);
if(card != null && card.isOwnedBy(playerId) && Zone.HAND.match(zone)) {
// Regular casting, to be an alternative to the AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE from hand (e.g. One with the Multiverse):
approvingObjects.add(new ApprovingObject(this, game));
}
}
// no mana restrict // no mana restrict
// Alternate spell abilities (Flashback, Overload) can't be cast with no mana to pay option // Alternate spell abilities (Flashback, Overload) can't be cast with no mana to pay option
if (getSpellAbilityType() == SpellAbilityType.BASE_ALTERNATE) { if (getSpellAbilityType() == SpellAbilityType.BASE_ALTERNATE) {
Player player = game.getPlayer(playerId); Player player = game.getPlayer(playerId);
if (player != null if (player != null
&& player.getCastSourceIdWithAlternateMana().contains(getSourceId())) { && player.getCastSourceIdWithAlternateMana()
.getOrDefault(getSourceId(), Collections.emptySet())
.contains(MageIdentifier.Default)
) {
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();
} }
} }
@ -159,13 +175,20 @@ public class SpellAbility extends ActivatedAbilityImpl {
// fused can be called from hand only, so not permitting object allows or other zones checks // fused can be called from hand only, so not permitting object allows or other zones checks
// see https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/251926-snapcaster-mage-and-fuse // see https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/251926-snapcaster-mage-and-fuse
if (game.getState().getZone(splitCard.getId()) == Zone.HAND) { if (game.getState().getZone(splitCard.getId()) == Zone.HAND) {
return new ActivationStatus(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId) return ActivationStatus.withoutApprovingObject(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)
&& splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId), null); && splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId));
} }
} }
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();
} else { } else {
return new ActivationStatus(canChooseTarget(game, playerId), approvingObject); if(canChooseTarget(game, playerId)) {
if(approvingObjects == null || approvingObjects.isEmpty()) {
return ActivationStatus.withoutApprovingObject(true);
}
else {
return new ActivationStatus(approvingObjects);
}
}
} }
} }
} }

View file

@ -46,7 +46,7 @@ class CastFromGraveyardOnceEffect extends AsThoughEffectImpl {
private final FilterCard filter; private final FilterCard filter;
CastFromGraveyardOnceEffect(FilterCard filter, String text) { CastFromGraveyardOnceEffect(FilterCard filter, String text) {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
this.filter = filter; this.filter = filter;
this.staticText = text; this.staticText = text;
} }

View file

@ -1,5 +1,6 @@
package mage.abilities.common; package mage.abilities.common;
import mage.ApprovingObject;
import mage.abilities.ActivatedAbilityImpl; import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.effects.common.PassEffect; import mage.abilities.effects.common.PassEffect;
import mage.constants.Zone; import mage.constants.Zone;
@ -30,7 +31,7 @@ public class PassAbility extends ActivatedAbilityImpl {
@Override @Override
public ActivationStatus canActivate(UUID playerId, Game game) { public ActivationStatus canActivate(UUID playerId, Game game) {
return ActivationStatus.getTrue(this, game); return new ActivationStatus(new ApprovingObject(this, game));
} }
@Override @Override

View file

@ -37,7 +37,7 @@ public class TapSourceCost extends CostImpl {
Permanent permanent = game.getPermanent(source.getSourceId()); Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null) { if (permanent != null) {
return !permanent.isTapped() return !permanent.isTapped()
&& (permanent.canTap(game) || null != game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game)); && (permanent.canTap(game) || !game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game).isEmpty());
} }
return false; return false;
} }

View file

@ -36,7 +36,7 @@ public class UntapSourceCost extends CostImpl {
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
Permanent permanent = game.getPermanent(source.getSourceId()); Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null) { if (permanent != null) {
return permanent.isTapped() && (permanent.canTap(game) || null != game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game)); return permanent.isTapped() && (permanent.canTap(game) || game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game).isEmpty());
} }
return false; return false;
} }

View file

@ -41,6 +41,4 @@ public interface AsThoughEffect extends ContinuousEffect {
@Override @Override
AsThoughEffect copy(); AsThoughEffect copy();
boolean isConsumable();
} }

View file

@ -1,5 +1,6 @@
package mage.abilities.effects; package mage.abilities.effects;
import mage.MageIdentifier;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.ActivatedAbility; import mage.abilities.ActivatedAbility;
import mage.cards.Card; import mage.cards.Card;
@ -19,23 +20,16 @@ import mage.cards.AdventureCard;
public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements AsThoughEffect { public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements AsThoughEffect {
protected AsThoughEffectType type; protected AsThoughEffectType type;
boolean consumable;
public AsThoughEffectImpl(AsThoughEffectType type, Duration duration, Outcome outcome) { public AsThoughEffectImpl(AsThoughEffectType type, Duration duration, Outcome outcome) {
this(type, duration, outcome, false);
}
public AsThoughEffectImpl(AsThoughEffectType type, Duration duration, Outcome outcome, boolean consumable) {
super(duration, outcome); super(duration, outcome);
this.type = type; this.type = type;
this.effectType = EffectType.ASTHOUGH; this.effectType = EffectType.ASTHOUGH;
this.consumable = consumable;
} }
protected AsThoughEffectImpl(final AsThoughEffectImpl effect) { protected AsThoughEffectImpl(final AsThoughEffectImpl effect) {
super(effect); super(effect);
this.type = effect.type; this.type = effect.type;
this.consumable = effect.consumable;
} }
@Override @Override
@ -84,6 +78,10 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements
* @return * @return
*/ */
protected boolean allowCardToPlayWithoutMana(UUID objectId, Ability source, UUID affectedControllerId, Game game) { protected boolean allowCardToPlayWithoutMana(UUID objectId, Ability source, UUID affectedControllerId, Game game) {
return allowCardToPlayWithoutMana(objectId, source, affectedControllerId, MageIdentifier.Default, game);
}
protected boolean allowCardToPlayWithoutMana(UUID objectId, Ability source, UUID affectedControllerId, MageIdentifier identifier, Game game){
Player player = game.getPlayer(affectedControllerId); Player player = game.getPlayer(affectedControllerId);
Card card = game.getCard(objectId); Card card = game.getCard(objectId);
if (card == null || player == null) { if (card == null || player == null) {
@ -92,33 +90,27 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements
if (!card.isLand(game)) { if (!card.isLand(game)) {
if (card instanceof SplitCard) { if (card instanceof SplitCard) {
Card leftCard = ((SplitCard) card).getLeftHalfCard(); Card leftCard = ((SplitCard) card).getLeftHalfCard();
player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts()); player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts(), identifier);
Card rightCard = ((SplitCard) card).getRightHalfCard(); Card rightCard = ((SplitCard) card).getRightHalfCard();
player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts()); player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier);
} else if (card instanceof ModalDoubleFacedCard) { } else if (card instanceof ModalDoubleFacedCard) {
Card leftCard = ((ModalDoubleFacedCard) card).getLeftHalfCard(); Card leftCard = ((ModalDoubleFacedCard) card).getLeftHalfCard();
Card rightCard = ((ModalDoubleFacedCard) card).getRightHalfCard(); Card rightCard = ((ModalDoubleFacedCard) card).getRightHalfCard();
// some MDFC's are land. IE: sea gate restoration // some MDFC's are land. IE: sea gate restoration
if (!leftCard.isLand(game)) { if (!leftCard.isLand(game)) {
player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts()); player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts(), identifier);
} }
if (!rightCard.isLand(game)) { if (!rightCard.isLand(game)) {
player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts()); player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier);
} }
} else if (card instanceof AdventureCard) { } else if (card instanceof AdventureCard) {
Card creatureCard = card.getMainCard(); Card creatureCard = card.getMainCard();
Card spellCard = ((AdventureCard) card).getSpellCard(); Card spellCard = ((AdventureCard) card).getSpellCard();
player.setCastSourceIdWithAlternateMana(creatureCard.getId(), null, creatureCard.getSpellAbility().getCosts()); player.setCastSourceIdWithAlternateMana(creatureCard.getId(), null, creatureCard.getSpellAbility().getCosts(), identifier);
player.setCastSourceIdWithAlternateMana(spellCard.getId(), null, spellCard.getSpellAbility().getCosts()); player.setCastSourceIdWithAlternateMana(spellCard.getId(), null, spellCard.getSpellAbility().getCosts(), identifier);
} }
player.setCastSourceIdWithAlternateMana(objectId, null, card.getSpellAbility().getCosts()); player.setCastSourceIdWithAlternateMana(objectId, null, card.getSpellAbility().getCosts(), identifier);
} }
return true; return true;
} }
@Override
public boolean isConsumable() {
return consumable;
}
} }

View file

@ -1,6 +1,7 @@
package mage.abilities.effects; package mage.abilities.effects;
import mage.ApprovingObject; import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.MageSingleton; import mage.abilities.MageSingleton;
@ -506,9 +507,10 @@ public class ContinuousEffects implements Serializable {
* @param affectedAbility null if check full object or ability if check only one ability from that object * @param affectedAbility null if check full object or ability if check only one ability from that object
* @param controllerId * @param controllerId
* @param game * @param game
* @return sourceId of the permitting effect if any exists otherwise returns null * @return Set of all the ApprovingObject related to that asThough.
*/ */
public ApprovingObject asThough(UUID objectId, AsThoughEffectType type, Ability affectedAbility, UUID controllerId, Game game) { public Set<ApprovingObject> asThough(UUID objectId, AsThoughEffectType type, Ability affectedAbility, UUID controllerId, Game game) {
Set<ApprovingObject> possibleApprovingObjects = new HashSet<>();
// usage check: effect must apply for specific ability only, not to full object (example: PLAY_FROM_NOT_OWN_HAND_ZONE) // usage check: effect must apply for specific ability only, not to full object (example: PLAY_FROM_NOT_OWN_HAND_ZONE)
if (type.needAffectedAbility() && affectedAbility == null) { if (type.needAffectedAbility() && affectedAbility == null) {
@ -553,18 +555,13 @@ public class ContinuousEffects implements Serializable {
idToCheck = objectId; idToCheck = objectId;
} }
Set<ApprovingObject> possibleApprovingObjects = new HashSet<>();
for (AsThoughEffect effect : asThoughEffectsList) { for (AsThoughEffect effect : asThoughEffectsList) {
Set<Ability> abilities = asThoughEffectsMap.get(type).getAbility(effect.getId()); Set<Ability> abilities = asThoughEffectsMap.get(type).getAbility(effect.getId());
for (Ability ability : abilities) { for (Ability ability : abilities) {
if (affectedAbility == null) { if (affectedAbility == null) {
// applies to full object (one effect can be used in multiple abilities) // applies to full object (one effect can be used in multiple abilities)
if (effect.applies(idToCheck, ability, controllerId, game)) { if (effect.applies(idToCheck, ability, controllerId, game)) {
if (effect.isConsumable() && !game.inCheckPlayableState()) { possibleApprovingObjects.add(new ApprovingObject(ability, game));
possibleApprovingObjects.add(new ApprovingObject(ability, game));
} else {
return new ApprovingObject(ability, game);
}
} }
} else { } else {
// applies to one affected ability // applies to one affected ability
@ -575,46 +572,13 @@ public class ContinuousEffects implements Serializable {
} }
if (effect.applies(idToCheck, affectedAbility, ability, game, controllerId)) { if (effect.applies(idToCheck, affectedAbility, ability, game, controllerId)) {
if (effect.isConsumable() && !game.inCheckPlayableState()) { possibleApprovingObjects.add(new ApprovingObject(ability, game));
possibleApprovingObjects.add(new ApprovingObject(ability, game));
} else {
return new ApprovingObject(ability, game);
}
} }
} }
} }
} }
if (possibleApprovingObjects.size() == 1) {
return possibleApprovingObjects.iterator().next();
} else if (possibleApprovingObjects.size() > 1) {
// Select the ability that you use to permit the action
Map<String, String> keyChoices = new HashMap<>();
for (ApprovingObject approvingObject : possibleApprovingObjects) {
MageObject mageObject = game.getObject(approvingObject.getApprovingAbility().getSourceId());
String choiceKey = approvingObject.getApprovingAbility().getId().toString();
String choiceValue;
if (mageObject == null) {
choiceValue = approvingObject.getApprovingAbility().getRule();
} else {
choiceValue = mageObject.getIdName() + ": " + approvingObject.getApprovingAbility().getRule(mageObject.getName());
}
keyChoices.put(choiceKey, choiceValue);
}
Choice choicePermitting = new ChoiceImpl(true);
choicePermitting.setMessage("Choose the permitting object");
choicePermitting.setKeyChoices(keyChoices);
Player player = game.getPlayer(controllerId);
player.choose(Outcome.Detriment, choicePermitting, game);
for (ApprovingObject approvingObject : possibleApprovingObjects) {
if (approvingObject.getApprovingAbility().getId().toString().equals(choicePermitting.getChoiceKey())) {
return approvingObject;
}
}
}
} }
return null; return possibleApprovingObjects;
} }
/** /**

View file

@ -1,18 +1,14 @@
package mage.abilities.effects.common.asthought; package mage.abilities.effects.common.asthought;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.AsThoughEffectImpl;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.AsThoughEffectType; import mage.constants.*;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.filter.FilterCard; import mage.filter.FilterCard;
import mage.game.Game; import mage.game.Game;
import java.util.UUID;
/** /**
* @author LevelX2 * @author LevelX2
*/ */
@ -65,7 +61,7 @@ public class PlayFromNotOwnHandZoneAllEffect extends AsThoughEffectImpl {
} }
break; break;
} }
return !onlyOwnedCards || card.getOwnerId().equals(source.getControllerId()) return (!onlyOwnedCards || card.getOwnerId().equals(source.getControllerId()))
&& filter.match(card, game) && filter.match(card, game)
&& game.getState().getZone(card.getId()).match(fromZone); && game.getState().getZone(card.getId()).match(fromZone);
} }

View file

@ -46,7 +46,7 @@ public class EchoEffect extends OneShotEffect {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null if (controller != null
&& source.getSourceObjectIfItStillExists(game) != null) { && source.getSourceObjectIfItStillExists(game) != null) {
if (game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.PAY_0_ECHO, source, source.getControllerId(), game) != null) { if (!game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.PAY_0_ECHO, source, source.getControllerId(), game).isEmpty()) {
Cost altCost = new GenericManaCost(0); Cost altCost = new GenericManaCost(0);
if (controller.chooseUse(Outcome.Benefit, "Pay {0} instead of the echo cost?", source, game)) { if (controller.chooseUse(Outcome.Benefit, "Pay {0} instead of the echo cost?", source, game)) {
altCost.clearPaid(); altCost.clearPaid();

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.Mana; import mage.Mana;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCost;
@ -55,7 +56,7 @@ public class EmergeAbility extends SpellAbility {
new FilterControlledCreaturePermanent(), this.getControllerId(), this, game)) { new FilterControlledCreaturePermanent(), this.getControllerId(), this, game)) {
ManaCost costToPay = CardUtil.reduceCost(emergeCost.copy(), creature.getManaValue()); ManaCost costToPay = CardUtil.reduceCost(emergeCost.copy(), creature.getManaValue());
if (costToPay.canPay(this, this, this.getControllerId(), game)) { if (costToPay.canPay(this, this, this.getControllerId(), game)) {
return ActivationStatus.getTrue(this, game); return new ActivationStatus(new ApprovingObject(this, game));
} }
} }
} }

View file

@ -68,7 +68,7 @@ class FlyingEffect extends RestrictionEffect implements MageSingleton {
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) { public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
return blocker.getAbilities().containsKey(FlyingAbility.getInstance().getId()) return blocker.getAbilities().containsKey(FlyingAbility.getInstance().getId())
|| blocker.getAbilities().containsKey(ReachAbility.getInstance().getId()) || blocker.getAbilities().containsKey(ReachAbility.getInstance().getId())
|| (null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_DRAGON, null, blocker.getControllerId(), game) || (!game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_DRAGON, null, blocker.getControllerId(), game).isEmpty()
&& attacker.hasSubtype(SubType.DRAGON, game)); && attacker.hasSubtype(SubType.DRAGON, game));
} }

View file

@ -65,18 +65,18 @@ class LandwalkEffect extends RestrictionEffect {
@Override @Override
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) { public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
if (game.getBattlefield().contains(filter, blocker.getControllerId(), source, game, 1) if (game.getBattlefield().contains(filter, blocker.getControllerId(), source, game, 1)
&& null == game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_LANDWALK, null, blocker.getControllerId(), game)) { && game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_LANDWALK, null, blocker.getControllerId(), game).isEmpty()) {
switch (filter.getMessage()) { switch (filter.getMessage()) {
case "plains": case "plains":
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_PLAINSWALK, null, blocker.getControllerId(), game); return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_PLAINSWALK, null, blocker.getControllerId(), game).isEmpty();
case "island": case "island":
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_ISLANDWALK, null, blocker.getControllerId(), game); return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_ISLANDWALK, null, blocker.getControllerId(), game).isEmpty();
case "swamp": case "swamp":
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SWAMPWALK, null, blocker.getControllerId(), game); return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SWAMPWALK, null, blocker.getControllerId(), game).isEmpty();
case "mountain": case "mountain":
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_MOUNTAINWALK, null, blocker.getControllerId(), game); return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_MOUNTAINWALK, null, blocker.getControllerId(), game).isEmpty();
case "forest": case "forest":
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_FORESTWALK, null, blocker.getControllerId(), game); return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_FORESTWALK, null, blocker.getControllerId(), game).isEmpty();
default: default:
return false; return false;
} }

View file

@ -70,7 +70,7 @@ class ShadowEffect extends RestrictionEffect implements MageSingleton {
@Override @Override
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) { public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
return blocker.getAbilities().containsKey(ShadowAbility.getInstance().getId()) return blocker.getAbilities().containsKey(ShadowAbility.getInstance().getId())
|| null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, null, blocker.getControllerId(), game); || !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, null, blocker.getControllerId(), game).isEmpty();
} }
@Override @Override

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCost;
import mage.abilities.dynamicvalue.common.OpponentsLostLifeCount; import mage.abilities.dynamicvalue.common.OpponentsLostLifeCount;
@ -48,7 +49,7 @@ public class SpectacleAbility extends SpellAbility {
public ActivationStatus canActivate(UUID playerId, Game game) { public ActivationStatus canActivate(UUID playerId, Game game) {
if (OpponentsLostLifeCount.instance.calculate(game, playerId) > 0 if (OpponentsLostLifeCount.instance.calculate(game, playerId) > 0
&& super.canActivate(playerId, game).canActivate()) { && super.canActivate(playerId, game).canActivate()) {
return ActivationStatus.getTrue(this, game); return new ActivationStatus(new ApprovingObject(this, game));
} }
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();
} }

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.cards.Card; import mage.cards.Card;
@ -54,7 +55,7 @@ public class SurgeAbility extends SpellAbility {
if (!player.hasOpponent(playerToCheckId, game)) { if (!player.hasOpponent(playerToCheckId, game)) {
if (watcher.getAmountOfSpellsPlayerCastOnCurrentTurn(playerToCheckId) > 0 if (watcher.getAmountOfSpellsPlayerCastOnCurrentTurn(playerToCheckId) > 0
&& super.canActivate(playerId, game).canActivate()) { && super.canActivate(playerId, game).canActivate()) {
return ActivationStatus.getTrue(this, game); return new ActivationStatus(new ApprovingObject(this, game));
} }
} }
} }

View file

@ -34,7 +34,7 @@ class BlockTappedPredicate implements Predicate<Permanent> {
@Override @Override
public boolean apply(Permanent input, Game game) { public boolean apply(Permanent input, Game game) {
return !input.isTapped() || null != game.getState().getContinuousEffects().asThough(input.getId(), AsThoughEffectType.BLOCK_TAPPED, null, input.getControllerId(), game); return !input.isTapped() || !game.getState().getContinuousEffects().asThough(input.getId(), AsThoughEffectType.BLOCK_TAPPED, null, input.getControllerId(), game).isEmpty();
} }
@Override @Override

View file

@ -174,8 +174,8 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
Player player = game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : attacker.getControllerId()); Player player = game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : attacker.getControllerId());
if ((attacker.getAbilities().containsKey(DamageAsThoughNotBlockedAbility.getInstance().getId()) && if ((attacker.getAbilities().containsKey(DamageAsThoughNotBlockedAbility.getInstance().getId()) &&
player.chooseUse(Outcome.Damage, "Have " + attacker.getLogName() + " assign damage as though it weren't blocked?", null, game)) || player.chooseUse(Outcome.Damage, "Have " + attacker.getLogName() + " assign damage as though it weren't blocked?", null, game)) ||
game.getContinuousEffects().asThough(attacker.getId(), AsThoughEffectType.DAMAGE_NOT_BLOCKED, !game.getContinuousEffects().asThough(attacker.getId(), AsThoughEffectType.DAMAGE_NOT_BLOCKED,
null, attacker.getControllerId(), game) != null) { null, attacker.getControllerId(), game).isEmpty()) {
// for handling creatures like Thorn Elemental // for handling creatures like Thorn Elemental
blocked = false; blocked = false;
unblockedDamage(first, game); unblockedDamage(first, game);

View file

@ -716,7 +716,7 @@ public class GameEvent implements Serializable {
if (approvingObject == null) { if (approvingObject == null) {
return false; return false;
} }
if (identifier == null) { if (identifier.equals(MageIdentifier.Default)) {
return false; return false;
} }
return identifier.equals(approvingObject.getApprovingAbility().getIdentifier()); return identifier.equals(approvingObject.getApprovingAbility().getIdentifier());

View file

@ -1234,13 +1234,13 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) { public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) {
if (source != null) { if (source != null) {
if (abilities.containsKey(ShroudAbility.getInstance().getId())) { if (abilities.containsKey(ShroudAbility.getInstance().getId())) {
if (null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game)) { if (game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game).isEmpty()) {
return false; return false;
} }
} }
if (game.getPlayer(this.getControllerId()).hasOpponent(sourceControllerId, game) if (game.getPlayer(this.getControllerId()).hasOpponent(sourceControllerId, game)
&& null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game).isEmpty()
&& abilities.stream() && abilities.stream()
.filter(HexproofBaseAbility.class::isInstance) .filter(HexproofBaseAbility.class::isInstance)
.map(HexproofBaseAbility.class::cast) .map(HexproofBaseAbility.class::cast)
@ -1416,10 +1416,10 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
// battles can never attack // battles can never attack
return false; return false;
} }
ApprovingObject approvingObject = game.getContinuousEffects().asThough( Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(
this.objectId, AsThoughEffectType.ATTACK_AS_HASTE, null, defenderId, game this.objectId, AsThoughEffectType.ATTACK_AS_HASTE, null, defenderId, game
); );
if (hasSummoningSickness() && approvingObject == null) { if (hasSummoningSickness() && approvingObjects.isEmpty()) {
return false; return false;
} }
//20101001 - 508.1c //20101001 - 508.1c
@ -1435,7 +1435,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
} }
return !abilities.containsKey(DefenderAbility.getInstance().getId()) return !abilities.containsKey(DefenderAbility.getInstance().getId())
|| null != game.getContinuousEffects().asThough(this.objectId, AsThoughEffectType.ATTACK, null, this.getControllerId(), game); || !game.getContinuousEffects().asThough(this.objectId, AsThoughEffectType.ATTACK, null, this.getControllerId(), game).isEmpty();
} }
private boolean canAttackCheckRestrictionEffects(UUID defenderId, Game game) { private boolean canAttackCheckRestrictionEffects(UUID defenderId, Game game) {
@ -1455,7 +1455,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override @Override
public boolean canBlock(UUID attackerId, Game game) { public boolean canBlock(UUID attackerId, Game game) {
if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game) == null || isBattle(game)) { if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty() || isBattle(game)) {
return false; return false;
} }
Permanent attacker = game.getPermanent(attackerId); Permanent attacker = game.getPermanent(attackerId);
@ -1488,7 +1488,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override @Override
public boolean canBlockAny(Game game) { public boolean canBlockAny(Game game) {
if (tapped && null == game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game)) { if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty()) {
return false; return false;
} }

View file

@ -1,9 +1,6 @@
package mage.players; package mage.players;
import mage.ApprovingObject; import mage.*;
import mage.MageItem;
import mage.MageObject;
import mage.Mana;
import mage.abilities.*; import mage.abilities.*;
import mage.abilities.costs.AlternativeSourceCosts; import mage.abilities.costs.AlternativeSourceCosts;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
@ -1054,13 +1051,28 @@ public interface Player extends MageItem, Copyable<Player> {
* cost * cost
* @param costs alternate other costs you need to pay * @param costs alternate other costs you need to pay
*/ */
void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs); default void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs) {
setCastSourceIdWithAlternateMana(sourceId, manaCosts, costs, MageIdentifier.Default);
}
Set<UUID> getCastSourceIdWithAlternateMana(); /**
* If the next spell cast has the set sourceId, the spell will be cast
* without mana (null) or the mana set to manaCosts instead of its normal
* mana costs.
*
* @param sourceId the source that can be cast without mana
* @param manaCosts alternate ManaCost, null if it can be cast without mana
* cost
* @param costs alternate other costs you need to pay
* @param identifier if not using the MageIdentifier.Default, only apply the alternate mana when ApprovingSource if of that kind.
*/
void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs, MageIdentifier identifier);
Map<UUID, ManaCosts<ManaCost>> getCastSourceIdManaCosts(); Map<UUID, Set<MageIdentifier>> getCastSourceIdWithAlternateMana();
Map<UUID, Costs<Cost>> getCastSourceIdCosts(); Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> getCastSourceIdManaCosts();
Map<UUID, Map<MageIdentifier, Costs<Cost>>> getCastSourceIdCosts();
void clearCastSourceIdManaCosts(); void clearCastSourceIdManaCosts();

View file

@ -163,9 +163,12 @@ public abstract class PlayerImpl implements Player, Serializable {
// indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs) // indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs)
// support multiple cards with alternative mana cost // support multiple cards with alternative mana cost
protected Set<UUID> castSourceIdWithAlternateMana = new HashSet<>(); //
protected Map<UUID, ManaCosts<ManaCost>> castSourceIdManaCosts = new HashMap<>(); // A card may be able to cast multiple way with multiple methods.
protected Map<UUID, Costs<Cost>> castSourceIdCosts = new HashMap<>(); // The specific MageIdentifier should be checked, before checking null as a fallback.
protected Map<UUID, Set<MageIdentifier>> castSourceIdWithAlternateMana = new HashMap<>();
protected Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> castSourceIdManaCosts = new HashMap<>();
protected Map<UUID, Map<MageIdentifier, Costs<Cost>>> castSourceIdCosts = new HashMap<>();
// indicates that the player is in mana payment phase // indicates that the player is in mana payment phase
protected boolean payManaMode = false; protected boolean payManaMode = false;
@ -279,13 +282,22 @@ public abstract class PlayerImpl implements Player, Serializable {
this.bufferTimeLeft = player.getBufferTimeLeft(); this.bufferTimeLeft = player.getBufferTimeLeft();
this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving;
this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); for (Entry<UUID, Set<MageIdentifier>> entry : player.getCastSourceIdWithAlternateMana().entrySet()) {
for (Entry<UUID, ManaCosts<ManaCost>> entry : player.getCastSourceIdManaCosts().entrySet()) { this.castSourceIdWithAlternateMana.put(entry.getKey(), (entry.getValue() == null ? null : new HashSet<>(entry.getValue())));
this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy()));
} }
for (Entry<UUID, Costs<Cost>> entry : player.getCastSourceIdCosts().entrySet()) { for (Entry<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); this.castSourceIdManaCosts.put(entry.getKey(), new HashMap<>());
for(Entry<MageIdentifier, ManaCosts<ManaCost>> subEntry : entry.getValue().entrySet()) {
this.castSourceIdManaCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
} }
for (Entry<UUID, Map<MageIdentifier, Costs<Cost>>> entry : player.getCastSourceIdCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), new HashMap<>());
for(Entry<MageIdentifier, Costs<Cost>> subEntry : entry.getValue().entrySet()) {
this.castSourceIdCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
}
this.payManaMode = player.payManaMode; this.payManaMode = player.payManaMode;
this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null; this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null;
for (Designation object : player.designations) { for (Designation object : player.designations) {
@ -364,13 +376,20 @@ public abstract class PlayerImpl implements Player, Serializable {
this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving();
this.clearCastSourceIdManaCosts(); this.clearCastSourceIdManaCosts();
this.castSourceIdWithAlternateMana.clear(); for (Entry<UUID, Set<MageIdentifier>> entry : player.getCastSourceIdWithAlternateMana().entrySet()) {
this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); this.castSourceIdWithAlternateMana.put(entry.getKey(), (entry.getValue() == null ? null : new HashSet<>(entry.getValue())));
for (Entry<UUID, ManaCosts<ManaCost>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy()));
} }
for (Entry<UUID, Costs<Cost>> entry : player.getCastSourceIdCosts().entrySet()) { for (Entry<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); this.castSourceIdManaCosts.put(entry.getKey(), new HashMap<>());
for(Entry<MageIdentifier, ManaCosts<ManaCost>> subEntry : entry.getValue().entrySet()) {
this.castSourceIdManaCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
}
for (Entry<UUID, Map<MageIdentifier, Costs<Cost>>> entry : player.getCastSourceIdCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), new HashMap<>());
for(Entry<MageIdentifier, Costs<Cost>> subEntry : entry.getValue().entrySet()) {
this.castSourceIdCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
} }
this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null; this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null;
@ -636,13 +655,13 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
if (source != null) { if (source != null) {
if (abilities.containsKey(ShroudAbility.getInstance().getId()) if (abilities.containsKey(ShroudAbility.getInstance().getId())
&& null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game)) { && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game).isEmpty()) {
return false; return false;
} }
if (sourceControllerId != null if (sourceControllerId != null
&& this.hasOpponent(sourceControllerId, game) && this.hasOpponent(sourceControllerId, game)
&& null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game).isEmpty()
&& abilities.stream() && abilities.stream()
.filter(HexproofBaseAbility.class::isInstance) .filter(HexproofBaseAbility.class::isInstance)
.map(HexproofBaseAbility.class::cast) .map(HexproofBaseAbility.class::cast)
@ -1080,25 +1099,37 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs) { public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs, MageIdentifier identifier) {
// cost must be copied for data consistence between game simulations // cost must be copied for data consistence between game simulations
castSourceIdWithAlternateMana.add(sourceId); castSourceIdWithAlternateMana
castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null); .computeIfAbsent(sourceId, k -> new HashSet<>())
castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null); .add(identifier);
castSourceIdManaCosts
.computeIfAbsent(sourceId, k -> new HashMap<>())
.put(identifier, manaCosts != null ? manaCosts.copy() : null);
castSourceIdCosts
.computeIfAbsent(sourceId, k -> new HashMap<>())
.put(identifier, costs != null ? costs.copy() : null);
if (identifier == null) {
boolean a = true;
}
} }
@Override @Override
public Set<UUID> getCastSourceIdWithAlternateMana() { public Map<UUID, Set<MageIdentifier>> getCastSourceIdWithAlternateMana() {
return castSourceIdWithAlternateMana; return castSourceIdWithAlternateMana;
} }
@Override @Override
public Map<UUID, Costs<Cost>> getCastSourceIdCosts() { public Map<UUID, Map<MageIdentifier, Costs<Cost>>> getCastSourceIdCosts() {
return castSourceIdCosts; return castSourceIdCosts;
} }
@Override @Override
public Map<UUID, ManaCosts<ManaCost>> getCastSourceIdManaCosts() { public Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> getCastSourceIdManaCosts() {
return castSourceIdManaCosts; return castSourceIdManaCosts;
} }
@ -1187,10 +1218,19 @@ public abstract class PlayerImpl implements Player, Serializable {
// ALTERNATIVE COST from dynamic effects // ALTERNATIVE COST from dynamic effects
// some effects set sourceId to cast without paying mana costs or other costs // some effects set sourceId to cast without paying mana costs or other costs
if (getCastSourceIdWithAlternateMana().contains(ability.getSourceId())) { MageIdentifier identifier = approvingObject == null
? MageIdentifier.Default
: approvingObject.getApprovingAbility().getIdentifier();
if (!getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(identifier)) {
// identifier has no alternate cast entry for that sourceId, using Default instead.
identifier = MageIdentifier.Default;
}
if (getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(identifier)) {
Ability spellAbility = spell.getSpellAbility(); Ability spellAbility = spell.getSpellAbility();
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()); ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()).get(identifier);
Costs<Cost> costs = getCastSourceIdCosts().get(ability.getSourceId()); Costs<Cost> costs = getCastSourceIdCosts().get(ability.getSourceId()).get(identifier);
if (alternateCosts == null) { if (alternateCosts == null) {
noMana = true; noMana = true;
} else { } else {
@ -1273,21 +1313,30 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
} }
ApprovingObjectResult approvingResult = chooseApprovingObject(
game,
activationStatus.getApprovingObjects().stream().collect(Collectors.toList()),
false
);
if (approvingResult.status.equals(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE)) {
return false; // canceled choice of approving object.
}
//20091005 - 305.1 //20091005 - 305.1
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND,
card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()))) { card.getId(), playLandAbility, playerId, approvingResult.approvingObject))) {
// int bookmark = game.bookmarkState(); // int bookmark = game.bookmarkState();
// land events must return original zone (uses for commander watcher) // land events must return original zone (uses for commander watcher)
Zone cardZoneBefore = game.getState().getZone(card.getId()); Zone cardZoneBefore = game.getState().getZone(card.getId());
GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND,
card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); card.getId(), playLandAbility, playerId, approvingResult.approvingObject);
landEventBefore.setZone(cardZoneBefore); landEventBefore.setZone(cardZoneBefore);
game.fireEvent(landEventBefore); game.fireEvent(landEventBefore);
if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) { if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) {
incrementLandsPlayed(); incrementLandsPlayed();
GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED,
card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); card.getId(), playLandAbility, playerId, approvingResult.approvingObject);
landEventAfter.setZone(cardZoneBefore); landEventAfter.setZone(cardZoneBefore);
game.fireEvent(landEventAfter); game.fireEvent(landEventAfter);
@ -1311,6 +1360,68 @@ public abstract class PlayerImpl implements Player, Serializable {
return true; return true;
} }
private enum ApprovingObjectResultStatus {
CHOSEN,
NO_POSSIBLE_CHOICE,
NOT_REQUIRED_NO_CHOICE,
}
private class ApprovingObjectResult {
public final ApprovingObjectResultStatus status;
public final ApprovingObject approvingObject; // not null iff status is CHOSEN
private ApprovingObjectResult(ApprovingObjectResultStatus status, ApprovingObject approvingObject) {
this.status = status;
this.approvingObject = approvingObject;
}
}
private ApprovingObjectResult chooseApprovingObject(Game game, List<ApprovingObject> possibleApprovingObjects, boolean required) {
// Choosing
if (possibleApprovingObjects.isEmpty()) {
return new ApprovingObjectResult(ApprovingObjectResultStatus.NO_POSSIBLE_CHOICE, null);
} else {
// Select the ability that you use to permit the action
Map<String, String> keyChoices = new HashMap<>();
int i = 0;
for (ApprovingObject possibleApprovingObject : possibleApprovingObjects) {
MageObject mageObject = game.getObject(possibleApprovingObject.getApprovingAbility().getSourceId());
String choiceValue = "";
MageIdentifier identifier = possibleApprovingObject.getApprovingAbility().getIdentifier();
if (!identifier.getAdditionalText().isEmpty()) {
choiceValue += identifier.getAdditionalText() + ": ";
}
if (mageObject == null) {
choiceValue += possibleApprovingObject.getApprovingAbility().getRule();
} else {
choiceValue += mageObject.getIdName() + ": ";
String moreDetails = possibleApprovingObject.getApprovingAbility().getRule(mageObject.getName());
choiceValue += moreDetails.isEmpty() ? "Cast normally" : moreDetails;
}
keyChoices.put((i++) + "", choiceValue);
}
int choice = 0;
if (!game.inCheckPlayableState() && keyChoices.size() > 1) {
Choice choicePermitting = new ChoiceImpl(required);
choicePermitting.setMessage("Choose the permitting object");
choicePermitting.setKeyChoices(keyChoices);
if (canRespond()) {
if (choose(Outcome.Neutral, choicePermitting, game)) {
String choiceKey = choicePermitting.getChoiceKey();
if (choiceKey != null) {
choice = Integer.parseInt(choiceKey);
}
} else {
return new ApprovingObjectResult(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE, null);
}
}
}
return new ApprovingObjectResult(ApprovingObjectResultStatus.CHOSEN, possibleApprovingObjects.get(choice));
}
}
protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) { protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) {
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY,
ability.getId(), ability, playerId))) { ability.getId(), ability, playerId))) {
@ -1463,7 +1574,16 @@ public abstract class PlayerImpl implements Player, Serializable {
result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game); result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game);
break; break;
case SPELL: case SPELL:
result = cast((SpellAbility) ability, game, false, activationStatus.getApprovingObject()); ApprovingObjectResult approvingResult = chooseApprovingObject(
game,
activationStatus.getApprovingObjects().stream().collect(Collectors.toList()),
false
);
if (approvingResult.status.equals(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE)) {
return false; // chosen to not approve any AsThough.
}
result = cast((SpellAbility) ability, game, false, approvingResult.approvingObject);
break; break;
default: default:
result = playAbility(ability.copy(), game); result = playAbility(ability.copy(), game);
@ -3452,9 +3572,9 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
// ALTERNATIVE COST FROM dynamic effects // ALTERNATIVE COST FROM dynamic effects
if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) { for(MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) {
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()); ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier);
Costs<Cost> costs = getCastSourceIdCosts().get(copy.getSourceId()); Costs<Cost> costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier);
boolean canPutToPlay = true; boolean canPutToPlay = true;
if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) { if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) {
@ -3499,9 +3619,7 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
// Get the ability, if any, which allows for spending many as if it were another color. // Get the ability, if any, which allows for spending many as if it were another color.
// TODO: This needs to be improved to handle multiple approving objects. Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(ability.getSourceId(),
// See https://github.com/magefree/mage/issues/8584
ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(),
AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game);
for (Mana mana : abilityOptions) { for (Mana mana : abilityOptions) {
if (mana.count() == 0) { if (mana.count() == 0) {
@ -3517,7 +3635,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// TODO: Describe this // TODO: Describe this
// Abilities that let us spend mana as if it were any (or other colors/types) must be handled separately // Abilities that let us spend mana as if it were any (or other colors/types) must be handled separately
// and can't be incorporated into calculating availableMana since the number of combinations would explode. // and can't be incorporated into calculating availableMana since the number of combinations would explode.
if (approvingObject != null && mana.count() <= avail.count()) { if (!approvingObjects.isEmpty() && mana.count() <= avail.count()) {
// TODO: I think this is wrong for spell that require colorless // TODO: I think this is wrong for spell that require colorless
return true; return true;
} }
@ -3764,7 +3882,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// So make it available all the time // So make it available all the time
boolean canUse; boolean canUse;
if (ability instanceof MorphAbility && object instanceof Card && (game.canPlaySorcery(getId()) if (ability instanceof MorphAbility && object instanceof Card && (game.canPlaySorcery(getId())
|| (null != game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game)))) { || (!game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game).isEmpty()))) {
canUse = canPlayCardByAlternateCost((Card) object, availableMana, playAbility, game); canUse = canPlayCardByAlternateCost((Card) object, availableMana, playAbility, game);
} else { } else {
canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions
@ -3843,23 +3961,23 @@ public abstract class PlayerImpl implements Player, Serializable {
continue; continue;
} }
ApprovingObject approvingObject; Set<ApprovingObject> approvingObjects;
if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) { if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) {
// play hand from non hand zone (except battlefield - you can't play already played permanents) // play hand from non hand zone (except battlefield - you can't play already played permanents)
approvingObject = game.getContinuousEffects().asThough(object.getId(), approvingObjects = game.getContinuousEffects().asThough(object.getId(),
AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game);
if (approvingObject == null && isPlaySpell if (approvingObjects.isEmpty() && isPlaySpell
&& ((SpellAbility) ability).getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) { && ((SpellAbility) ability).getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) {
approvingObject = game.getContinuousEffects().asThough(object.getId(), approvingObjects = game.getContinuousEffects().asThough(object.getId(),
AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game);
} }
} else { } else {
// other abilities from direct zones // other abilities from direct zones
approvingObject = null; approvingObjects = new HashSet<>();
} }
boolean canActivateAsHandZone = approvingObject != null boolean canActivateAsHandZone = !approvingObjects.isEmpty()
|| (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard());
boolean possibleToPlay = canActivateAsHandZone boolean possibleToPlay = canActivateAsHandZone
&& ability.getZone().match(Zone.HAND) && ability.getZone().match(Zone.HAND)
@ -4434,8 +4552,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) { public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) {
if (null != game.getContinuousEffects().asThough(card.getId(), if (!game.getContinuousEffects().asThough(card.getId(),
AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) { AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game).isEmpty()) {
// two modes: look at the card or do not look and activate other abilities // two modes: look at the card or do not look and activate other abilities
String lookMessage = "Look at " + card.getIdName(); String lookMessage = "Look at " + card.getIdName();
String lookYes = "Yes, look at the card"; String lookYes = "Yes, look at the card";

View file

@ -2,6 +2,7 @@ package mage.util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import mage.ApprovingObject; import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject; import mage.MageObject;
import mage.Mana; import mage.Mana;
import mage.abilities.*; import mage.abilities.*;
@ -147,7 +148,7 @@ public final class CardUtil {
ability.addManaCostsToPay(adjustedCost); ability.addManaCostsToPay(adjustedCost);
} }
private static ManaCosts<ManaCost> adjustCost(ManaCosts<ManaCost> manaCosts, int reduceCount) { public static ManaCosts<ManaCost> adjustCost(ManaCosts<ManaCost> manaCosts, int reduceCount) {
ManaCosts<ManaCost> newCost = new ManaCostsImpl<>(); ManaCosts<ManaCost> newCost = new ManaCostsImpl<>();
// nothing to change // nothing to change
@ -1447,8 +1448,8 @@ public final class CardUtil {
Costs<Cost> additionalCostsLeft = leftHalfCard.getSpellAbility().getCosts(); Costs<Cost> additionalCostsLeft = leftHalfCard.getSpellAbility().getCosts();
Costs<Cost> additionalCostsRight = rightHalfCard.getSpellAbility().getCosts(); Costs<Cost> additionalCostsRight = rightHalfCard.getSpellAbility().getCosts();
// set alternative cost and any additional cost // set alternative cost and any additional cost
player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsLeft); player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsLeft, MageIdentifier.Default);
player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsRight); player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsRight, MageIdentifier.Default);
} }
// allow the card to be cast // allow the card to be cast
game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), Boolean.TRUE); game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), Boolean.TRUE);
@ -1465,13 +1466,13 @@ public final class CardUtil {
// get additional cost if any // get additional cost if any
Costs<Cost> additionalCostsMDFCLeft = leftHalfCard.getSpellAbility().getCosts(); Costs<Cost> additionalCostsMDFCLeft = leftHalfCard.getSpellAbility().getCosts();
// set alternative cost and any additional cost // set alternative cost and any additional cost
player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsMDFCLeft); player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsMDFCLeft, MageIdentifier.Default);
} }
if (!rightHalfCard.isLand(game)) { if (!rightHalfCard.isLand(game)) {
// get additional cost if any // get additional cost if any
Costs<Cost> additionalCostsMDFCRight = rightHalfCard.getSpellAbility().getCosts(); Costs<Cost> additionalCostsMDFCRight = rightHalfCard.getSpellAbility().getCosts();
// set alternative cost and any additional cost // set alternative cost and any additional cost
player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsMDFCRight); player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsMDFCRight, MageIdentifier.Default);
} }
} }
// allow the card to be cast // allow the card to be cast
@ -1488,8 +1489,8 @@ public final class CardUtil {
Costs<Cost> additionalCostsCreature = creatureCard.getSpellAbility().getCosts(); Costs<Cost> additionalCostsCreature = creatureCard.getSpellAbility().getCosts();
Costs<Cost> additionalCostsSpellCard = spellCard.getSpellAbility().getCosts(); Costs<Cost> additionalCostsSpellCard = spellCard.getSpellAbility().getCosts();
// set alternative cost and any additional cost // set alternative cost and any additional cost
player.setCastSourceIdWithAlternateMana(creatureCard.getId(), manaCost, additionalCostsCreature); player.setCastSourceIdWithAlternateMana(creatureCard.getId(), manaCost, additionalCostsCreature, MageIdentifier.Default);
player.setCastSourceIdWithAlternateMana(spellCard.getId(), manaCost, additionalCostsSpellCard); player.setCastSourceIdWithAlternateMana(spellCard.getId(), manaCost, additionalCostsSpellCard, MageIdentifier.Default);
} }
// allow the card to be cast // allow the card to be cast
game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), Boolean.TRUE); game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), Boolean.TRUE);
@ -1500,7 +1501,7 @@ public final class CardUtil {
if (manaCost != null) { if (manaCost != null) {
// get additional cost if any // get additional cost if any
Costs<Cost> additionalCostsNormalCard = card.getSpellAbility().getCosts(); Costs<Cost> additionalCostsNormalCard = card.getSpellAbility().getCosts();
player.setCastSourceIdWithAlternateMana(card.getMainCard().getId(), manaCost, additionalCostsNormalCard); player.setCastSourceIdWithAlternateMana(card.getMainCard().getId(), manaCost, additionalCostsNormalCard, MageIdentifier.Default);
} }
// cast it // cast it