[SPM] implement The Death of Gwen Stacy and update ExileGraveyardTargetPlayerEffect (#13955)

* refactor ExileGraveyardAllTargetPlayerEffect to allow multiple targets

* [SPM] implement The Death of Gwen Stacy

* change ExileGraveyardAllTargetPlayerEffect to do one batch movement
This commit is contained in:
Jmlundeen 2025-09-07 16:50:21 -05:00 committed by GitHub
parent d46ccdd8bc
commit d2a7991f8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 209 additions and 99 deletions

View file

@ -40,7 +40,7 @@ public final class CallousBloodmage extends CardImpl {
ability.addMode(mode);
// Exile target player's graveyard.
mode = new Mode(new ExileGraveyardAllTargetPlayerEffect().setText("exile target player's graveyard"));
mode = new Mode(new ExileGraveyardAllTargetPlayerEffect());
mode.addTarget(new TargetPlayer());
ability.addMode(mode);
this.addAbility(ability);

View file

@ -35,13 +35,12 @@ public final class DiscipleOfPerdition extends CardImpl {
// When Disciple of Perdition dies, choose one. If you have exactly 13 life, you may choose both.
// * You draw a card and you lose 1 life.
Ability ability = new DiesSourceTriggeredAbility(new DrawCardSourceControllerEffect(1, true), false);
ability.getModes().setChooseText("choose one. If you have exactly 13 life, you may choose both.");
ability.getModes().setChooseText("choose one. If you have exactly 13 life, you may choose both instead.");
ability.getModes().setMoreCondition(2, new LifeCompareCondition(TargetController.YOU, ComparisonType.EQUAL_TO, 13));
ability.addEffect(new LoseLifeSourceControllerEffect(1).concatBy("and"));
// * Exile target opponent's graveyard. That player loses 1 life.
ability.addMode(new Mode(new ExileGraveyardAllTargetPlayerEffect()
.setText("Exile target opponent's graveyard"))
ability.addMode(new Mode(new ExileGraveyardAllTargetPlayerEffect())
.addEffect(new LoseLifeTargetEffect(1).setText("that player loses 1 life"))
.addTarget(new TargetOpponent()));
this.addAbility(ability);

View file

@ -58,8 +58,7 @@ public final class ElspethsNightmare extends CardImpl {
// III - Exile target opponent's graveyard.
sagaAbility.addChapterEffect(
this, SagaChapter.CHAPTER_III, SagaChapter.CHAPTER_III,
new ExileGraveyardAllTargetPlayerEffect()
.setText("exile target opponent's graveyard"),
new ExileGraveyardAllTargetPlayerEffect(),
new TargetOpponent()
);
this.addAbility(sagaAbility);

View file

@ -6,21 +6,15 @@ import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.SacrificeSourceCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.ExileGraveyardAllTargetPlayerEffect;
import mage.abilities.mana.SimpleManaAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetPlayer;
import java.util.Objects;
import java.util.UUID;
/**
@ -35,7 +29,7 @@ public final class StonespeakerCrystal extends CardImpl {
this.addAbility(new SimpleManaAbility(Zone.BATTLEFIELD, Mana.ColorlessMana(2), new TapSourceCost()));
// {2}, {T}, Sacrifice Stonespeaker Crystal: Exile any number of target players' graveyards. Draw a card.
Ability ability = new SimpleActivatedAbility(new StonespeakerCrystalEffect(), new GenericManaCost(2));
Ability ability = new SimpleActivatedAbility(new ExileGraveyardAllTargetPlayerEffect(), new GenericManaCost(2));
ability.addEffect(new DrawCardSourceControllerEffect(1));
ability.addCost(new TapSourceCost());
ability.addCost(new SacrificeSourceCost());
@ -52,38 +46,3 @@ public final class StonespeakerCrystal extends CardImpl {
return new StonespeakerCrystal(this);
}
}
class StonespeakerCrystalEffect extends OneShotEffect {
StonespeakerCrystalEffect() {
super(Outcome.Benefit);
staticText = "exile any number of target players' graveyards";
}
private StonespeakerCrystalEffect(final StonespeakerCrystalEffect effect) {
super(effect);
}
@Override
public StonespeakerCrystalEffect copy() {
return new StonespeakerCrystalEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
Cards cards = new CardsImpl();
this.getTargetPointer()
.getTargets(game, source)
.stream()
.map(game::getPlayer)
.filter(Objects::nonNull)
.map(Player::getGraveyard)
.forEach(cards::addAll);
controller.moveCards(cards, Zone.EXILED, source, game);
return true;
}
}

View file

@ -0,0 +1,116 @@
package mage.cards.t;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.SagaAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.DestroyTargetEffect;
import mage.abilities.effects.common.ExileGraveyardAllTargetPlayerEffect;
import mage.cards.*;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.players.PlayerList;
import mage.target.Target;
import mage.target.TargetPlayer;
import mage.target.common.TargetCreaturePermanent;
import mage.target.common.TargetDiscard;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
*
* @author Jmlundeen
*/
public final class TheDeathOfGwenStacy extends CardImpl {
public TheDeathOfGwenStacy(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{B}");
this.subtype.add(SubType.SAGA);
// (As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)
SagaAbility sagaAbility = new SagaAbility(this);
// I -- Destroy target creature.
sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_I, new DestroyTargetEffect(), new TargetCreaturePermanent());
// II -- Each player may discard a card. Each player who doesn't loses 3 life.
sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_II, new TheDeathOfGwenStacyEffect());
// III -- Exile any number of target players' graveyards.
sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_III, new ExileGraveyardAllTargetPlayerEffect(),
new TargetPlayer(0, Integer.MAX_VALUE, false));
this.addAbility(sagaAbility);
}
private TheDeathOfGwenStacy(final TheDeathOfGwenStacy card) {
super(card);
}
@Override
public TheDeathOfGwenStacy copy() {
return new TheDeathOfGwenStacy(this);
}
}
class TheDeathOfGwenStacyEffect extends OneShotEffect {
TheDeathOfGwenStacyEffect() {
super(Outcome.Neutral);
this.staticText = "each player may discard a card. Each player who doesn't loses 3 life";
}
private TheDeathOfGwenStacyEffect(final TheDeathOfGwenStacyEffect effect) {
super(effect);
}
@Override
public TheDeathOfGwenStacyEffect copy() {
return new TheDeathOfGwenStacyEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
MageObject sourceObject = source.getSourceObject(game);
if (controller == null || sourceObject == null) {
return false;
}
// Store for each player the cards to discard, that's important because all discard shall happen at the same time
Map<UUID, Cards> cardsToDiscard = new HashMap<>();
PlayerList playersInRange = game.getState().getPlayersInRange(controller.getId(), game);
// choose cards to discard
for (UUID playerId : playersInRange) {
Player player = game.getPlayer(playerId);
if (player != null) {
Target target = new TargetDiscard(0, 1, new FilterCard(), playerId)
.withChooseHint("Choose a card to discard or lose 3 life");
player.chooseTarget(outcome, target, source, game);
Cards cards = new CardsImpl(target.getTargets());
cardsToDiscard.put(playerId, cards);
}
}
// discard all chosen cards
for (UUID playerId : playersInRange) {
Player player = game.getPlayer(playerId);
if (player != null) {
Cards cardsPlayer = cardsToDiscard.get(playerId);
if (cardsPlayer != null && !cardsPlayer.isEmpty()) {
for (UUID cardId : cardsPlayer) {
Card card = game.getCard(cardId);
player.discard(card, false, source, game);
}
} else {
player.loseLife(3, game, source, false);
}
}
}
return true;
}
}

View file

@ -1,28 +1,20 @@
package mage.cards.t;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.MultipliedValue;
import mage.abilities.dynamicvalue.common.CreaturesYouControlCount;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.abilities.effects.common.DestroyTargetEffect;
import mage.abilities.effects.common.ExileGraveyardAllTargetPlayerEffect;
import mage.abilities.hint.common.CreaturesYouControlHint;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetPlayer;
import mage.target.common.TargetCreaturePermanent;
import mage.target.common.TargetEnchantmentPermanent;
import java.util.Objects;
import java.util.UUID;
/**
@ -48,7 +40,7 @@ public final class ThrabenCharm extends CardImpl {
this.getSpellAbility().addMode(mode);
// * Exile any number of target players' graveyards.
mode = new Mode(new ThrabenCharmEffect());
mode = new Mode(new ExileGraveyardAllTargetPlayerEffect());
mode.addTarget(new TargetPlayer(0, Integer.MAX_VALUE, false));
this.getSpellAbility().addMode(mode);
}
@ -62,38 +54,3 @@ public final class ThrabenCharm extends CardImpl {
return new ThrabenCharm(this);
}
}
class ThrabenCharmEffect extends OneShotEffect {
ThrabenCharmEffect() {
super(Outcome.Benefit);
staticText = "exile any number of target players' graveyards";
}
private ThrabenCharmEffect(final ThrabenCharmEffect effect) {
super(effect);
}
@Override
public ThrabenCharmEffect copy() {
return new ThrabenCharmEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
Cards cards = new CardsImpl();
this.getTargetPointer()
.getTargets(game, source)
.stream()
.map(game::getPlayer)
.filter(Objects::nonNull)
.map(Player::getGraveyard)
.forEach(cards::addAll);
controller.moveCards(cards, Zone.EXILED, source, game);
return true;
}
}

View file

@ -272,6 +272,8 @@ public final class MarvelsSpiderMan extends ExpansionSet {
cards.add(new SetCardInfo("Taxi Driver", 97, Rarity.COMMON, mage.cards.t.TaxiDriver.class));
cards.add(new SetCardInfo("The Clone Saga", 219, Rarity.RARE, mage.cards.t.TheCloneSaga.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Clone Saga", 28, Rarity.RARE, mage.cards.t.TheCloneSaga.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Death of Gwen Stacy", 223, Rarity.RARE, mage.cards.t.TheDeathOfGwenStacy.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Death of Gwen Stacy", 54, Rarity.RARE, mage.cards.t.TheDeathOfGwenStacy.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Spot's Portal", 68, Rarity.UNCOMMON, mage.cards.t.TheSpotsPortal.class));
cards.add(new SetCardInfo("The Spot, Living Portal", 153, Rarity.RARE, mage.cards.t.TheSpotLivingPortal.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Spot, Living Portal", 231, Rarity.RARE, mage.cards.t.TheSpotLivingPortal.class, NON_FULL_USE_VARIOUS));

View file

@ -0,0 +1,48 @@
package org.mage.test.cards.single.spm;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestCommander4Players;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author Jmlundeen
*/
public class TheDeathOfGwenStacyTest extends CardTestCommander4Players {
/*
The Death of Gwen Stacy
{2}{B}
Enchantment - Saga
(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)
I -- Destroy target creature.
II -- Each player may discard a card. Each player who doesn't loses 3 life.
III -- Exile any number of target players' graveyards.
*/
private static final String theDeathOfGwenStacy = "The Death of Gwen Stacy";
@Test
public void testTheDeathOfGwenStacy() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, theDeathOfGwenStacy);
addCard(Zone.HAND, playerC, "Mountain");
addCard(Zone.HAND, playerB, "Mountain");
addTarget(playerA, "Mountain");
addTarget(playerD, TestPlayer.TARGET_SKIP);
addTarget(playerC, "Mountain");
addTarget(playerB, "Mountain");
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertLife(playerC, 20);
assertLife(playerD, 20 - 3);
}
}

