From 2f154772148b2e4e54c517dcbc645d680acc44f9 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Fri, 5 Sep 2025 12:40:38 -0500 Subject: [PATCH] [SPM] implement Spider-Verse --- Mage.Sets/src/mage/cards/s/SpiderVerse.java | 113 ++++++++++++++++++ Mage.Sets/src/mage/sets/MarvelsSpiderMan.java | 2 + .../cards/single/spm/SpiderVerseTest.java | 81 +++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SpiderVerse.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/spm/SpiderVerseTest.java diff --git a/Mage.Sets/src/mage/cards/s/SpiderVerse.java b/Mage.Sets/src/mage/cards/s/SpiderVerse.java new file mode 100644 index 00000000000..772030ca421 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SpiderVerse.java @@ -0,0 +1,113 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ruleModifying.LegendRuleDoesntApplyEffect; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterSpell; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.card.CastFromZonePredicate; +import mage.filter.predicate.mageobject.MageObjectReferencePredicate; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; +import mage.util.functions.StackObjectCopyApplier; + +import java.util.UUID; + +/** + * + * @author Jmlundeen + */ +public final class SpiderVerse extends CardImpl { + + private static final FilterControlledCreaturePermanent creatureFilter = new FilterControlledCreaturePermanent("Spiders you control"); + + private static final FilterSpell spellFilter = new FilterSpell("a spell from anywhere other than your hand"); + + static { + creatureFilter.add(SubType.SPIDER.getPredicate()); + spellFilter.add(Predicates.not(new CastFromZonePredicate(Zone.HAND))); + } + + public SpiderVerse(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{R}{R}"); + + + // The "legend rule" doesn't apply to Spiders you control. + this.addAbility(new SimpleStaticAbility(new LegendRuleDoesntApplyEffect(creatureFilter))); + + // Whenever you cast a spell from anywhere other than your hand, you may copy it. If you do, you may choose new targets for the copy. If the copy is a permanent spell, it gains haste. Do this only once each turn. + this.addAbility(new SpellCastControllerTriggeredAbility( + new SpiderVerseEffect(), + true + ).setDoOnlyOnceEachTurn(true) + ); + } + + private SpiderVerse(final SpiderVerse card) { + super(card); + } + + @Override + public SpiderVerse copy() { + return new SpiderVerse(this); + } +} + +class SpiderVerseEffect extends OneShotEffect { + + SpiderVerseEffect() { + super(Outcome.Benefit); + staticText = "copy it. If you do, you may choose new targets for the copy. " + + "If the copy is a permanent spell, it gains haste"; + } + + protected SpiderVerseEffect(final SpiderVerseEffect effect) { + super(effect); + } + + @Override + public SpiderVerseEffect copy() { + return new SpiderVerseEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Spell spell = (Spell) getValue("spellCast"); + if (spell == null) { + return false; + } + spell.createCopyOnStack( + game, source, source.getControllerId(), true, + 1, SpiderVerseCopyApplier.instance); + return true; + } +} + +enum SpiderVerseCopyApplier implements StackObjectCopyApplier { + instance; + + @Override + public void modifySpell(StackObject stackObject, Game game) { + if (!stackObject.isPermanent(game)) { + return; + } + Spell spell = (Spell) stackObject; + spell.addAbilityForCopy(HasteAbility.getInstance()); + } + + @Override + public MageObjectReferencePredicate getNextNewTargetType() { + return null; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java b/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java index d935624027e..b25be686847 100644 --- a/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java +++ b/Mage.Sets/src/mage/sets/MarvelsSpiderMan.java @@ -244,6 +244,8 @@ public final class MarvelsSpiderMan extends ExpansionSet { cards.add(new SetCardInfo("Spider-Slayer, Hatred Honed", 175, Rarity.UNCOMMON, mage.cards.s.SpiderSlayerHatredHoned.class)); cards.add(new SetCardInfo("Spider-Suit", 176, Rarity.UNCOMMON, mage.cards.s.SpiderSuit.class)); cards.add(new SetCardInfo("Spider-UK", 17, Rarity.UNCOMMON, mage.cards.s.SpiderUK.class)); + cards.add(new SetCardInfo("Spider-Verse", 263, Rarity.MYTHIC, mage.cards.s.SpiderVerse.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Spider-Verse", 93, Rarity.MYTHIC, mage.cards.s.SpiderVerse.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Spider-Woman, Stunning Savior", 152, Rarity.RARE, mage.cards.s.SpiderWomanStunningSavior.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Spider-Woman, Stunning Savior", 230, Rarity.RARE, mage.cards.s.SpiderWomanStunningSavior.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Spiders-Man, Heroic Horde", 117, Rarity.UNCOMMON, mage.cards.s.SpidersManHeroicHorde.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/spm/SpiderVerseTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/spm/SpiderVerseTest.java new file mode 100644 index 00000000000..91bdd0de6e5 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/spm/SpiderVerseTest.java @@ -0,0 +1,81 @@ +package org.mage.test.cards.single.spm; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +/** + * + * @author Jmlundeen + */ +public class SpiderVerseTest extends CardTestCommander4Players { + + /* + Spider-Verse + {3}{R}{R} + Enchantment + The "legend rule" doesn't apply to Spiders you control. + Whenever you cast a spell from anywhere other than your hand, you may copy it. If you do, you may choose new targets for the copy. If the copy is a permanent spell, it gains haste. Do this only once each turn. + */ + private static final String spiderVerse = "Spider-Verse"; + + /* + Lightning Bolt + {R} + Instant + Lightning Bolt deals 3 damage to any target. + */ + private static final String lightningBolt = "Lightning Bolt"; + + /* + Bear Cub + {1}{G} + Creature - Bear + + 2/2 + */ + private static final String bearCub = "Bear Cub"; + + /* + Ashiok, Nightmare Muse + {3}{U}{B} + Legendary Planeswalker - Ashiok + + +1: Create a 2/3 blue and black Nightmare creature token with "Whenever this creature attacks or blocks, each opponent exiles the top two cards of their library." + -3: Return target nonland permanent to its owner's hand, then that player exiles a card from their hand. + -7: You may cast up to three face-up cards your opponents own from exile without paying their mana costs. + */ + private static final String ashiokNightmareMuse = "Ashiok, Nightmare Muse"; + + @Test + public void testSpiderVerse() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, spiderVerse); + addCard(Zone.BATTLEFIELD, playerA, ashiokNightmareMuse); + addCard(Zone.EXILED, playerB, lightningBolt); + addCard(Zone.EXILED, playerB, bearCub); + + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, ashiokNightmareMuse, CounterType.LOYALTY, 9); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-7"); + setChoice(playerA, lightningBolt); + setChoice(playerA, true); + addTarget(playerA, playerD); + setChoice(playerA, false, 2); + setChoice(playerA, true, 2); // use spider-verse / new target + addTarget(playerA, playerB); + + activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "-7"); + setChoice(playerA, true, 2); // ashiok + spider-verse + attack(5, playerA, bearCub, playerC); // copy has haste + + setStopAt(5, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 3); + assertLife(playerD, 20 - 3); + assertLife(playerC, 20 - 2); + } +} \ No newline at end of file