From 79165f269c80fe9f1b84122348c6bdeafd84b40f Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Thu, 16 May 2024 21:17:21 +0200 Subject: [PATCH] implement [MH3] Sorin of House Markov // Sorin, Ravenous Neonate --- .../src/mage/cards/s/SorinOfHouseMarkov.java | 68 ++++++++ .../mage/cards/s/SorinRavenousNeonate.java | 109 +++++++++++++ Mage.Sets/src/mage/sets/ModernHorizons3.java | 2 + .../single/mh3/SorinOfHouseMarkovTest.java | 150 ++++++++++++++++++ .../common/YouGainedLifeCondition.java | 2 + .../common/ControllerGainedLifeCount.java | 1 + .../effects/ContinuousEffectImpl.java | 6 + 7 files changed, 338 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SorinOfHouseMarkov.java create mode 100644 Mage.Sets/src/mage/cards/s/SorinRavenousNeonate.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/SorinOfHouseMarkovTest.java diff --git a/Mage.Sets/src/mage/cards/s/SorinOfHouseMarkov.java b/Mage.Sets/src/mage/cards/s/SorinOfHouseMarkov.java new file mode 100644 index 00000000000..dda228155be --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SorinOfHouseMarkov.java @@ -0,0 +1,68 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Pronoun; +import mage.abilities.common.BeginningOfPostCombatMainTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.YouGainedLifeCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.ExileAndReturnSourceEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.abilities.keyword.ExtortAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.watchers.common.PlayerGainedLifeWatcher; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class SorinOfHouseMarkov extends CardImpl { + + private static final Condition condition = new YouGainedLifeCondition(ComparisonType.OR_GREATER, 3); + private static final Hint hint = new ConditionHint(condition, "You gained 3 or more life this turn"); + + public SorinOfHouseMarkov(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.NOBLE); + this.power = new MageInt(1); + this.toughness = new MageInt(4); + + this.secondSideCardClazz = SorinRavenousNeonate.class; + + // Flying + this.addAbility(LifelinkAbility.getInstance()); + + // Extort + this.addAbility(new ExtortAbility()); + + // At the beginning of your postcombat main phase, if you gained 3 or more life this turn, exile Sorin of House Markov, then return him to the battlefield transformed under his owner's control. + this.addAbility(new TransformAbility()); + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new BeginningOfPostCombatMainTriggeredAbility( + new ExileAndReturnSourceEffect(PutCards.BATTLEFIELD_TRANSFORMED, Pronoun.SHE), + TargetController.YOU, + false + ), condition, "At the beginning of your postcombat main phase, " + + "if you gained 3 or more life this turn, exile {this}, " + + "then return him to the battlefield transformed under his owner's control." + ).addHint(hint), new PlayerGainedLifeWatcher()); + } + + private SorinOfHouseMarkov(final SorinOfHouseMarkov card) { + super(card); + } + + @Override + public SorinOfHouseMarkov copy() { + return new SorinOfHouseMarkov(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SorinRavenousNeonate.java b/Mage.Sets/src/mage/cards/s/SorinRavenousNeonate.java new file mode 100644 index 00000000000..7e7d3e3dff4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SorinRavenousNeonate.java @@ -0,0 +1,109 @@ +package mage.cards.s; + +import mage.ObjectColor; +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.ControllerGainedLifeCount; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.continuous.BecomesCreatureTypeTargetEffect; +import mage.abilities.effects.common.continuous.GainControlTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.abilities.keyword.ExtortAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.filter.predicate.mageobject.ColorPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.FoodToken; +import mage.target.common.TargetAnyTarget; +import mage.target.common.TargetCreaturePermanent; +import mage.watchers.common.PlayerGainedLifeWatcher; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class SorinRavenousNeonate extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("white permanent other than that creature or {this}"); + private static final Condition condition = new PermanentsOnTheBattlefieldCondition(filter, true); + private static final Hint hint = new ConditionHint(condition, "you control another white permanent"); + + static { + filter.add(new ColorPredicate(ObjectColor.WHITE)); + filter.add(AnotherPredicate.instance); + filter.add(SorinRavenousNeonatePredicate.instance); + } + + public SorinRavenousNeonate(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, ""); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SORIN); + this.setStartingLoyalty(3); + + this.color.setWhite(true); + this.color.setBlack(true); + this.nightCard = true; + + // Extort + this.addAbility(new ExtortAbility()); + + // +2: Create a Food token. + this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new FoodToken()), 2)); + + // -1: Sorin, Ravenous Neonate deals damage equal to the amount of life you gained this turn to any target. + Ability ability = new LoyaltyAbility(new DamageTargetEffect(ControllerGainedLifeCount.instance), -1); + ability.addTarget(new TargetAnyTarget()); + this.addAbility(ability.addHint(ControllerGainedLifeCount.getHint()), new PlayerGainedLifeWatcher()); + + // -6: Gain control of target creature. It becomes a Vampire in addition to its other types. Put a lifelink counter on it if you control a white permanent other than that creature or Sorin. + ability = new LoyaltyAbility(new GainControlTargetEffect(Duration.EndOfGame), -6); + ability.addTarget(new TargetCreaturePermanent()); + ability.addEffect(new BecomesCreatureTypeTargetEffect(Duration.EndOfGame, SubType.VAMPIRE, false) + .setText("It becomes a Vampire in addition to its other types")); + ability.addEffect(new ConditionalOneShotEffect( + new AddCountersTargetEffect(CounterType.LIFELINK.createInstance()), + condition, "Put a lifelink counter on it if you control a white permanent other than that creature or {this}" + )); + this.addAbility(ability.addHint(hint)); + } + + private SorinRavenousNeonate(final SorinRavenousNeonate card) { + super(card); + } + + @Override + public SorinRavenousNeonate copy() { + return new SorinRavenousNeonate(this); + } +} + +enum SorinRavenousNeonatePredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + Ability source = input.getSource(); + Permanent permanent = input.getObject(); + return source != null + && permanent != null + && !permanent.getId().equals(source.getFirstTarget()); + } +} \ 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 2652b80196c..c3c84f8b06e 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons3.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons3.java @@ -77,6 +77,8 @@ public final class ModernHorizons3 extends ExpansionSet { cards.add(new SetCardInfo("Serum Visionary", 69, Rarity.COMMON, mage.cards.s.SerumVisionary.class)); cards.add(new SetCardInfo("Six", 169, Rarity.RARE, mage.cards.s.Six.class)); cards.add(new SetCardInfo("Snow-Covered Wastes", 229, Rarity.UNCOMMON, mage.cards.s.SnowCoveredWastes.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Sorin of House Markov", 245, Rarity.MYTHIC, mage.cards.s.SorinOfHouseMarkov.class)); + cards.add(new SetCardInfo("Sorin, Ravenous Neonate", 245, Rarity.MYTHIC, mage.cards.s.SorinRavenousNeonate.class)); cards.add(new SetCardInfo("Spawn-Gang Commander", 140, Rarity.UNCOMMON, mage.cards.s.SpawnGangCommander.class)); cards.add(new SetCardInfo("Swamp", 306, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Sylvan Safekeeper", 287, Rarity.RARE, mage.cards.s.SylvanSafekeeper.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/SorinOfHouseMarkovTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/SorinOfHouseMarkovTest.java new file mode 100644 index 00000000000..54e85ee8fdf --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/SorinOfHouseMarkovTest.java @@ -0,0 +1,150 @@ +package org.mage.test.cards.single.mh3; + +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class SorinOfHouseMarkovTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.s.SorinOfHouseMarkov Sorin of House Markov} {1}{B} + * Legendary Creature — Human Noble + * Lifelink + * Extort (Whenever you cast a spell, you may pay {W/B}. If you do, each opponent loses 1 life and you gain that much life.) + * At the beginning of your postcombat main phase, if you gained 3 or more life this turn, exile Sorin of House Markov, then him to the battlefield transformed under his owner’s control. + * 1/4 + * // {@link mage.cards.s.SorinRavenousNeonate Sorin, Ravenous Neonate} + * Legendary Planeswalker — Sorin + * Extort (Whenever you cast a spell, you may pay {W/B}. If you do, each opponent loses 1 life and you gain that much life.) + * +2: Create a Food token. + * −1: Sorin, Ravenous Neonate deals damage equal to the amount of life you gained this turn to any target. + * −6: Gain control of target creature. It becomes a Vampire in addition to its other types. Put a lifelink counter on it if you control a white permanent other than that creature or Sorin. + * Loyalty: 3 + */ + private static final String sorin = "Sorin of House Markov"; + private static final String sorinPW = "Sorin, Ravenous Neonate"; + + @Test + public void test_Gain2Life_NoTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, sorin); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + addCard(Zone.HAND, playerA, "Courier Griffin"); // {3}{W} etb, gain 2 life + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Courier Griffin"); + setChoice(playerA, false); // no to Extort trigger + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Courier Griffin", 1); + assertPermanentCount(playerA, sorin, 1); + assertLife(playerA, 20 + 2); + } + + @Test + public void test_Gain3Life_Trigger_ThenMinus1() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, sorin); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.HAND, playerA, "Courier Griffin"); // {3}{W} etb, gain 2 life + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Courier Griffin"); + setChoice(playerA, true); // pay for Extort + + checkPermanentCount("sorin did not transform yet", 1, PhaseStep.BEGIN_COMBAT, playerA, sorin, 1); + + // Sorin triggers post combat + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN, playerA, true); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "-1", playerB); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Courier Griffin", 1); + assertLife(playerA, 20 + 3); + assertLife(playerB, 20 - 1 - 3); + assertPermanentCount(playerA, sorinPW, 1); + assertCounterCount(playerA, sorinPW, CounterType.LOYALTY, 3 - 1); + } + + @Test + public void test_Plus2_Plus2_Minus1() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, sorinPW); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2"); + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "+2"); + + // Activate both foods + activateAbility(5, PhaseStep.UPKEEP, playerA, "{2}, {T}, Sacrifice this artifact: You gain 3 life"); + activateAbility(5, PhaseStep.UPKEEP, playerA, "{2}, {T}, Sacrifice this artifact: You gain 3 life"); + + activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "-1"); + addTarget(playerA, playerB); + + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 + 6); + assertLife(playerB, 20 - 6); + assertPermanentCount(playerA, sorinPW, 1); + assertCounterCount(playerA, sorinPW, CounterType.LOYALTY, 3 + 2 + 2 - 1); + } + + @Test + public void test_Minus6_NoOtherWhite() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, sorinPW); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); // notably not white + addCard(Zone.BATTLEFIELD, playerB, "Elite Vanguard"); + + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, sorinPW, CounterType.LOYALTY, 4); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-6"); + addTarget(playerA, "Elite Vanguard"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Elite Vanguard", 1); + assertSubtype("Elite Vanguard", SubType.HUMAN); + assertSubtype("Elite Vanguard", SubType.VAMPIRE); // Vampire in addition + assertCounterCount(playerA, "Elite Vanguard", CounterType.LIFELINK, 0); + assertCounterCount(playerA, sorinPW, CounterType.LOYALTY, 1); + } + + @Test + public void test_Minus6_OtherWhite() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, sorinPW); + addCard(Zone.BATTLEFIELD, playerA, "Baneslayer Angel"); // notably white + addCard(Zone.BATTLEFIELD, playerB, "Elite Vanguard"); + + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, sorinPW, CounterType.LOYALTY, 4); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-6"); + addTarget(playerA, "Elite Vanguard"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Elite Vanguard", 1); + assertSubtype("Elite Vanguard", SubType.HUMAN); + assertSubtype("Elite Vanguard", SubType.VAMPIRE); // Vampire in addition + assertCounterCount(playerA, "Elite Vanguard", CounterType.LIFELINK, 1); + assertCounterCount(playerA, sorinPW, CounterType.LOYALTY, 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/YouGainedLifeCondition.java b/Mage/src/main/java/mage/abilities/condition/common/YouGainedLifeCondition.java index 6ebf31b6aa3..3a2336e0b20 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/YouGainedLifeCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/YouGainedLifeCondition.java @@ -7,6 +7,8 @@ import mage.game.Game; import mage.watchers.common.PlayerGainedLifeWatcher; /** + * Needs PlayerGainedLifeWatcher to work + *

* Created by IGOUDT on 5-4-2017. */ public class YouGainedLifeCondition extends IntCompareCondition { diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerGainedLifeCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerGainedLifeCount.java index 21ee85e53c9..2b3a36936fd 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerGainedLifeCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerGainedLifeCount.java @@ -9,6 +9,7 @@ import mage.game.Game; import mage.watchers.common.PlayerGainedLifeWatcher; /** + * Needs PlayerGainedLifeWatcher to work. * Amount of life the controller gained this turn. * * @author LevelX2 diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java index bb0aeabf1d6..156e9400251 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java @@ -515,4 +515,10 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu } return true; } + + @Override + public ContinuousEffect setText(String staticText) { + super.setText(staticText); + return this; + } }