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 <type>"

* 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
This commit is contained in:
Alexander Novotny 2023-06-08 13:58:28 -07:00 committed by GitHub
parent 49075d6893
commit 0b2f582d84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 217 additions and 57 deletions

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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));

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
}