From 0b2f582d84f142a1caf793e8431c89280f2da356 Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Thu, 8 Jun 2023 13:58:28 -0700 Subject: [PATCH] Added Storm of Saruman card (#10433) * Added Storm of Saruman card Some classes have been added/adjusted for code reusability: - CastSecondSpellTriggeredAbility has been modified to set a target pointer to either the caster or the spell (used here to set a target pointer to the spell for the copy effect) - CopyTargetSpellEffect has been modified to allow specifying a copy applier (used here to apply the legenedary-stripping effect) - RemoveTypeCopyApplier has been added as a generic copy applier for any cards which read "except it isn't " * Fixed verify failure - Remove ward hint on Storm of Saruman * Fixed a typo - ammount -> amount * Modified Double Major to use new CopyTargetSpellEffect * Re-added ability text for Double Major --- Mage.Sets/src/mage/cards/d/DoubleMajor.java | 61 ++--------------- .../src/mage/cards/s/StormOfSaruman.java | 41 ++++++++++++ .../TheLordOfTheRingsTalesOfMiddleEarth.java | 1 + .../cards/single/ltr/StormOfSarumanTest.java | 45 +++++++++++++ .../CastSecondSpellTriggeredAbility.java | 30 +++++++++ .../effects/common/CopyTargetSpellEffect.java | 29 +++++++- .../util/functions/RemoveTypeCopyApplier.java | 67 +++++++++++++++++++ 7 files changed, 217 insertions(+), 57 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/s/StormOfSaruman.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/StormOfSarumanTest.java create mode 100644 Mage/src/main/java/mage/util/functions/RemoveTypeCopyApplier.java diff --git a/Mage.Sets/src/mage/cards/d/DoubleMajor.java b/Mage.Sets/src/mage/cards/d/DoubleMajor.java index 5e6fa17dd52..f204639a97f 100644 --- a/Mage.Sets/src/mage/cards/d/DoubleMajor.java +++ b/Mage.Sets/src/mage/cards/d/DoubleMajor.java @@ -1,21 +1,15 @@ package mage.cards.d; -import mage.abilities.Ability; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CopyTargetSpellEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.SuperType; import mage.constants.TargetController; import mage.filter.FilterSpell; import mage.filter.common.FilterCreatureSpell; -import mage.filter.predicate.mageobject.MageObjectReferencePredicate; -import mage.game.Game; -import mage.game.stack.Spell; -import mage.game.stack.StackObject; import mage.target.TargetSpell; -import mage.util.functions.StackObjectCopyApplier; +import mage.util.functions.RemoveTypeCopyApplier; import java.util.UUID; @@ -34,7 +28,10 @@ public final class DoubleMajor extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{G}{U}"); // Copy target creature spell you control, except it isn't legendary if the spell is legendary. - this.getSpellAbility().addEffect(new DoubleMajorEffect()); + this.getSpellAbility().addEffect( + new CopyTargetSpellEffect(false, false, false, 1, new RemoveTypeCopyApplier(SuperType.LEGENDARY)) + .setText( + "Copy target creature spell you control, except it isn't legendary if the spell is legendary.")); this.getSpellAbility().addTarget(new TargetSpell(filter)); } @@ -46,48 +43,4 @@ public final class DoubleMajor extends CardImpl { public DoubleMajor copy() { return new DoubleMajor(this); } -} - -class DoubleMajorEffect extends OneShotEffect { - - DoubleMajorEffect() { - super(Outcome.Copy); - staticText = "copy target creature spell you control, except it isn't legendary if the spell is legendary"; - } - - private DoubleMajorEffect(final DoubleMajorEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - Spell spell = game.getSpell(source.getFirstTarget()); - if (spell == null) { - return false; - } - spell.createCopyOnStack( - game, source, source.getControllerId(), - false, 1, DoubleMajorApplier.instance - ); - return true; - } - - @Override - public DoubleMajorEffect copy() { - return new DoubleMajorEffect(this); - } -} - -enum DoubleMajorApplier implements StackObjectCopyApplier { - instance; - - @Override - public void modifySpell(StackObject stackObject, Game game) { - stackObject.removeSuperType(SuperType.LEGENDARY); - } - - @Override - public MageObjectReferencePredicate getNextNewTargetType() { - return null; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/StormOfSaruman.java b/Mage.Sets/src/mage/cards/s/StormOfSaruman.java new file mode 100644 index 00000000000..cb66c5c78ba --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StormOfSaruman.java @@ -0,0 +1,41 @@ +package mage.cards.s; + +import mage.abilities.common.CastSecondSpellTriggeredAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.CopyTargetSpellEffect; +import mage.abilities.keyword.WardAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.util.functions.RemoveTypeCopyApplier; + +import java.util.UUID; + +/** + * @author alexander-novo + */ +public final class StormOfSaruman extends CardImpl { + + public StormOfSaruman(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[] { CardType.ENCHANTMENT }, "{4}{U}{U}"); + + // Ward {3} + this.addAbility(new WardAbility(new GenericManaCost(3), false)); + + // Whenever you cast your second spell each turn, copy it, except the copy isn't legendary. You may choose new targets for the copy. + this.addAbility(new CastSecondSpellTriggeredAbility(Zone.BATTLEFIELD, + new CopyTargetSpellEffect(false, true, true, 1, new RemoveTypeCopyApplier(SuperType.LEGENDARY)) + .setText("copy it, except the copy isn't legendary. You may choose new targets for the copy."), + TargetController.YOU, false, + SetTargetPointer.SPELL)); + } + + private StormOfSaruman(final StormOfSaruman card) { + super(card); + } + + @Override + public StormOfSaruman copy() { + return new StormOfSaruman(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java b/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java index f186fe67acd..9b359211ada 100644 --- a/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java +++ b/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java @@ -122,6 +122,7 @@ public final class TheLordOfTheRingsTalesOfMiddleEarth extends ExpansionSet { cards.add(new SetCardInfo("Snarling Warg", 109, Rarity.COMMON, mage.cards.s.SnarlingWarg.class)); cards.add(new SetCardInfo("Stern Scolding", 71, Rarity.UNCOMMON, mage.cards.s.SternScolding.class)); cards.add(new SetCardInfo("Stew the Coneys", 189, Rarity.UNCOMMON, mage.cards.s.StewTheConeys.class)); + cards.add(new SetCardInfo("Storm of Saruman", 72, Rarity.MYTHIC, mage.cards.s.StormOfSaruman.class)); cards.add(new SetCardInfo("Surrounded by Orcs", 73, Rarity.COMMON, mage.cards.s.SurroundedByOrcs.class)); cards.add(new SetCardInfo("Swamp", 266, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Swarming of Moria", 150, Rarity.COMMON, mage.cards.s.SwarmingOfMoria.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/StormOfSarumanTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/StormOfSarumanTest.java new file mode 100644 index 00000000000..bdcfa9cb726 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/StormOfSarumanTest.java @@ -0,0 +1,45 @@ +package org.mage.test.cards.single.ltr; + +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import mage.constants.PhaseStep; +import mage.constants.Zone; + +public class StormOfSarumanTest extends CardTestPlayerBase { + + static final String storm = "Storm of Saruman"; + + @Test + // Author: alexander-novo + // A test for basic functionality of the card - makes sure it copies cards and makes them nonlegendary + public void testCopiesNonLegendary() { + String hound = "Isamaru, Hound of Konda"; + String bolt = "Lightning Bolt"; + + // Bolt will be our first spell - to make sure it doesn't trigger + // Isamaru will be our second spell - to make sure we get two, since it's legendary + addCard(Zone.HAND, playerA, bolt, 1); + addCard(Zone.HAND, playerA, hound, 1); + addCard(Zone.BATTLEFIELD, playerA, storm, 1); + + // The mana needed to cast those spells + addCard(Zone.BATTLEFIELD, playerA, "mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "plains", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB); + checkStackObject("Bolt check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "Whenever you cast your second spell each turn", 0); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hound); + checkStackObject("Hound check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "Whenever you cast your second spell each turn", 1); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerB, 17); + assertPermanentCount(playerA, hound, 2); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/CastSecondSpellTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/CastSecondSpellTriggeredAbility.java index 28baae38942..59e21abf841 100644 --- a/Mage/src/main/java/mage/abilities/common/CastSecondSpellTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/CastSecondSpellTriggeredAbility.java @@ -6,10 +6,12 @@ import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.abilities.hint.Hint; import mage.abilities.hint.ValueHint; +import mage.constants.SetTargetPointer; import mage.constants.TargetController; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; +import mage.target.targetpointer.FixedTarget; import mage.watchers.common.CastSpellLastTurnWatcher; /** @@ -19,6 +21,7 @@ public class CastSecondSpellTriggeredAbility extends TriggeredAbilityImpl { private static final Hint hint = new ValueHint("Spells you cast this turn", SpellCastValue.instance); private final TargetController targetController; + private final SetTargetPointer setTargetPointer; public CastSecondSpellTriggeredAbility(Effect effect) { this(effect, TargetController.YOU); @@ -29,18 +32,33 @@ public class CastSecondSpellTriggeredAbility extends TriggeredAbilityImpl { } public CastSecondSpellTriggeredAbility(Zone zone, Effect effect, TargetController targetController, boolean optional) { + this(zone, effect, targetController, optional, SetTargetPointer.NONE); + } + + /** + * + * @param zone What zone the ability can trigger from (see {@link mage.abilities.Ability#getZone}) + * @param effect What effect will happen when this ability triggers (see {@link mage.abilities.Ability#getEffects}) + * @param targetController Which player(s) to pay attention to + * @param optional Whether the effect is optional (see {@link mage.abilities.TriggeredAbility#isOptional}) + * @param setTargetPointer Who to set the target pointer of the effects to. Only accepts NONE, PLAYER (the player who cast the spell), and SPELL (the spell which was cast) + */ + public CastSecondSpellTriggeredAbility(Zone zone, Effect effect, TargetController targetController, + boolean optional, SetTargetPointer setTargetPointer) { super(zone, effect, optional); this.addWatcher(new CastSpellLastTurnWatcher()); if (targetController == TargetController.YOU) { this.addHint(hint); } this.targetController = targetController; + this.setTargetPointer = setTargetPointer; setTriggerPhrase(generateTriggerPhrase()); } private CastSecondSpellTriggeredAbility(final CastSecondSpellTriggeredAbility ability) { super(ability); this.targetController = ability.targetController; + this.setTargetPointer = ability.setTargetPointer; } @Override @@ -73,6 +91,18 @@ public class CastSecondSpellTriggeredAbility extends TriggeredAbilityImpl { CastSpellLastTurnWatcher watcher = game.getState().getWatcher(CastSpellLastTurnWatcher.class); if (watcher != null && watcher.getAmountOfSpellsPlayerCastOnCurrentTurn(event.getPlayerId()) == 2) { this.getEffects().setValue("spellCast", game.getSpell(event.getTargetId())); + switch (this.setTargetPointer) { + case PLAYER: + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + break; + case SPELL: + this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId())); + break; + case NONE: + break; + default: + throw new IllegalArgumentException("SetTargetPointer " + this.setTargetPointer + " not supported"); + } return true; } return false; diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopyTargetSpellEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopyTargetSpellEffect.java index 734a639171a..5d7faeec980 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopyTargetSpellEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopyTargetSpellEffect.java @@ -8,8 +8,7 @@ import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; import mage.game.stack.Spell; -import mage.game.stack.StackObject; -import mage.players.Player; +import mage.util.functions.StackObjectCopyApplier; /** * @author BetaSteward_at_googlemail.com @@ -20,6 +19,8 @@ public class CopyTargetSpellEffect extends OneShotEffect { private final boolean useLKI; private String copyThatSpellName = "that spell"; private final boolean chooseTargets; + private final int amount; + private final StackObjectCopyApplier applier; public CopyTargetSpellEffect() { this(false); @@ -34,10 +35,29 @@ public class CopyTargetSpellEffect extends OneShotEffect { } public CopyTargetSpellEffect(boolean useController, boolean useLKI, boolean chooseTargets) { + this(useController, useLKI, chooseTargets, 1); + } + + public CopyTargetSpellEffect(boolean useController, boolean useLKI, boolean chooseTargets, int amount) { + this(useController, useLKI, chooseTargets, amount, null); + } + + /** + * + * @param useController Whether to create the copy under the control of the original spell's controller (true) or the controller of the ability that this effect is on (false) + * @param useLKI Whether to get last-known information about the spell before resolving the effect (for instance for abilities which don't target a spell but reference it some other way) + * @param chooseTargets Whether the new copy and choose new targets + * @param amount The amount of copies to create + * @param applier An applier to apply to the newly created copies. Used to change copiable values of the copy, such as types or name + */ + public CopyTargetSpellEffect(boolean useController, boolean useLKI, boolean chooseTargets, int amount, + StackObjectCopyApplier applier) { super(Outcome.Copy); this.useController = useController; this.useLKI = useLKI; this.chooseTargets = chooseTargets; + this.amount = amount; + this.applier = applier; } public CopyTargetSpellEffect(final CopyTargetSpellEffect effect) { @@ -46,6 +66,8 @@ public class CopyTargetSpellEffect extends OneShotEffect { this.useController = effect.useController; this.copyThatSpellName = effect.copyThatSpellName; this.chooseTargets = effect.chooseTargets; + this.amount = effect.amount; + this.applier = effect.applier; } public Effect withSpellName(String copyThatSpellName) { @@ -65,7 +87,8 @@ public class CopyTargetSpellEffect extends OneShotEffect { spell = (Spell) game.getLastKnownInformation(targetPointer.getFirst(game, source), Zone.STACK); } if (spell != null) { - spell.createCopyOnStack(game, source, useController ? spell.getControllerId() : source.getControllerId(), chooseTargets); + spell.createCopyOnStack(game, source, useController ? spell.getControllerId() : source.getControllerId(), + chooseTargets, amount, applier); return true; } return false; diff --git a/Mage/src/main/java/mage/util/functions/RemoveTypeCopyApplier.java b/Mage/src/main/java/mage/util/functions/RemoveTypeCopyApplier.java new file mode 100644 index 00000000000..5a09598a85a --- /dev/null +++ b/Mage/src/main/java/mage/util/functions/RemoveTypeCopyApplier.java @@ -0,0 +1,67 @@ +package mage.util.functions; + +import java.util.UUID; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.predicate.mageobject.MageObjectReferencePredicate; +import mage.game.Game; +import mage.game.stack.StackObject; + +public class RemoveTypeCopyApplier extends CopyApplier implements StackObjectCopyApplier { + + private final CardType type; + private final SuperType superType; + private final SubType subType; + + public RemoveTypeCopyApplier(CardType type) { + this.type = type; + this.superType = null; + this.subType = null; + } + + public RemoveTypeCopyApplier(SuperType superType) { + this.superType = superType; + this.subType = null; + this.type = null; + } + + public RemoveTypeCopyApplier(SubType subType) { + this.subType = subType; + this.superType = null; + this.type = null; + } + + @Override + public boolean apply(Game game, MageObject blueprint, Ability source, UUID targetObjectId) { + if (type != null && blueprint.getCardType().contains(type)) { + blueprint.getCardType().remove(type); + } else if (superType != null && blueprint.getSuperType().contains(superType)) { + blueprint.getSuperType().remove(superType); + } else if (subType != null && blueprint.getSubtype().contains(subType)) { + blueprint.getSubtype().remove(subType); + } + + return true; + } + + @Override + public void modifySpell(StackObject stackObject, Game game) { + if (type != null && stackObject.getCardType().contains(type)) { + stackObject.getCardType().remove(type); + } else if (superType != null && stackObject.getSuperType().contains(superType)) { + stackObject.getSuperType().remove(superType); + } else if (subType != null && stackObject.getSubtype().contains(subType)) { + stackObject.getSubtype().remove(subType); + } + } + + @Override + public MageObjectReferencePredicate getNextNewTargetType() { + return null; + } + +}