View file

@ -1,13 +1,19 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
* @author LevelX2
*/
@ -22,7 +28,6 @@ public class ExileGraveyardAllTargetPlayerEffect extends OneShotEffect {
public ExileGraveyardAllTargetPlayerEffect(boolean toUniqueExile) {
super(Outcome.Exile);
this.toUniqueExile = toUniqueExile;
staticText = "exile target player's graveyard";
}
private ExileGraveyardAllTargetPlayerEffect(final ExileGraveyardAllTargetPlayerEffect effect) {
@ -38,14 +43,39 @@ public class ExileGraveyardAllTargetPlayerEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
Player targetPlayer = game.getPlayer(this.getTargetPointer().getFirst(game, source));
if (targetPlayer == null || controller == null) {
if (controller == null) {
return false;
}
Set<Card> cardsToExile = new HashSet<>();
for (UUID playerId : this.getTargetPointer().getTargets(game, source)) {
Player targetPlayer = game.getPlayer(playerId);
if (targetPlayer == null) {
continue;
}
cardsToExile.addAll(targetPlayer.getGraveyard().getCards(game));
}
return toUniqueExile ?
controller.moveCardsToExile(
targetPlayer.getGraveyard().getCards(game), source, game, true,
cardsToExile, source, game, true,
CardUtil.getExileZoneId(game, source), CardUtil.getSourceName(game, source)
) : controller.moveCards(targetPlayer.getGraveyard(), Zone.EXILED, source, game);
) : controller.moveCards(cardsToExile, Zone.EXILED, source, game);
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
StringBuilder sb = new StringBuilder("exile ");
sb.append(getTargetPointer().describeTargets(mode.getTargets(), "target player"));
if (sb.toString().toLowerCase().endsWith("player") || sb.toString().toLowerCase().endsWith("opponent")) {
if (getTargetPointer().isPlural(mode.getTargets())) {
sb.append("s' graveyards");
} else {
sb.append("'s graveyard");
}
}
return sb.toString();
}
}