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) {
// 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;
List<MageObject> producers;
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)) {
continue;
}
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue;
}
if (activateAbility(manaAbility, game)) {
@ -1560,11 +1562,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof ColoredManaCost) {
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)) {
continue;
}
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue;
}
if (activateAbility(manaAbility, game)) {
@ -1578,11 +1580,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof SnowManaCost) {
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)) {
continue;
}
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue;
}
if (activateAbility(manaAbility, game)) {
@ -1596,11 +1598,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof HybridManaCost) {
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)) {
continue;
}
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue;
}
if (activateAbility(manaAbility, game)) {
@ -1614,11 +1616,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof MonoHybridManaCost) {
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)) {
continue;
}
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue;
}
if (activateAbility(manaAbility, game)) {
@ -1632,11 +1634,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof ColorlessManaCost) {
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)) {
continue;
}
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue;
}
if (activateAbility(manaAbility, game)) {
@ -1650,11 +1652,11 @@ public class ComputerPlayer extends PlayerImpl implements Player {
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof GenericManaCost) {
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)) {
continue;
}
if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) {
continue;
}
if (activateAbility(manaAbility, game)) {
@ -1673,7 +1675,7 @@ public class ComputerPlayer extends PlayerImpl implements Player {
// pay phyrexian life costs
if (cost.isPhyrexian()) {
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;
return paidPhyrexian;
}
@ -1688,7 +1690,7 @@ public class ComputerPlayer extends PlayerImpl implements Player {
ManaOptions specialMana = specialAction == null ? null : specialAction.getManaOptions(ability, game, unpaid);
if (specialMana != null) {
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)) {
continue;
}

View file

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

View file

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

View file

@ -1,5 +1,6 @@
package mage.cards.b;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.common.SimpleStaticAbility;
@ -44,7 +45,7 @@ public final class BolassCitadel extends CardImpl {
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.
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.
Ability ability = new SimpleActivatedAbility(new LoseLifeOpponentsEffect(10), new TapSourceCost());
@ -118,7 +119,7 @@ class BolassCitadelPlayTheTopCardEffect extends AsThoughEffectImpl {
Costs newCosts = new CostsImpl();
newCosts.add(lifeCost);
newCosts.addAll(cardToCheck.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts);
player.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts, MageIdentifier.BolassCitadelAlternateCast);
}
return true;
}

View file

@ -104,7 +104,7 @@ class CemeteryIlluminatorExileEffect extends OneShotEffect {
class CemeteryIlluminatorPlayTopEffect extends AsThoughEffectImpl {
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}";
}

View file

@ -63,7 +63,7 @@ public final class DanithaNewBenaliasLight extends CardImpl {
class DanithaNewBenaliasLightCastFromGraveyardEffect extends AsThoughEffectImpl {
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";
}

View file

@ -1,7 +1,6 @@
package mage.cards.d;
import java.util.UUID;
import mage.MageIdentifier;
import mage.MageInt;
import mage.abilities.Ability;
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.cost.SpellCostReductionForEachSourceEffect;
import mage.abilities.hint.ValueHint;
import mage.constants.*;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.stack.Spell;
@ -27,6 +26,8 @@ import mage.players.Player;
import mage.target.common.TargetCardInYourGraveyard;
import mage.watchers.common.SpellsCastWatcher;
import java.util.UUID;
/**
*
* @author weirddan455
@ -53,7 +54,7 @@ public final class Demilich extends CardImpl {
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.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemilichPlayEffect()));
this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemilichPlayEffect()).setIdentifier(MageIdentifier.DemilichAlternateCast));
}
private Demilich(final Demilich card) {
@ -123,7 +124,7 @@ class DemilichPlayEffect extends AsThoughEffectImpl {
if (controller != null) {
Costs<Cost> costs = new CostsImpl<>();
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;
}
}

View file

@ -1,7 +1,8 @@
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.Costs;
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.mana.ManaCostsImpl;
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.common.AttachEffect;
import mage.abilities.effects.common.continuous.AddCardSubtypeAttachedEffect;
import mage.abilities.effects.common.continuous.BoostEnchantedEffect;
import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect;
import mage.target.TargetPermanent;
import mage.abilities.keyword.EnchantAbility;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl;
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
@ -54,7 +55,7 @@ public final class DemonicEmbrace extends CardImpl {
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.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemonicEmbracePlayEffect()));
this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemonicEmbracePlayEffect()).setIdentifier(MageIdentifier.DemonicEmbraceAlternateCast));
}
private DemonicEmbrace(final DemonicEmbrace card) {
@ -98,7 +99,10 @@ class DemonicEmbracePlayEffect extends AsThoughEffectImpl {
Costs<Cost> costs = new CostsImpl<>();
costs.add(new PayLifeCost(3));
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;
}
}

View file

@ -1,5 +1,6 @@
package mage.cards.f;
import mage.MageIdentifier;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldAbility;
@ -18,12 +19,12 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.counters.CounterType;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetControlledCreaturePermanent;
import java.util.UUID;
import mage.filter.common.FilterControlledCreaturePermanent;
/**
* @author TheElk801
@ -56,7 +57,10 @@ public final class FalcoSparaPactweaver extends CardImpl {
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.
this.addAbility(new SimpleStaticAbility(new FalcoSparaPactweaverEffect()));
this.addAbility(
new SimpleStaticAbility(new FalcoSparaPactweaverEffect())
.setIdentifier(MageIdentifier.FalcoSparaPactweaverAlternateCast)
);
}
private FalcoSparaPactweaver(final FalcoSparaPactweaver card) {
@ -113,7 +117,10 @@ class FalcoSparaPactweaverEffect extends AsThoughEffectImpl {
Costs<Cost> newCosts = new CostsImpl<>();
newCosts.add(new RemoveCounterCost(new TargetControlledCreaturePermanent(1, 1, new FilterControlledCreaturePermanent(), true)));
newCosts.addAll(cardToCheck.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(cardToCheck.getId(), cardToCheck.getManaCost(), newCosts);
player.setCastSourceIdWithAlternateMana(
cardToCheck.getId(), cardToCheck.getManaCost(), newCosts,
MageIdentifier.FalcoSparaPactweaverAlternateCast
);
return true;
}
}

View file

@ -57,7 +57,7 @@ public final class GisaAndGeralf extends CardImpl {
class GisaAndGeralfCastFromGraveyardEffect extends AsThoughEffectImpl {
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";
}

View file

@ -1,9 +1,12 @@
package mage.cards.g;
import java.util.HashSet;
import java.util.Set;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
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.common.LookLibraryAndPickControllerEffect;
import mage.cards.Card;
@ -15,14 +18,12 @@ import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
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 java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
*
* @author jeffwadsworth
@ -36,11 +37,16 @@ public class GlimpseTheCosmos extends CardImpl {
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.
this.addAbility(new SimpleStaticAbility(Zone.GRAVEYARD,
new ConditionalAsThoughEffect(
new GlimpseTheCosmosPlayEffect(),
new PermanentsOnTheBattlefieldCondition(new FilterControlledPermanent(SubType.GIANT)))).setIdentifier(MageIdentifier.GlimpseTheCosmosWatcher),
new GlimpseTheCosmosWatcher());
this.addAbility(
new SimpleStaticAbility(
Zone.GRAVEYARD,
new ConditionalAsThoughEffect(
new GlimpseTheCosmosPlayEffect(),
new PermanentsOnTheBattlefieldCondition(new FilterControlledPermanent(SubType.GIANT))
)
).setIdentifier(MageIdentifier.GlimpseTheCosmosWatcher),
new GlimpseTheCosmosWatcher()
);
this.addAbility(new SimpleStaticAbility(Zone.ALL, new GlimpseTheCosmosReplacementEffect()));
@ -85,7 +91,10 @@ class GlimpseTheCosmosPlayEffect extends AsThoughEffectImpl {
if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) {
Player controller = game.getPlayer(affectedControllerId);
if (controller != null) {
controller.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{U}"), null);
controller.setCastSourceIdWithAlternateMana(
sourceId, new ManaCostsImpl<>("{U}"), null,
MageIdentifier.GlimpseTheCosmosWatcher
);
return true;
}
}

View file

@ -136,7 +136,7 @@ class HaukensInsightLookEffect extends AsThoughEffectImpl {
class HaukensInsightPlayEffect extends AsThoughEffectImpl {
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";
}
@ -164,7 +164,7 @@ class HaukensInsightPlayEffect extends AsThoughEffectImpl {
UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), game.getState().getZoneChangeCounter(source.getSourceId()));
ExileZone exileZone = game.getExile().getExileZone(exileId);
if (exileZone != null && exileZone.contains(CardUtil.getMainCardId(game, objectId))) {
allowCardToPlayWithoutMana(objectId, source, affectedControllerId, game);
allowCardToPlayWithoutMana(objectId, source, affectedControllerId, MageIdentifier.HaukensInsightWatcher, game);
return true;
}
}

View file

@ -1,5 +1,6 @@
package mage.cards.h;
import mage.MageIdentifier;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
@ -37,7 +38,9 @@ public final class Helbrute extends CardImpl {
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.
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) {
@ -91,7 +94,10 @@ class HelbruteEffect extends AsThoughEffectImpl {
}
Costs<Cost> costs = new CostsImpl<>();
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;
}
}

View file

@ -84,7 +84,7 @@ enum JohannApprenticeSorcererHint implements Hint {
class JohannApprenticeSorcererPlayTopEffect extends AsThoughEffectImpl {
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";
}

View file

@ -72,7 +72,7 @@ public final class KaghaShadowArchdruid extends CardImpl {
class KaghaShadowArchdruidEffect extends AsThoughEffectImpl {
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.";
}

View file

@ -98,7 +98,7 @@ class KaradorGhostChieftainCostReductionEffect extends CostModificationEffectImp
class KaradorGhostChieftainCastFromGraveyardEffect extends AsThoughEffectImpl {
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";
}

View file

@ -60,7 +60,7 @@ public final class KessDissidentMage extends CardImpl {
class KessDissidentMageCastFromGraveyardEffect extends AsThoughEffectImpl {
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";
}

View file

@ -1,5 +1,6 @@
package mage.cards.m;
import mage.MageIdentifier;
import mage.MageObjectReference;
import mage.abilities.Ability;
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}");
// 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());
this.addAbility(ability, new MaestrosAscendancyWatcher());
}
@ -87,7 +89,10 @@ class MaestrosAscendancyCastEffect extends AsThoughEffectImpl {
Costs<Cost> newCosts = new CostsImpl<>();
newCosts.addAll(card.getSpellAbility().getCosts());
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;
}
}

View file

@ -66,7 +66,7 @@ public final class MuldrothaTheGravetide extends CardImpl {
class MuldrothaTheGravetideCastFromGraveyardEffect extends AsThoughEffectImpl {
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. "
+ "<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;
import mage.MageIdentifier;
import mage.MageInt;
import mage.MageObjectReference;
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.
this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(
new NashiMoonSagesScionEffect(), false
), new NashiMoonSagesScionWatcher());
).setIdentifier(MageIdentifier.NashiMoonSagesScionAlternateCast), new NashiMoonSagesScionWatcher());
}
private NashiMoonSagesScion(final NashiMoonSagesScion card) {
@ -194,7 +195,10 @@ class NashiMoonSagesScionPlayEffect extends CanPlayCardControllerEffect {
Costs<Cost> newCosts = new CostsImpl<>();
newCosts.add(lifeCost);
newCosts.addAll(cardToCheck.getSpellAbility().getCosts());
controller.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts);
controller.setCastSourceIdWithAlternateMana(
cardToCheck.getId(), null, newCosts,
MageIdentifier.NashiMoonSagesScionAlternateCast
);
return true;
}
}

View file

@ -4,15 +4,18 @@ import mage.MageIdentifier;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect;
import mage.abilities.effects.common.continuous.PlayTheTopCardEffect;
import mage.abilities.hint.common.ConditionPermanentHint;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
import mage.watchers.Watcher;
@ -22,7 +25,7 @@ import java.util.Set;
import java.util.UUID;
/**
* @author TheElk801
* @author TheElk801, Susucr
*/
public final class OneWithTheMultiverse extends CardImpl {
@ -36,8 +39,15 @@ public final class OneWithTheMultiverse extends CardImpl {
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.
this.addAbility(new SimpleStaticAbility(new OneWithTheMultiverseEffect())
.setIdentifier(MageIdentifier.OneWithTheMultiverseWatcher), new OneWithTheMultiverseWatcher());
this.addAbility(
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) {
@ -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 {
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 " +
"or the top of your library without paying its mana cost.";
}
@ -78,13 +102,14 @@ class OneWithTheMultiverseEffect extends AsThoughEffectImpl {
return false;
}
Player controller = game.getPlayer(source.getControllerId());
Permanent sourceObject = game.getPermanent(source.getSourceId());
Card card = game.getCard(CardUtil.getMainCardId(game, objectId));
OneWithTheMultiverseWatcher watcher = game.getState().getWatcher(OneWithTheMultiverseWatcher.class);
if (controller == null
|| card == null
|| watcher == null
|| watcher.isAbilityUsed(new MageObjectReference(source))
|| !card.isOwnedBy(source.getControllerId())) {
|| sourceObject == null
|| watcher.isAbilityUsed(new MageObjectReference(sourceObject, game))) {
return false;
}
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()))) {
return false;
}
allowCardToPlayWithoutMana(objectId, source, affectedControllerId, game);
allowCardToPlayWithoutMana(objectId, source, affectedControllerId, MageIdentifier.OneWithTheMultiverseWatcher, game);
return true;
}
}

