From cc4c6be78b89090e0b7d53efefef7fc6a34448c1 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:16:53 +0200 Subject: [PATCH] implement [MH3] The Creation of Avacyn --- .../src/mage/cards/t/TheCreationOfAvacyn.java | 186 ++++++++++++++++++ Mage.Sets/src/mage/sets/ModernHorizons3.java | 1 + .../single/mh3/TheCreationOfAvacynTest.java | 167 ++++++++++++++++ 3 files changed, 354 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TheCreationOfAvacyn.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/TheCreationOfAvacynTest.java diff --git a/Mage.Sets/src/mage/cards/t/TheCreationOfAvacyn.java b/Mage.Sets/src/mage/cards/t/TheCreationOfAvacyn.java new file mode 100644 index 00000000000..b92c5e90b3f --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheCreationOfAvacyn.java @@ -0,0 +1,186 @@ +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.LoseLifeSourceControllerEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.ExileZone; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInLibrary; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class TheCreationOfAvacyn extends CardImpl { + + public TheCreationOfAvacyn(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}{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 -- Search your library for a card, exile it face down, then shuffle. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_I, new TheCreationOfAvacynOneEffect()); + + // II -- Turn the exiled card face up. If it's a creature card, you lose life equal to its mana value. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_II, new TheCreationOfAvacynTwoEffect()); + + // III -- You may put the exiled card onto the battlefield if it's a creature card. If you don't put it onto the battlefield, put it into its owner's hand. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_III, new TheCreationOfAvacynThreeEffect()); + + this.addAbility(sagaAbility); + } + + private TheCreationOfAvacyn(final TheCreationOfAvacyn card) { + super(card); + } + + @Override + public TheCreationOfAvacyn copy() { + return new TheCreationOfAvacyn(this); + } +} + +class TheCreationOfAvacynOneEffect extends OneShotEffect { + + TheCreationOfAvacynOneEffect() { + super(Outcome.Benefit); + staticText = "Search your library for a card, exile it face down, then shuffle"; + } + + private TheCreationOfAvacynOneEffect(final TheCreationOfAvacynOneEffect effect) { + super(effect); + } + + @Override + public TheCreationOfAvacynOneEffect copy() { + return new TheCreationOfAvacynOneEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + // Search your library for a card + TargetCardInLibrary target = new TargetCardInLibrary(); + if (controller.searchLibrary(target, source, game)) { + Card card = controller.getLibrary().getCard(target.getFirstTarget(), game); + if (card != null) { + // exile it face down + card.setFaceDown(true, game); + UUID exileId = CardUtil.getExileZoneId(game, source, 1); + MageObject sourceObject = source.getSourceObject(game); + String exileName = sourceObject != null ? sourceObject.getName() : ""; + controller.moveCardsToExile(card, source, game, false, exileId, exileName); + card.setFaceDown(true, game); + + // then shuffle + controller.shuffleLibrary(source, game); + + } + } + return true; + } +} + +class TheCreationOfAvacynTwoEffect extends OneShotEffect { + + TheCreationOfAvacynTwoEffect() { + super(Outcome.Neutral); + staticText = "Turn the exiled card face up. If it's a creature card, you lose life equal to its mana value"; + } + + private TheCreationOfAvacynTwoEffect(final TheCreationOfAvacynTwoEffect effect) { + super(effect); + } + + @Override + public TheCreationOfAvacynTwoEffect copy() { + return new TheCreationOfAvacynTwoEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + // From Release Notes ruling: + // In the unusual case where two or more cards are exiled face down with The Creation of Avacyn's first chapter + // ability (likely because the triggered ability was copied or the ability triggered a second time), the second + // chapter ability will turn all of the exiled cards face up. If at least one of them is a creature card, you'll + // lose life equal to the combined mana value of all the exiled cards. + UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter()); + ExileZone exileZone = game.getExile().getExileZone(exileId); + if (exileZone == null || exileZone.isEmpty()) { + return false; + } + boolean creatureCard = false; + int mv = 0; + for (Card card : exileZone.getCards(game)) { + card.setFaceDown(false, game); + creatureCard |= card.isCreature(game); + mv += card.getManaValue(); + } + if (creatureCard) { + new LoseLifeSourceControllerEffect(mv) + .apply(game, source); + } + return true; + } +} + +class TheCreationOfAvacynThreeEffect extends OneShotEffect { + + TheCreationOfAvacynThreeEffect() { + super(Outcome.Neutral); + staticText = "You may put the exiled card onto the battlefield if it's a creature card. " + + "If you don't put it onto the battlefield, put it into its owner's hand."; + } + + private TheCreationOfAvacynThreeEffect(final TheCreationOfAvacynThreeEffect effect) { + super(effect); + } + + @Override + public TheCreationOfAvacynThreeEffect copy() { + return new TheCreationOfAvacynThreeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + // From Release Notes Ruling: + // In the unusual case where two or more cards are exiled face down with The Creation of Avacyn's first ability + // when the third chapter ability resolves, if at least one of the exiled cards is a creature card, you may choose + // to put all or none of the exiled cards that are permanent cards onto the battlefield. Regardless of what you + // choose, any remaining exiled cards will be put into their owners' hands. + UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter()); + ExileZone exileZone = game.getExile().getExileZone(exileId); + Player controller = game.getPlayer(source.getControllerId()); + if (exileZone == null || exileZone.isEmpty()) { + return false; + } + boolean creatureCard = false; + for (Card card : exileZone.getCards(game)) { + creatureCard |= card.isCreature(game); + } + if (creatureCard) { + if (controller.chooseUse(Outcome.PutCreatureInPlay, "Put the permanent in play?", source, game)) { + controller.moveCards(exileZone, Zone.BATTLEFIELD, source, game); + game.getState().processAction(game); + } + } + controller.moveCards(exileZone, Zone.HAND, source, game); + return true; + } + +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/ModernHorizons3.java b/Mage.Sets/src/mage/sets/ModernHorizons3.java index a0707e674d2..c6d91109fb3 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons3.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons3.java @@ -267,6 +267,7 @@ public final class ModernHorizons3 extends ExpansionSet { cards.add(new SetCardInfo("Temperamental Oozewagg", 172, Rarity.COMMON, mage.cards.t.TemperamentalOozewagg.class)); cards.add(new SetCardInfo("Tempest Harvester", 73, Rarity.COMMON, mage.cards.t.TempestHarvester.class)); cards.add(new SetCardInfo("Territory Culler", 173, Rarity.UNCOMMON, mage.cards.t.TerritoryCuller.class)); + cards.add(new SetCardInfo("The Creation of Avacyn", 86, Rarity.UNCOMMON, mage.cards.t.TheCreationOfAvacyn.class)); cards.add(new SetCardInfo("The Hunger Tide Rises", 158, Rarity.UNCOMMON, mage.cards.t.TheHungerTideRises.class)); cards.add(new SetCardInfo("The Necrobloom", 194, Rarity.RARE, mage.cards.t.TheNecrobloom.class)); cards.add(new SetCardInfo("Thraben Charm", 45, Rarity.COMMON, mage.cards.t.ThrabenCharm.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/TheCreationOfAvacynTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/TheCreationOfAvacynTest.java new file mode 100644 index 00000000000..2b24c07c2a5 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/TheCreationOfAvacynTest.java @@ -0,0 +1,167 @@ +package org.mage.test.cards.single.mh3; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class TheCreationOfAvacynTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.t.TheCreationOfAvacyn The Creation of Avacyn} {1}{B}{B} + * Enchantment — Saga + * (As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.) + * I — Search your library for a card, exile it face down, then shuffle. + * II — Turn the exiled card face up. If it’s a creature card, you lose life equal to its mana value. + * III — You may put the exiled card onto the battlefield if it’s a creature card. If you don’t put it onto the battlefield, put it into its owner’s hand. + */ + private static final String creation = "The Creation of Avacyn"; + + @Test + public void test_Creature_PutIntoPlay() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.HAND, playerA, creation); + addCard(Zone.LIBRARY, playerA, "Avacyn, Angel of Hope"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, creation); + addTarget(playerA, "Avacyn, Angel of Hope"); + + setChoice(playerA, true); // put Avacyn into play + + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 8); + assertPermanentCount(playerA, "Avacyn, Angel of Hope", 1); + } + + @Test + public void test_Creature_DontPutIntoPlay() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.HAND, playerA, creation); + addCard(Zone.LIBRARY, playerA, "Avacyn, Angel of Hope"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, creation); + addTarget(playerA, "Avacyn, Angel of Hope"); + + setChoice(playerA, false); // put Avacyn into play + + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 8); + assertPermanentCount(playerA, "Avacyn, Angel of Hope", 0); + assertHandCount(playerA, "Avacyn, Angel of Hope", 1); + } + + @Test + public void test_NotCreature() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.HAND, playerA, creation); + addCard(Zone.LIBRARY, playerA, "Helvault"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, creation); + addTarget(playerA, "Helvault"); + + // No choice on III + + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20); + assertHandCount(playerA, "Helvault", 1); + } + + @Test + public void test_StrionicResonator_DoubleCreature() { + //setStrictChooseMode(true); // targetting the first saga ability is difficult in test + + // {2}, {T}: Copy target triggered ability you control. You may choose new targets for the copy. + addCard(Zone.BATTLEFIELD, playerA, "Strionic Resonator"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.HAND, playerA, creation); + addCard(Zone.LIBRARY, playerA, "Avacyn, Angel of Hope"); + addCard(Zone.LIBRARY, playerA, "Griselbrand"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, creation); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); // Creation resolve, trigger on the stack + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}"); + + addTarget(playerA, "Avacyn, Angel of Hope"); + addTarget(playerA, "Griselbrand"); + + setChoice(playerA, true); + + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 8 - 8); + assertPermanentCount(playerA, "Avacyn, Angel of Hope", 1); + assertPermanentCount(playerA, "Griselbrand", 1); + } + + @Test + public void test_StrionicResonator_CreatureAndPermanent() { + //setStrictChooseMode(true); // targetting the first saga ability is difficult in test + + // {2}, {T}: Copy target triggered ability you control. You may choose new targets for the copy. + addCard(Zone.BATTLEFIELD, playerA, "Strionic Resonator"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.HAND, playerA, creation); + addCard(Zone.LIBRARY, playerA, "Avacyn, Angel of Hope"); + addCard(Zone.LIBRARY, playerA, "Helvault"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, creation); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); // Creation resolve, trigger on the stack + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}"); + + addTarget(playerA, "Avacyn, Angel of Hope"); + addTarget(playerA, "Helvault"); + + setChoice(playerA, true); + + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 8 - 3); + assertPermanentCount(playerA, "Avacyn, Angel of Hope", 1); + assertPermanentCount(playerA, "Helvault", 1); + } + + @Test + public void test_StrionicResonator_CreatureAndSpell() { + //setStrictChooseMode(true); // targetting the first saga ability is difficult in test + + // {2}, {T}: Copy target triggered ability you control. You may choose new targets for the copy. + addCard(Zone.BATTLEFIELD, playerA, "Strionic Resonator"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.HAND, playerA, creation); + addCard(Zone.LIBRARY, playerA, "Avacyn, Angel of Hope"); + addCard(Zone.LIBRARY, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, creation); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); // Creation resolve, trigger on the stack + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}"); + + addTarget(playerA, "Avacyn, Angel of Hope"); + addTarget(playerA, "Lightning Bolt"); + + setChoice(playerA, true); + + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 8 - 1); + assertPermanentCount(playerA, "Avacyn, Angel of Hope", 1); + assertHandCount(playerA, "Lightning Bolt", 1); + } +}