diff --git a/Mage.Sets/src/mage/cards/s/SilverScrutiny.java b/Mage.Sets/src/mage/cards/s/SilverScrutiny.java new file mode 100644 index 0000000000..e0fb792ed9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SilverScrutiny.java @@ -0,0 +1,57 @@ +package mage.cards.s; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.CastAsThoughItHadFlashIfConditionAbility; +import mage.abilities.condition.Condition; +import mage.abilities.dynamicvalue.common.ManacostVariableValue; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.Game; +import mage.game.stack.Spell; + +import java.util.UUID; + +/** + * + * @author awjackson + */ +public final class SilverScrutiny extends CardImpl { + + public SilverScrutiny(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{U}{U}"); + + // You may cast Silver Scrutiny as though it had flash if X is 3 or less. + this.addAbility(new CastAsThoughItHadFlashIfConditionAbility( + SilverScrutinyCondition.instance, + "You may cast {this} as though it had flash if X is 3 or less." + )); + + // Draw X cards. + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(ManacostVariableValue.REGULAR)); + } + + private SilverScrutiny(final SilverScrutiny card) { + super(card); + } + + @Override + public SilverScrutiny copy() { + return new SilverScrutiny(this); + } +} + +enum SilverScrutinyCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + Spell spell = game.getStack().getSpell(source.getSourceId()); + if (spell == null) { + return false; + } + return spell.getStackAbility().getManaCostsToPay().getX() < 4; + } +} diff --git a/Mage.Sets/src/mage/cards/t/TimelyWard.java b/Mage.Sets/src/mage/cards/t/TimelyWard.java index 70e4341873..b0fa57a0c9 100644 --- a/Mage.Sets/src/mage/cards/t/TimelyWard.java +++ b/Mage.Sets/src/mage/cards/t/TimelyWard.java @@ -1,12 +1,11 @@ package mage.cards.t; import mage.abilities.Ability; +import mage.abilities.common.CastAsThoughItHadFlashIfConditionAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.Condition; import mage.abilities.condition.common.SourceTargetsPermanentCondition; -import mage.abilities.decorator.ConditionalAsThoughEffect; import mage.abilities.effects.common.AttachEffect; -import mage.abilities.effects.common.continuous.CastAsThoughItHadFlashSourceEffect; import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; import mage.abilities.keyword.EnchantAbility; import mage.abilities.keyword.IndestructibleAbility; @@ -39,9 +38,9 @@ public final class TimelyWard extends CardImpl { this.subtype.add(SubType.AURA); // You may cast this spell as though it had flash if it targets a commander. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new ConditionalAsThoughEffect( - new CastAsThoughItHadFlashSourceEffect(Duration.EndOfGame), condition - ).setText("you may cast this spell as though it had flash if it targets a commander"))); + this.addAbility(new CastAsThoughItHadFlashIfConditionAbility(condition, + "You may cast this spell as though it had flash if it targets a commander." + )); // Enchant creature TargetPermanent auraTarget = new TargetCreaturePermanent(); diff --git a/Mage.Sets/src/mage/sets/DominariaUnited.java b/Mage.Sets/src/mage/sets/DominariaUnited.java index 15d84ae818..787719a5fa 100644 --- a/Mage.Sets/src/mage/sets/DominariaUnited.java +++ b/Mage.Sets/src/mage/sets/DominariaUnited.java @@ -223,6 +223,7 @@ public final class DominariaUnited extends ExpansionSet { cards.add(new SetCardInfo("Shivan Devastator", 143, Rarity.MYTHIC, mage.cards.s.ShivanDevastator.class)); cards.add(new SetCardInfo("Shivan Reef", 255, Rarity.RARE, mage.cards.s.ShivanReef.class)); cards.add(new SetCardInfo("Shore Up", 64, Rarity.COMMON, mage.cards.s.ShoreUp.class)); + cards.add(new SetCardInfo("Silver Scrutiny", 65, Rarity.RARE, mage.cards.s.SilverScrutiny.class)); cards.add(new SetCardInfo("Silverback Elder", 177, Rarity.MYTHIC, mage.cards.s.SilverbackElder.class)); cards.add(new SetCardInfo("Slimefoot's Survey", 178, Rarity.UNCOMMON, mage.cards.s.SlimefootsSurvey.class)); cards.add(new SetCardInfo("Smash to Dust", 144, Rarity.COMMON, mage.cards.s.SmashToDust.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/restriction/MeddlingMageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/restriction/MeddlingMageTest.java index b0e8f70c73..987cad4598 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/restriction/MeddlingMageTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/restriction/MeddlingMageTest.java @@ -56,6 +56,7 @@ public class MeddlingMageTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{2}, {T}:"); setChoice(playerA, true); // create copy setChoice(playerA, true); // cast copy + addTarget(playerA, "Meddling Mage"); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index a91c197850..d27577ed0e 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -309,15 +309,6 @@ public abstract class AbilityImpl implements Ability { VariableManaCost variableManaCost = handleManaXCosts(game, noMana, controller); String announceString = handleOtherXCosts(game, controller); - // For effects from cards like Void Winnower x costs have to be set - if (this.getAbilityType() == AbilityType.SPELL) { - GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, this.getId(), this, getControllerId()); - castEvent.setZone(game.getState().getZone(CardUtil.getMainCardId(game, sourceId))); - if (game.replaceEvent(castEvent, this)) { - return false; - } - } - handlePhyrexianManaCosts(game, controller); /* 20130201 - 601.2b @@ -343,7 +334,7 @@ public abstract class AbilityImpl implements Ability { for (UUID modeId : this.getModes().getSelectedModes()) { this.getModes().setActiveMode(modeId); - //20121001 - 601.2c + // 20121001 - 601.2c // 601.2c The player announces their choice of an appropriate player, object, or zone for // each target the spell requires. A spell may require some targets only if an alternative or // additional cost (such as a buyback or kicker cost), or a particular mode, was chosen for it; @@ -376,6 +367,18 @@ public abstract class AbilityImpl implements Ability { } } // end modes + // 20220908 - 601.2e + // 601.2e The game checks to see if the proposed spell can legally be cast. If the proposed spell + // is illegal, the game returns to the moment before the casting of that spell was proposed + // (see rule 727, "Handling Illegal Actions"). + if (this.getAbilityType() == AbilityType.SPELL) { + GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL_LATE, this.getId(), this, getControllerId()); + castEvent.setZone(game.getState().getZone(CardUtil.getMainCardId(game, sourceId))); + if (game.replaceEvent(castEvent, this)) { + return false; + } + } + // this is a hack to prevent mana abilities with mana costs from causing endless loops - pay other costs first if (this instanceof ActivatedManaAbilityImpl && !costs.pay(this, game, this, controllerId, noMana, null)) { logger.debug("activate mana ability failed - non mana costs"); @@ -437,7 +440,7 @@ public abstract class AbilityImpl implements Ability { case MADNESS: case DISTURB: // from Snapcaster Mage: - // If you cast a spell from a graveyard using its flashback ability, you can’t pay other alternative costs + // If you cast a spell from a graveyard using its flashback ability, you can't pay other alternative costs // (such as that of Foil). (2018-12-07) canUseAlternativeCost = false; // You may pay any optional additional costs the spell has, such as kicker costs. You must pay any @@ -570,10 +573,10 @@ public abstract class AbilityImpl implements Ability { protected VariableManaCost handleManaXCosts(Game game, boolean noMana, Player controller) { // 20210723 - 601.2b // If the spell has alternative or additional costs that will - // be paid as it’s being cast such as buyback or kicker costs (see rules 118.8 and 118.9), + // be paid as it's being cast such as buyback or kicker costs (see rules 118.8 and 118.9), // the player announces their intentions to pay any or all of those costs (see rule 601.2f). - // A player can’t apply two alternative methods of casting or two alternative costs to a - // single spell. If the spell has a variable cost that will be paid as it’s being cast + // A player can't apply two alternative methods of casting or two alternative costs to a + // single spell. If the spell has a variable cost that will be paid as it's being cast // (such as an {X} in its mana cost; see rule 107.3), the player announces the value of that // variable. If the value of that variable is defined in the text of the spell by a choice // that player would make later in the announcement or resolution of the spell, that player diff --git a/Mage/src/main/java/mage/abilities/common/CastAsThoughItHadFlashIfConditionAbility.java b/Mage/src/main/java/mage/abilities/common/CastAsThoughItHadFlashIfConditionAbility.java new file mode 100644 index 0000000000..4b10a7e329 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/CastAsThoughItHadFlashIfConditionAbility.java @@ -0,0 +1,89 @@ +package mage.abilities.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.abilities.effects.common.continuous.CastAsThoughItHadFlashSourceEffect; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; + +/** + * Implements: + * You may cast {this} as though it had flash if [condition that depends on X value, targets, etc.] + * + * @author awjackson + */ +public class CastAsThoughItHadFlashIfConditionAbility extends SimpleStaticAbility { + + private final String rule; + + public CastAsThoughItHadFlashIfConditionAbility(Condition condition, String rule) { + super(Zone.ALL, new CastAsThoughItHadFlashSourceEffect(Duration.EndOfGame)); + this.addEffect(new CantFlashUnlessConditionEffect(condition)); + this.setRuleAtTheTop(true); + this.rule = rule; + } + + private CastAsThoughItHadFlashIfConditionAbility(final CastAsThoughItHadFlashIfConditionAbility ability) { + super(ability); + this.rule = ability.rule; + } + + @Override + public CastAsThoughItHadFlashIfConditionAbility copy() { + return new CastAsThoughItHadFlashIfConditionAbility(this); + } + + @Override + public String getRule() { + return rule; + } +} + +class CantFlashUnlessConditionEffect extends ContinuousRuleModifyingEffectImpl { + + private final Condition condition; + + public CantFlashUnlessConditionEffect(Condition condition) { + super(Duration.EndOfGame, Outcome.Neutral); + this.condition = condition; + } + + private CantFlashUnlessConditionEffect(final CantFlashUnlessConditionEffect effect) { + super(effect); + this.condition = effect.condition; + } + + @Override + public CantFlashUnlessConditionEffect copy() { + return new CantFlashUnlessConditionEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CAST_SPELL_LATE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (!event.getSourceId().equals(source.getSourceId())) { + return false; + } + // the condition can't be evaluated until the spell is on the stack + if (game.inCheckPlayableState()) { + return false; + } + // ignore if casting as a sorcery + if (game.isMainPhase() && game.isActivePlayer(event.getPlayerId()) && game.getStack().size() == 1) { + return false; + } + // TODO: this is a hack and doesn't handle all other ways a spell could be cast as though it had flash + if (Boolean.TRUE.equals(game.getState().getValue("PlayFromNotOwnHandZone" + source.getSourceId()))) { + return false; + } + return !condition.apply(game, source); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/SacrificeIfCastAtInstantTimeTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/SacrificeIfCastAtInstantTimeTriggeredAbility.java index 6cb592522c..2436d1bebf 100644 --- a/Mage/src/main/java/mage/abilities/common/SacrificeIfCastAtInstantTimeTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/SacrificeIfCastAtInstantTimeTriggeredAbility.java @@ -38,14 +38,18 @@ public class SacrificeIfCastAtInstantTimeTriggeredAbility extends TriggeredAbili @Override public boolean checkTrigger(GameEvent event, Game game) { - // The sacrifice occurs only if you cast it using its own ability. If you cast it using some other - // effect (for instance, if it gained flash from Vedalken Orrery), then it won't be sacrificed. - // CHECK + // TODO: The sacrifice should occur only if you cast it using its own ability. If you cast it using some + // other effect (for instance, if it gained flash from Vedalken Orrery), then it shouldn't be sacrificed. + // see https://github.com/magefree/mage/issues/9512 Spell spell = game.getStack().getSpell(event.getTargetId()); - if (spell != null && spell.getSourceId().equals(getSourceId())) { - return !(game.isMainPhase() && game.isActivePlayer(event.getPlayerId()) && game.getStack().size() == 1); + if (spell == null || !spell.getSourceId().equals(getSourceId())) { + return false; } - return false; + // TODO: this is a hack and doesn't handle all other ways a spell could be cast as though it had flash + if (Boolean.TRUE.equals(game.getState().getValue("PlayFromNotOwnHandZone" + getSourceId()))) { + return false; + } + return !(game.isMainPhase() && game.isActivePlayer(event.getPlayerId()) && game.getStack().size() == 1); } @Override