View file

@ -1,5 +1,6 @@
package mage.cards.r;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.Cost;
@ -39,8 +40,9 @@ public final class RaffinesGuidance extends CardImpl {
// Enchanted creature gets +1/+1.
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.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new RafinnesGuidancePlayEffect()));
// 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())
.setIdentifier(MageIdentifier.RafinnesGuidanceAlternateCast));
}
private RaffinesGuidance(final RaffinesGuidance card) {
@ -71,7 +73,10 @@ class RafinnesGuidancePlayEffect extends AsThoughEffectImpl {
Player player = game.getPlayer(affectedControllerId);
if (player != null) {
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;
}
}

View file

@ -2,10 +2,14 @@
package mage.cards.r;
import java.util.UUID;
import mage.MageIdentifier;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.CantBlockAbility;
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.common.continuous.BoostControlledEffect;
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 {
@ -47,9 +51,8 @@ public final class RisenExecutioner extends CardImpl {
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.
// 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.addEffect(new RisenExecutionerCostIncreasingEffect());
Ability ability = new SimpleStaticAbility(Zone.ALL, new RisenExecutionerCastEffect())
.setIdentifier(MageIdentifier.RisenExectutionerAlternateCast);
this.addAbility(ability);
}
@ -66,6 +69,12 @@ public final class RisenExecutioner extends CardImpl {
class RisenExecutionerCastEffect extends AsThoughEffectImpl {
protected static final FilterCreatureCard filter = new FilterCreatureCard();
static {
filter.add(AnotherPredicate.instance);
}
RisenExecutionerCastEffect() {
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";
@ -87,56 +96,23 @@ class RisenExecutionerCastEffect extends AsThoughEffectImpl {
@Override
public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) {
if (sourceId.equals(source.getSourceId())) {
Card card = game.getCard(source.getSourceId());
if (card != null
&& card.isOwnedBy(affectedControllerId)
&& game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) {
return true;
}
if (!sourceId.equals(source.getSourceId())) {
return false;
}
Card card = game.getCard(source.getSourceId());
if(card == null
|| !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());
if (controller != null) {
CardUtil.increaseCost(abilityToModify, controller.getGraveyard().count(filter, source.getControllerId(), source, game));
if(controller == null) {
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;
}
@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;
import mage.MageIdentifier;
import mage.MageInt;
import mage.abilities.Ability;
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.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new RonaSheoldredsFaithfulEffect()));
this.addAbility(new SimpleStaticAbility(Zone.ALL, new RonaSheoldredsFaithfulEffect())
.setIdentifier(MageIdentifier.RonaSheoldredsFaithfulAlternateCast));
}
private RonaSheoldredsFaithful(final RonaSheoldredsFaithful card) {
@ -90,7 +92,10 @@ class RonaSheoldredsFaithfulEffect extends AsThoughEffectImpl {
}
Costs<Cost> costs = new CostsImpl<>();
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;
}
}

View file

@ -1,7 +1,7 @@
package mage.cards.s;
import java.util.UUID;
import mage.MageIdentifier;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
@ -14,16 +14,13 @@ import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.AsThoughEffectType;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.constants.*;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetControlledCreaturePermanent;
import java.util.UUID;
/**
*
* @author LevelX2
@ -40,7 +37,8 @@ public final class ScourgeOfNelToth extends CardImpl {
// Flying
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.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new ScourgeOfNelTothPlayEffect()));
this.addAbility(new SimpleStaticAbility(Zone.ALL, new ScourgeOfNelTothPlayEffect())
.setIdentifier(MageIdentifier.ScourgeOfNelTothAlternateCast));
}
private ScourgeOfNelToth(final ScourgeOfNelToth card) {
@ -80,10 +78,12 @@ class ScourgeOfNelTothPlayEffect extends AsThoughEffectImpl {
if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) {
Player player = game.getPlayer(affectedControllerId);
if (player != null) {
// can sometimes be cast with base mana cost from grave????
Costs<Cost> costs = new CostsImpl<>();
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;
}
}

View file

@ -1,5 +1,6 @@
package mage.cards.s;
import mage.MageIdentifier;
import mage.MageInt;
import mage.abilities.Ability;
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.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SqueeDubiousMonarchEffect()));
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SqueeDubiousMonarchEffect())
.setIdentifier(MageIdentifier.SqueeDubiousMonarchAlternateCast));
}
private SqueeDubiousMonarch(final SqueeDubiousMonarch card) {
@ -101,7 +103,10 @@ class SqueeDubiousMonarchEffect extends AsThoughEffectImpl {
}
Costs<Cost> costs = new CostsImpl<>();
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;
}
}

View file

@ -81,7 +81,7 @@ class CantBeBlockedUnlessAllEffect extends RestrictionEffect {
// check if all creatures of defender are able to block this permanent
// permanent.canBlock() can't be used because causing recursive call
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;
}
// check blocker restrictions

View file

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

View file

@ -77,7 +77,7 @@ class WishEffect extends OneShotEffect {
class WishPlayFromSideboardEffect extends AsThoughEffectImpl {
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) {

View file

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

View file

@ -1,5 +1,6 @@
package mage.cards.x;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
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.
this.getSpellAbility().addEffect(new XandersPactExileEffect());
this.getSpellAbility().setIdentifier(MageIdentifier.XandersPactAlternateCast);
}
private XandersPact(final XandersPact card) {
@ -120,7 +122,10 @@ class XandersPactCastEffect extends CanPlayCardControllerEffect {
Costs<Cost> newCosts = new CostsImpl<>();
newCosts.add(new PayLifeCost(cardToCheck.getManaValue()));
newCosts.addAll(cardToCheck.getSpellAbility().getCosts());
controller.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts);
controller.setCastSourceIdWithAlternateMana(
cardToCheck.getId(), null, newCosts,
MageIdentifier.XandersPactAlternateCast
);
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);
// play as adventure spell
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);
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, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5);
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}
//
@ -119,15 +120,64 @@ public class KaradorGhostChieftainTest extends CardTestPlayerBase {
// 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 Karagar
// cast zombie creature and approves by Karador
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Accursed Horde");
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)
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 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);
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.");
playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Plains");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Ephemerate", "Grizzly Bears");
setChoice(playerA, "Emblem Tibalt");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);

View file

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

View file

@ -1,6 +1,7 @@
package org.mage.test.stub;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject;
import mage.Mana;
import mage.abilities.*;
@ -1270,22 +1271,22 @@ public class PlayerStub implements Player {
}
@Override
public Set<UUID> getCastSourceIdWithAlternateMana() {
public Map<UUID, Set<MageIdentifier>> getCastSourceIdWithAlternateMana() {
return null;
}
@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
public Map<UUID, Costs<Cost>> getCastSourceIdCosts() {
public Map<UUID, Map<MageIdentifier,Costs<Cost>>> getCastSourceIdCosts() {
return null;
}
@Override
public Map<UUID, ManaCosts<ManaCost>> getCastSourceIdManaCosts() {
public Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> getCastSourceIdManaCosts() {
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
* correct watcher or other processing.
*
* @author LevelX2
* @author LevelX2, Susucr
*/
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,
CemeteryIlluminatorWatcher,
GisaAndGeralfWatcher,
@ -19,7 +31,65 @@ public enum MageIdentifier {
WishWatcher,
GlimpseTheCosmosWatcher,
SerraParagonWatcher,
OneWithTheMultiverseWatcher,
OneWithTheMultiverseWatcher("Without paying manacost"),
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<CardIcon> icons = new ArrayList<>();
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 int sourcePermanentTransformCount = 0;

View file

@ -7,41 +7,57 @@ import mage.constants.TargetController;
import mage.constants.TimingRule;
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 {
final class ActivationStatus {
private final boolean canActivate;
private final ApprovingObject approvingObject;
// Expected to not be modified after creation.
private final Set<ApprovingObject> approvingObjects;
public ActivationStatus(boolean canActivate, ApprovingObject approvingObject) {
this.canActivate = canActivate;
this.approvingObject = approvingObject;
// If true, the Activation Status will not check if there is an approvingObject.
private final boolean forcedCanActivate;
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() {
return canActivate;
}
public ApprovingObject getApprovingObject() {
return approvingObject;
}
public static ActivationStatus getFalse() {
return new ActivationStatus(false, null);
return forcedCanActivate || !approvingObjects.isEmpty();
}
/**
* @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) {
ApprovingObject approvingObject = approvingObjectAbility == null ? null : new ApprovingObject(approvingObjectAbility, game);
return new ActivationStatus(true, approvingObject);
public Set<ApprovingObject> getApprovingObjects() {
return approvingObjects;
}
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.util.CardUtil;
import java.util.Set;
import java.util.UUID;
/**
@ -179,15 +180,16 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
// timing check
//20091005 - 602.5d/602.5e
boolean asInstant;
ApprovingObject approvingObject = game.getContinuousEffects()
Set<ApprovingObject> approvingObjects = game
.getContinuousEffects()
.asThough(sourceId,
AsThoughEffectType.ACTIVATE_AS_INSTANT,
this,
controllerId,
game);
asInstant = approvingObject != null;
asInstant |= (timing == TimingRule.INSTANT);
game
);
boolean asInstant = !approvingObjects.isEmpty()
|| (timing == TimingRule.INSTANT);
if (!asInstant && !game.canPlaySorcery(playerId)) {
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,
// activatorId must be removed
this.activatorId = playerId;
return new ActivationStatus(true, approvingObject);
if (approvingObjects.isEmpty()) {
return ActivationStatus.withoutApprovingObject(true);
}
else {
return new ActivationStatus(approvingObjects);
}
}
@Override

View file

@ -1,11 +1,13 @@
package mage.abilities;
import mage.ApprovingObject;
import mage.cards.Card;
import mage.constants.AbilityType;
import mage.constants.AsThoughEffectType;
import mage.constants.Zone;
import mage.game.Game;
import java.util.Set;
import java.util.UUID;
/**
@ -33,15 +35,35 @@ public class PlayLandAbility extends ActivatedAbilityImpl {
// no super.canActivate() call
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (!controlsAbility(playerId, game) && null == approvingObject) {
Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (!controlsAbility(playerId, game) && approvingObjects.isEmpty()) {
return ActivationStatus.getFalse();
}
//20091005 - 114.2a
return new ActivationStatus(game.isActivePlayer(playerId)
&& game.getPlayer(playerId).canPlayLand()
&& game.canPlaySorcery(playerId),
approvingObject);
if(!game.isActivePlayer(playerId)
|| !game.getPlayer(playerId).canPlayLand()
|| !game.canPlaySorcery(playerId)) {
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

View file

@ -1,6 +1,7 @@
package mage.abilities;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCost;
@ -45,6 +46,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
this.spellAbilityType = spellAbilityType;
this.spellAbilityCastMode = spellAbilityCastMode;
this.addManaCost(cost);
this.setIdentifier(MageIdentifier.Default);
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
|| object.isInstant(game)
|| object.hasAbility(FlashAbility.getInstance(), game)
@ -116,13 +118,13 @@ public class SpellAbility extends ActivatedAbilityImpl {
}
// play from not own hand
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (approvingObject == null && getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) {
Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (approvingObjects.isEmpty() && getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) {
// 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);
if (!(card != null && card.isOwnedBy(playerId))) {
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
// Alternate spell abilities (Flashback, Overload) can't be cast with no mana to pay option
if (getSpellAbilityType() == SpellAbilityType.BASE_ALTERNATE) {
Player player = game.getPlayer(playerId);
if (player != null
&& player.getCastSourceIdWithAlternateMana().contains(getSourceId())) {
&& player.getCastSourceIdWithAlternateMana()
.getOrDefault(getSourceId(), Collections.emptySet())
.contains(MageIdentifier.Default)
) {
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
// 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) {
return new ActivationStatus(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)
&& splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId), null);
return ActivationStatus.withoutApprovingObject(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)
&& splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId));
}
}
return ActivationStatus.getFalse();
} 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;
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.staticText = text;
}

View file

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

View file

@ -37,7 +37,7 @@ public class TapSourceCost extends CostImpl {
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null) {
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;
}

View file

@ -36,7 +36,7 @@ public class UntapSourceCost extends CostImpl {
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
Permanent permanent = game.getPermanent(source.getSourceId());
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;
}

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package mage.abilities.effects;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject;
import mage.abilities.Ability;
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 controllerId
* @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)
if (type.needAffectedAbility() && affectedAbility == null) {
@ -553,18 +555,13 @@ public class ContinuousEffects implements Serializable {
idToCheck = objectId;
}
Set<ApprovingObject> possibleApprovingObjects = new HashSet<>();
for (AsThoughEffect effect : asThoughEffectsList) {
Set<Ability> abilities = asThoughEffectsMap.get(type).getAbility(effect.getId());
for (Ability ability : abilities) {
if (affectedAbility == null) {
// applies to full object (one effect can be used in multiple abilities)
if (effect.applies(idToCheck, ability, controllerId, game)) {
if (effect.isConsumable() && !game.inCheckPlayableState()) {
possibleApprovingObjects.add(new ApprovingObject(ability, game));
} else {
return new ApprovingObject(ability, game);
}
possibleApprovingObjects.add(new ApprovingObject(ability, game));
}
} else {
// applies to one affected ability
@ -575,46 +572,13 @@ public class ContinuousEffects implements Serializable {
}
if (effect.applies(idToCheck, affectedAbility, ability, game, controllerId)) {
if (effect.isConsumable() && !game.inCheckPlayableState()) {
possibleApprovingObjects.add(new ApprovingObject(ability, game));
} else {
return new ApprovingObject(ability, game);
}
possibleApprovingObjects.add(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;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.cards.Card;
import mage.constants.AsThoughEffectType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.game.Game;
import java.util.UUID;
/**
* @author LevelX2
*/
@ -65,7 +61,7 @@ public class PlayFromNotOwnHandZoneAllEffect extends AsThoughEffectImpl {
}
break;
}
return !onlyOwnedCards || card.getOwnerId().equals(source.getControllerId())
return (!onlyOwnedCards || card.getOwnerId().equals(source.getControllerId()))
&& filter.match(card, game)
&& game.getState().getZone(card.getId()).match(fromZone);
}

View file

@ -46,7 +46,7 @@ public class EchoEffect extends OneShotEffect {
Player controller = game.getPlayer(source.getControllerId());
if (controller != 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);
if (controller.chooseUse(Outcome.Benefit, "Pay {0} instead of the echo cost?", source, game)) {
altCost.clearPaid();

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.Mana;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCost;
@ -55,7 +56,7 @@ public class EmergeAbility extends SpellAbility {
new FilterControlledCreaturePermanent(), this.getControllerId(), this, game)) {
ManaCost costToPay = CardUtil.reduceCost(emergeCost.copy(), creature.getManaValue());
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) {
return blocker.getAbilities().containsKey(FlyingAbility.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));
}

View file

@ -65,18 +65,18 @@ class LandwalkEffect extends RestrictionEffect {
@Override
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
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()) {
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":
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":
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":
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":
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:
return false;
}

View file

@ -70,7 +70,7 @@ class ShadowEffect extends RestrictionEffect implements MageSingleton {
@Override
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
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

View file

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

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.cards.Card;
@ -54,7 +55,7 @@ public class SurgeAbility extends SpellAbility {
if (!player.hasOpponent(playerToCheckId, game)) {
if (watcher.getAmountOfSpellsPlayerCastOnCurrentTurn(playerToCheckId) > 0
&& 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
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

View file

@ -174,8 +174,8 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
Player player = game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : attacker.getControllerId());
if ((attacker.getAbilities().containsKey(DamageAsThoughNotBlockedAbility.getInstance().getId()) &&
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,
null, attacker.getControllerId(), game) != null) {
!game.getContinuousEffects().asThough(attacker.getId(), AsThoughEffectType.DAMAGE_NOT_BLOCKED,
null, attacker.getControllerId(), game).isEmpty()) {
// for handling creatures like Thorn Elemental
blocked = false;
unblockedDamage(first, game);

View file

@ -716,7 +716,7 @@ public class GameEvent implements Serializable {
if (approvingObject == null) {
return false;
}
if (identifier == null) {
if (identifier.equals(MageIdentifier.Default)) {
return false;
}
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) {
if (source != null) {
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;
}
}
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()
.filter(HexproofBaseAbility.class::isInstance)
.map(HexproofBaseAbility.class::cast)
@ -1416,10 +1416,10 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
// battles can never attack
return false;
}
ApprovingObject approvingObject = game.getContinuousEffects().asThough(
Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(
this.objectId, AsThoughEffectType.ATTACK_AS_HASTE, null, defenderId, game
);
if (hasSummoningSickness() && approvingObject == null) {
if (hasSummoningSickness() && approvingObjects.isEmpty()) {
return false;
}
//20101001 - 508.1c
@ -1435,7 +1435,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
}
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) {
@ -1455,7 +1455,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override
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;
}
Permanent attacker = game.getPermanent(attackerId);
@ -1488,7 +1488,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override
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;
}

View file

@ -1,9 +1,6 @@
package mage.players;
import mage.ApprovingObject;
import mage.MageItem;
import mage.MageObject;
import mage.Mana;
import mage.*;
import mage.abilities.*;
import mage.abilities.costs.AlternativeSourceCosts;
import mage.abilities.costs.Cost;
@ -1054,13 +1051,28 @@ public interface Player extends MageItem, Copyable<Player> {
* cost
* @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();

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)
// support multiple cards with alternative mana cost
protected Set<UUID> castSourceIdWithAlternateMana = new HashSet<>();
protected Map<UUID, ManaCosts<ManaCost>> castSourceIdManaCosts = new HashMap<>();
protected Map<UUID, Costs<Cost>> castSourceIdCosts = new HashMap<>();
//
// A card may be able to cast multiple way with multiple methods.
// 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
protected boolean payManaMode = false;
@ -279,13 +282,22 @@ public abstract class PlayerImpl implements Player, Serializable {
this.bufferTimeLeft = player.getBufferTimeLeft();
this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving;
this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana());
for (Entry<UUID, ManaCosts<ManaCost>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy()));
for (Entry<UUID, Set<MageIdentifier>> entry : player.getCastSourceIdWithAlternateMana().entrySet()) {
this.castSourceIdWithAlternateMana.put(entry.getKey(), (entry.getValue() == null ? null : new HashSet<>(entry.getValue())));
}
for (Entry<UUID, Costs<Cost>> entry : player.getCastSourceIdCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy()));
for (Entry<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> entry : player.getCastSourceIdManaCosts().entrySet()) {
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.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null;
for (Designation object : player.designations) {
@ -364,13 +376,20 @@ public abstract class PlayerImpl implements Player, Serializable {
this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving();
this.clearCastSourceIdManaCosts();
this.castSourceIdWithAlternateMana.clear();
this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana());
for (Entry<UUID, ManaCosts<ManaCost>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy()));
for (Entry<UUID, Set<MageIdentifier>> entry : player.getCastSourceIdWithAlternateMana().entrySet()) {
this.castSourceIdWithAlternateMana.put(entry.getKey(), (entry.getValue() == null ? null : new HashSet<>(entry.getValue())));
}
for (Entry<UUID, Costs<Cost>> entry : player.getCastSourceIdCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy()));
for (Entry<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> entry : player.getCastSourceIdManaCosts().entrySet()) {
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;
@ -636,13 +655,13 @@ public abstract class PlayerImpl implements Player, Serializable {
}
if (source != null) {
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;
}
if (sourceControllerId != null
&& 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()
.filter(HexproofBaseAbility.class::isInstance)
.map(HexproofBaseAbility.class::cast)
@ -1080,25 +1099,37 @@ public abstract class PlayerImpl implements Player, Serializable {
}
@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
castSourceIdWithAlternateMana.add(sourceId);
castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null);
castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null);
castSourceIdWithAlternateMana
.computeIfAbsent(sourceId, k -> new HashSet<>())
.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
public Set<UUID> getCastSourceIdWithAlternateMana() {
public Map<UUID, Set<MageIdentifier>> getCastSourceIdWithAlternateMana() {
return castSourceIdWithAlternateMana;
}
@Override
public Map<UUID, Costs<Cost>> getCastSourceIdCosts() {
public Map<UUID, Map<MageIdentifier, Costs<Cost>>> getCastSourceIdCosts() {
return castSourceIdCosts;
}
@Override
public Map<UUID, ManaCosts<ManaCost>> getCastSourceIdManaCosts() {
public Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> getCastSourceIdManaCosts() {
return castSourceIdManaCosts;
}
@ -1187,10 +1218,19 @@ public abstract class PlayerImpl implements Player, Serializable {
// ALTERNATIVE COST from dynamic effects
// 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();
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId());
Costs<Cost> costs = getCastSourceIdCosts().get(ability.getSourceId());
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()).get(identifier);
Costs<Cost> costs = getCastSourceIdCosts().get(ability.getSourceId()).get(identifier);
if (alternateCosts == null) {
noMana = true;
} 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
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();
// land events must return original zone (uses for commander watcher)
Zone cardZoneBefore = game.getState().getZone(card.getId());
GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND,
card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject());
card.getId(), playLandAbility, playerId, approvingResult.approvingObject);
landEventBefore.setZone(cardZoneBefore);
game.fireEvent(landEventBefore);
if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) {
incrementLandsPlayed();
GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED,
card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject());
card.getId(), playLandAbility, playerId, approvingResult.approvingObject);
landEventAfter.setZone(cardZoneBefore);
game.fireEvent(landEventAfter);
@ -1311,6 +1360,68 @@ public abstract class PlayerImpl implements Player, Serializable {
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) {
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY,
ability.getId(), ability, playerId))) {
@ -1463,7 +1574,16 @@ public abstract class PlayerImpl implements Player, Serializable {
result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game);
break;
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;
default:
result = playAbility(ability.copy(), game);
@ -3452,9 +3572,9 @@ public abstract class PlayerImpl implements Player, Serializable {
}
// ALTERNATIVE COST FROM dynamic effects
if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) {
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId());
Costs<Cost> costs = getCastSourceIdCosts().get(copy.getSourceId());
for(MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) {
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier);
Costs<Cost> costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier);
boolean canPutToPlay = true;
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.
// TODO: This needs to be improved to handle multiple approving objects.
// See https://github.com/magefree/mage/issues/8584
ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(),
Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(ability.getSourceId(),
AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game);
for (Mana mana : abilityOptions) {
if (mana.count() == 0) {
@ -3517,7 +3635,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// TODO: Describe this
// 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.
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
return true;
}
@ -3764,7 +3882,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// So make it available all the time
boolean canUse;
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);
} else {
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;
}
ApprovingObject approvingObject;
Set<ApprovingObject> approvingObjects;
if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) {
// 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);
if (approvingObject == null && isPlaySpell
if (approvingObjects.isEmpty() && isPlaySpell
&& ((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);
}
} else {
// other abilities from direct zones
approvingObject = null;
approvingObjects = new HashSet<>();
}
boolean canActivateAsHandZone = approvingObject != null
boolean canActivateAsHandZone = !approvingObjects.isEmpty()
|| (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard());
boolean possibleToPlay = canActivateAsHandZone
&& ability.getZone().match(Zone.HAND)
@ -4434,8 +4552,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override
public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) {
if (null != game.getContinuousEffects().asThough(card.getId(),
AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) {
if (!game.getContinuousEffects().asThough(card.getId(),
AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game).isEmpty()) {
// two modes: look at the card or do not look and activate other abilities
String lookMessage = "Look at " + card.getIdName();
String lookYes = "Yes, look at the card";

View file

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