From 038cf01aa8295f28128d57c904096ce48a81631b Mon Sep 17 00:00:00 2001 From: xenohedron Date: Sun, 21 Jan 2024 00:11:16 -0500 Subject: [PATCH] partial fix for Syrix, Carrier of the Flame closes #10057, related to #10550 --- .../src/mage/cards/h/HarnessTheStorm.java | 43 +++++----- .../mage/cards/s/SyrixCarrierOfTheFlame.java | 78 ++++--------------- .../ncc/SyrixCarrierOfTheFlameTest.java | 73 +++++++++++++++++ .../common/CardsLeftGraveyardWatcher.java | 56 +++++++++++++ 4 files changed, 165 insertions(+), 85 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/SyrixCarrierOfTheFlameTest.java create mode 100644 Mage/src/main/java/mage/watchers/common/CardsLeftGraveyardWatcher.java diff --git a/Mage.Sets/src/mage/cards/h/HarnessTheStorm.java b/Mage.Sets/src/mage/cards/h/HarnessTheStorm.java index e2dfac76c1e..2f5fe12510c 100644 --- a/Mage.Sets/src/mage/cards/h/HarnessTheStorm.java +++ b/Mage.Sets/src/mage/cards/h/HarnessTheStorm.java @@ -1,10 +1,8 @@ package mage.cards.h; -import java.util.UUID; import mage.ApprovingObject; import mage.abilities.Ability; import mage.abilities.common.SpellCastControllerTriggeredAbility; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; @@ -12,7 +10,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.filter.FilterCard; -import mage.filter.FilterSpell; import mage.filter.common.FilterInstantOrSorcerySpell; import mage.filter.predicate.mageobject.NamePredicate; import mage.game.Game; @@ -22,6 +19,8 @@ import mage.players.Player; import mage.target.common.TargetCardInYourGraveyard; import mage.watchers.common.CastFromHandWatcher; +import java.util.UUID; + /** * * @author LevelX2 @@ -33,9 +32,7 @@ public final class HarnessTheStorm extends CardImpl { // Whenever you cast an instant or sorcery spell from your hand, you may cast // target card with the same name as that spell from your graveyard. - this.addAbility(new HarnessTheStormTriggeredAbility(new HarnessTheStormEffect(), - new FilterInstantOrSorcerySpell("an instant or sorcery spell from your hand"), - false), new CastFromHandWatcher()); + this.addAbility(new HarnessTheStormTriggeredAbility(), new CastFromHandWatcher()); } private HarnessTheStorm(final HarnessTheStorm card) { @@ -51,8 +48,10 @@ public final class HarnessTheStorm extends CardImpl { class HarnessTheStormTriggeredAbility extends SpellCastControllerTriggeredAbility { - public HarnessTheStormTriggeredAbility(Effect effect, FilterSpell filter, boolean optional) { - super(effect, filter, optional); + private static final FilterInstantOrSorcerySpell filterSpell = new FilterInstantOrSorcerySpell("an instant or sorcery spell from your hand"); + + HarnessTheStormTriggeredAbility() { + super(new HarnessTheStormEffect(), filterSpell, false); } private HarnessTheStormTriggeredAbility(final HarnessTheStormTriggeredAbility ability) { @@ -89,7 +88,7 @@ class HarnessTheStormEffect extends OneShotEffect { HarnessTheStormEffect() { super(Outcome.Benefit); this.staticText = "you may cast target card with the same name as that " - + "spell from your graveyard. (you still pay its costs.)"; + + "spell from your graveyard. (You still pay its costs.)"; } private HarnessTheStormEffect(final HarnessTheStormEffect effect) { @@ -104,20 +103,20 @@ class HarnessTheStormEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - - if (controller != null) { - Card card = controller.getGraveyard().get(getTargetPointer().getFirst(game, source), game); - if (card != null) { - if (controller.chooseUse(outcome.Benefit, "Cast " + card.getIdName() + " from your graveyard?", source, game)) { - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); - controller.cast(controller.chooseAbilityForCast(card, game, false), - game, false, new ApprovingObject(source, game)); - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); - } - } - return true; + if (controller == null) { + return false; } + Card card = controller.getGraveyard().get(getTargetPointer().getFirst(game, source), game); + if (card == null) { + return false; + } + if (controller.chooseUse(outcome, "Cast " + card.getIdName() + " from your graveyard?", source, game)) { + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + controller.cast(controller.chooseAbilityForCast(card, game, false), + game, false, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + } + return true; - return false; } } diff --git a/Mage.Sets/src/mage/cards/s/SyrixCarrierOfTheFlame.java b/Mage.Sets/src/mage/cards/s/SyrixCarrierOfTheFlame.java index a3879363f5e..df82d3450e2 100644 --- a/Mage.Sets/src/mage/cards/s/SyrixCarrierOfTheFlame.java +++ b/Mage.Sets/src/mage/cards/s/SyrixCarrierOfTheFlame.java @@ -18,29 +18,22 @@ import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledPermanent; import mage.filter.predicate.mageobject.AnotherPredicate; import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.ZoneChangeEvent; import mage.players.Player; import mage.target.TargetPermanent; import mage.target.common.TargetAnyTarget; -import mage.watchers.Watcher; +import mage.watchers.common.CardsLeftGraveyardWatcher; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; /** - * @author Alex-Vasile + * @author Alex-Vasile, Merlingilb, xenohedron */ public class SyrixCarrierOfTheFlame extends CardImpl { - private static final String description = "Phoenix you control"; - private static final FilterPermanent anotherPhoenixFilter = new FilterControlledPermanent("another Phoenix you control"); - private static final FilterPermanent phoenixFilter = new FilterControlledPermanent(description); + private static final FilterPermanent anotherPhoenixFilter = new FilterControlledPermanent(SubType.PHOENIX, "another Phoenix you control"); + private static final FilterPermanent phoenixFilter = new FilterControlledPermanent(SubType.PHOENIX, "Phoenix you control"); static { anotherPhoenixFilter.add(AnotherPredicate.instance); - anotherPhoenixFilter.add(SubType.PHOENIX.getPredicate()); - phoenixFilter.add(SubType.PHOENIX.getPredicate()); } public SyrixCarrierOfTheFlame(UUID ownerId, CardSetInfo setInfo) { @@ -57,16 +50,15 @@ public class SyrixCarrierOfTheFlame extends CardImpl { // At the beginning of each end step, if a creature card left your graveyard this turn, // target Phoenix you control deals damage equal to its power to any target. - BeginningOfEndStepTriggeredAbility ability = new BeginningOfEndStepTriggeredAbility( + Ability ability = new BeginningOfEndStepTriggeredAbility( new DamageWithPowerFromOneToAnotherTargetEffect(), - TargetController.EACH_PLAYER, + TargetController.ANY, SyrixCarrierOfTheFlameCondition.instance, false ); ability.addTarget(new TargetPermanent(phoenixFilter)); ability.addTarget(new TargetAnyTarget()); - ability.addWatcher(new SyrixCarrierOfTheFlameWatcher()); - this.addAbility(ability); + this.addAbility(ability, new CardsLeftGraveyardWatcher()); // Whenever another Phoenix you control dies, you may cast Syrix, Carrier of the Flame from your graveyard. this.addAbility(new DiesCreatureTriggeredAbility( @@ -88,10 +80,9 @@ public class SyrixCarrierOfTheFlame extends CardImpl { } } -/** - * Based on Harness the Storm - */ +// Based on Harness the Storm class SyrixCarrierOfTheFlameCastEffect extends OneShotEffect { + SyrixCarrierOfTheFlameCastEffect() { super(Outcome.Benefit); this.staticText = "you may cast {this} from your graveyard"; @@ -133,56 +124,17 @@ class SyrixCarrierOfTheFlameCastEffect extends OneShotEffect { enum SyrixCarrierOfTheFlameCondition implements Condition { instance; - private static final String string = "a creature card left your graveyard this turn"; - @Override public boolean apply(Game game, Ability source) { - SyrixCarrierOfTheFlameWatcher watcher = game.getState().getWatcher(SyrixCarrierOfTheFlameWatcher.class); - return watcher != null && watcher.hadACreatureLeave(source.getControllerId()); + CardsLeftGraveyardWatcher watcher = game.getState().getWatcher(CardsLeftGraveyardWatcher.class); + return watcher != null && watcher + .getCardsThatLeftGraveyard(source.getControllerId(), game) + .stream() + .anyMatch(card -> card.isCreature(game)); } @Override public String toString() { - return string; - } -} - -/** - * Creature card left your graveyard this turn - */ -class SyrixCarrierOfTheFlameWatcher extends Watcher { - - // Player IDs who had a creature card leave their graveyard - private final Set creatureCardLeftPlayerIds = new HashSet<>(); - - SyrixCarrierOfTheFlameWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - if (!(event.getType() == GameEvent.EventType.ZONE_CHANGE && event instanceof ZoneChangeEvent)) { - return; - } - ZoneChangeEvent zoneChangeEvent = (ZoneChangeEvent) event; - - if (zoneChangeEvent.getFromZone() != Zone.GRAVEYARD) { - return; - } - - Card card = zoneChangeEvent.getTarget(); - if (card != null && card.isCreature(game)) { - creatureCardLeftPlayerIds.add(card.getOwnerId()); - } - } - - public boolean hadACreatureLeave(UUID playerId) { - return creatureCardLeftPlayerIds.contains(playerId); - } - - @Override - public void reset() { - super.reset(); - creatureCardLeftPlayerIds.clear(); + return "a creature card left your graveyard this turn"; } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/SyrixCarrierOfTheFlameTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/SyrixCarrierOfTheFlameTest.java new file mode 100644 index 00000000000..5408e56ea6e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/SyrixCarrierOfTheFlameTest.java @@ -0,0 +1,73 @@ +package org.mage.test.cards.single.ncc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class SyrixCarrierOfTheFlameTest extends CardTestPlayerBase { + + private static final String syrix = "Syrix, Carrier of the Flame"; // 3/3 + // At the beginning of each end step, if a creature card left your graveyard this turn, + // target Phoenix you control deals damage equal to its power to any target. + // Whenever another Phoenix you control dies, you may cast Syrix, Carrier of the Flame from your graveyard. + private static final String phoenix = "Firewing Phoenix"; // 4/2 + private static final String shock = "Shock"; + private static final String historian = "Illustrious Historian"; + // {5}, Exile Illustrious Historian from your graveyard: Create a tapped 3/2 red and white Spirit creature token. + + @Test + public void testDamageTrigger() { + addCard(Zone.BATTLEFIELD, playerA, syrix); + addCard(Zone.BATTLEFIELD, playerA, phoenix); + addCard(Zone.GRAVEYARD, playerA, historian); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}, Exile "); + + checkExileCount("exiled", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, historian, 1); + checkPT("token", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, "Spirit Token", 3, 2); + checkLife("before trigger", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, 20); + checkLife("before trigger", 2, PhaseStep.POSTCOMBAT_MAIN, playerB, 20); + + addTarget(playerA, phoenix); // target Phoenix + addTarget(playerA, playerB); // deals damage + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.UPKEEP); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 16); + assertPowerToughness(playerA, syrix, 3, 3); + assertPowerToughness(playerA, phoenix, 4, 2); + + } + + @Ignore("Usable zone issue, see #10550") + @Test + public void testCast() { + addCard(Zone.GRAVEYARD, playerA, syrix); + addCard(Zone.BATTLEFIELD, playerA, phoenix); + addCard(Zone.HAND, playerA, shock); + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 6); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shock, phoenix); + // phoenix dies, syrix ability triggers + setChoice(playerA, true); // yes to cast + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertPowerToughness(playerA, syrix, 3, 3); + assertGraveyardCount(playerA, phoenix, 1); + assertGraveyardCount(playerA, shock, 1); + assertTappedCount("Badlands", true, 6); + + } + +} diff --git a/Mage/src/main/java/mage/watchers/common/CardsLeftGraveyardWatcher.java b/Mage/src/main/java/mage/watchers/common/CardsLeftGraveyardWatcher.java new file mode 100644 index 00000000000..68f3efe3add --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/CardsLeftGraveyardWatcher.java @@ -0,0 +1,56 @@ +package mage.watchers.common; + +import mage.cards.Card; +import mage.constants.WatcherScope; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.watchers.Watcher; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Keeps track of the UUIDs of the cards that left graveyard this turn. + */ +public class CardsLeftGraveyardWatcher extends Watcher { + + // Player id -> card ids + private final Map> cardsLeftGraveyardThisTurn = new HashMap<>(); + + public CardsLeftGraveyardWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.ZONE_CHANGE + || ((ZoneChangeEvent) event).getFromZone() != Zone.GRAVEYARD) { + return; + } + UUID playerId = event.getPlayerId(); + if (playerId == null || game.getCard(event.getTargetId()) == null) { + return; + } + cardsLeftGraveyardThisTurn.computeIfAbsent(playerId, k -> new HashSet<>()) + .add(event.getTargetId()); + } + + /** + * The cards that left a specific player's graveyard this turn. + */ + public Set getCardsThatLeftGraveyard(UUID playerId, Game game) { + return cardsLeftGraveyardThisTurn.getOrDefault(playerId, Collections.emptySet()) + .stream() + .map(game::getCard) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + @Override + public void reset() { + super.reset(); + cardsLeftGraveyardThisTurn.clear(); + } +}