diff --git a/Mage.Sets/src/mage/cards/a/ArdenAngel.java b/Mage.Sets/src/mage/cards/a/ArdenAngel.java index 44cb6196e67..42f06cc0430 100644 --- a/Mage.Sets/src/mage/cards/a/ArdenAngel.java +++ b/Mage.Sets/src/mage/cards/a/ArdenAngel.java @@ -3,7 +3,7 @@ package mage.cards.a; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; -import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SourceInGraveyardCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.FlyingAbility; @@ -34,7 +34,7 @@ public final class ArdenAngel extends CardImpl { // At the beginning of your upkeep, if Arden Angel is in your graveyard, roll a four-sided die. If the result is 1, return Arden Angel from your graveyard to the battlefield. this.addAbility(new ConditionalInterveningIfTriggeredAbility( new BeginningOfUpkeepTriggeredAbility(new ArdenAngelEffect(), TargetController.YOU, false), - ArdenAngelCondition.instance, "At the beginning of your upkeep, if {this} is in your graveyard, " + + SourceInGraveyardCondition.instance, "At the beginning of your upkeep, if {this} is in your graveyard, " + "roll a four-sided die. If the result is 1, return {this} from your graveyard to the battlefield." )); } @@ -49,15 +49,6 @@ public final class ArdenAngel extends CardImpl { } } -enum ArdenAngelCondition implements Condition { - instance; - - @Override - public boolean apply(Game game, Ability source) { - return game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD; - } -} - class ArdenAngelEffect extends OneShotEffect { ArdenAngelEffect() { diff --git a/Mage.Sets/src/mage/cards/g/GlowingOne.java b/Mage.Sets/src/mage/cards/g/GlowingOne.java new file mode 100644 index 00000000000..5d3233a13f7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GlowingOne.java @@ -0,0 +1,59 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.common.MillTriggeredAbility; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.common.FilterNonlandCard; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class GlowingOne extends CardImpl { + + private static final FilterCard filter = new FilterNonlandCard(); + + public GlowingOne(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.MUTANT); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Deathtouch + this.addAbility(DeathtouchAbility.getInstance()); + + // Whenever Glowing One deals combat damage to a player, they get four rad counters. + this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility( + new AddCountersTargetEffect(CounterType.RAD.createInstance(4)), false, true) + ); + + // Whenever a player mills a nonland card, you gain 1 life. + this.addAbility(new MillTriggeredAbility( + Zone.BATTLEFIELD, new GainLifeEffect(1), + TargetController.ANY, filter, false + )); + } + + private GlowingOne(final GlowingOne card) { + super(card); + } + + @Override + public GlowingOne copy() { + return new GlowingOne(this); + } +} diff --git a/Mage.Sets/src/mage/cards/i/InfestingRadroach.java b/Mage.Sets/src/mage/cards/i/InfestingRadroach.java new file mode 100644 index 00000000000..d8fd5be0947 --- /dev/null +++ b/Mage.Sets/src/mage/cards/i/InfestingRadroach.java @@ -0,0 +1,68 @@ +package mage.cards.i; + +import mage.MageInt; +import mage.abilities.common.CantBlockAbility; +import mage.abilities.common.DealsDamageToAPlayerTriggeredAbility; +import mage.abilities.common.MillTriggeredAbility; +import mage.abilities.condition.common.SourceInGraveyardCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; +import mage.abilities.effects.common.ReturnToHandSourceEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.common.FilterNonlandCard; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class InfestingRadroach extends CardImpl { + + private static final FilterCard filter = new FilterNonlandCard(); + + public InfestingRadroach(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}"); + + this.subtype.add(SubType.INSECT); + this.subtype.add(SubType.MUTANT); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Infesting Radroach can't block. + this.addAbility(new CantBlockAbility()); + + // Whenever Infesting Radroach deals combat damage to a player, they get that many rad counters. + this.addAbility(new DealsDamageToAPlayerTriggeredAbility( + new AddCountersTargetEffect(CounterType.RAD.createInstance(), SavedDamageValue.MANY), + false, true + )); + + // Whenever an opponent mills a nonland card, if Infesting Radroach is in your graveyard, you may return it to your hand. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new MillTriggeredAbility(Zone.GRAVEYARD, new ReturnToHandSourceEffect(), TargetController.OPPONENT, filter, true), + SourceInGraveyardCondition.instance, "Whenever an opponent mills a nonland card, " + + "if {this} is in your graveyard, you may return it to your hand" + )); + } + + private InfestingRadroach(final InfestingRadroach card) { + super(card); + } + + @Override + public InfestingRadroach copy() { + return new InfestingRadroach(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/Fallout.java b/Mage.Sets/src/mage/sets/Fallout.java index d31891b1067..d2cd8c15d8c 100644 --- a/Mage.Sets/src/mage/sets/Fallout.java +++ b/Mage.Sets/src/mage/sets/Fallout.java @@ -131,6 +131,7 @@ public final class Fallout extends ExpansionSet { cards.add(new SetCardInfo("General's Enforcer", 217, Rarity.UNCOMMON, mage.cards.g.GeneralsEnforcer.class)); cards.add(new SetCardInfo("Glacial Fortress", 266, Rarity.RARE, mage.cards.g.GlacialFortress.class)); cards.add(new SetCardInfo("Glimmer of Genius", 176, Rarity.UNCOMMON, mage.cards.g.GlimmerOfGenius.class)); + cards.add(new SetCardInfo("Glowing One", 76, Rarity.UNCOMMON, mage.cards.g.GlowingOne.class)); cards.add(new SetCardInfo("Grave Titan", 346, Rarity.MYTHIC, mage.cards.g.GraveTitan.class)); cards.add(new SetCardInfo("Guardian Project", 199, Rarity.RARE, mage.cards.g.GuardianProject.class)); cards.add(new SetCardInfo("Gunner Conscript", 77, Rarity.UNCOMMON, mage.cards.g.GunnerConscript.class)); @@ -146,6 +147,7 @@ public final class Fallout extends ExpansionSet { cards.add(new SetCardInfo("Ian the Reckless", 59, Rarity.UNCOMMON, mage.cards.i.IanTheReckless.class)); cards.add(new SetCardInfo("Impassioned Orator", 162, Rarity.COMMON, mage.cards.i.ImpassionedOrator.class)); cards.add(new SetCardInfo("Inexorable Tide", 177, Rarity.RARE, mage.cards.i.InexorableTide.class)); + cards.add(new SetCardInfo("Infesting Radroach", 46, Rarity.UNCOMMON, mage.cards.i.InfestingRadroach.class)); cards.add(new SetCardInfo("Inspiring Call", 203, Rarity.UNCOMMON, mage.cards.i.InspiringCall.class)); cards.add(new SetCardInfo("Intangible Virtue", 163, Rarity.UNCOMMON, mage.cards.i.IntangibleVirtue.class)); cards.add(new SetCardInfo("Intelligence Bobblehead", 134, Rarity.UNCOMMON, mage.cards.i.IntelligenceBobblehead.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/GlowingOneTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/GlowingOneTest.java new file mode 100644 index 00000000000..e162571eb62 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/GlowingOneTest.java @@ -0,0 +1,63 @@ +package org.mage.test.cards.single.pip; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class GlowingOneTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.g.GlowingOne Glowing One} {2}{G} + * Creature — Zombie Mutant + * Deathtouch + * Whenever Glowing One deals combat damage to a player, they get four rad counters. + * Whenever a player mills a nonland card, you gain 1 life. + * 2/2 + */ + private static final String glowing = "Glowing One"; + + @Test + public void test_MillSelf() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, glowing); + addCard(Zone.HAND, playerA, "Stitcher's Supplier"); // etb, mill 3 + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.LIBRARY, playerA, "Goblin Piker", 2); + addCard(Zone.LIBRARY, playerA, "Taiga", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stitcher's Supplier"); + setChoice(playerA, "Whenever"); // 2 triggers to stack. + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, 3); + assertLife(playerA, 20 + 2); + } + + @Test + public void test_MillOpponent() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, glowing); + addCard(Zone.BATTLEFIELD, playerB, "Swamp"); + addCard(Zone.LIBRARY, playerB, "Goblin Piker", 1); + addCard(Zone.LIBRARY, playerB, "Taiga", 2); + addCard(Zone.LIBRARY, playerB, "Stitcher's Supplier"); // etb, mill 3 + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Stitcher's Supplier"); + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerB, 3); + assertLife(playerA, 20 + 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/InfestingRadroachTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/InfestingRadroachTest.java new file mode 100644 index 00000000000..b4396d3fd9b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/InfestingRadroachTest.java @@ -0,0 +1,80 @@ +package org.mage.test.cards.single.pip; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class InfestingRadroachTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.i.InfestingRadroach Infesting Radroach} {2}{B} + * Creature — Insect Mutant + * Flying + * Infesting Radroach can’t block. + * Whenever Infesting Radroach deals combat damage to a player, they get that many rad counters. + * Whenever an opponent mills a nonland card, if Infesting Radroach is in your graveyard, you may return it to your hand. + * 2/2 + */ + private static final String radroach = "Infesting Radroach"; + + @Test + public void test_MillSelf() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.GRAVEYARD, playerA, radroach); + addCard(Zone.HAND, playerA, "Stitcher's Supplier"); // etb, mill 3 + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.LIBRARY, playerA, "Goblin Piker", 2); + addCard(Zone.LIBRARY, playerA, "Taiga", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stitcher's Supplier"); + // no trigger + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, 4); + } + + @Test + public void test_MillOpponent() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.GRAVEYARD, playerA, radroach); + addCard(Zone.BATTLEFIELD, playerB, "Swamp"); + addCard(Zone.LIBRARY, playerB, "Goblin Piker", 2); + addCard(Zone.LIBRARY, playerB, "Taiga", 1); + addCard(Zone.LIBRARY, playerB, "Stitcher's Supplier"); // etb, mill 3 + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Stitcher's Supplier"); + setChoice(playerA, "Whenever"); // 2 triggers + setChoice(playerA, true); // yes to first, second trigger fizzles + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerB, 3); + assertHandCount(playerA, radroach, 1); + } + + @Test + public void test_Damage() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, radroach); + + attack(1, playerA, radroach, playerB); + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertCounterCount(playerB, CounterType.RAD, 2); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/MillTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/MillTriggeredAbility.java new file mode 100644 index 00000000000..328e49964c2 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/MillTriggeredAbility.java @@ -0,0 +1,79 @@ + +package mage.abilities.common; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.cards.Card; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.UUID; + +/** + * @author Susucr + */ +public class MillTriggeredAbility extends TriggeredAbilityImpl { + + private final TargetController targetController; + private final FilterCard filter; + + public MillTriggeredAbility(Zone zone, Effect effect, TargetController targetController, FilterCard filter, boolean optional) { + super(zone, effect, optional); + this.targetController = targetController; + this.filter = filter; + setTriggerPhrase(generateTriggerPhrase()); + } + + private MillTriggeredAbility(final MillTriggeredAbility ability) { + super(ability); + this.targetController = ability.targetController; + this.filter = ability.filter; + } + + @Override + public MillTriggeredAbility copy() { + return new MillTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.MILLED_CARD; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + UUID playerId = event.getPlayerId(); + switch (targetController) { + case ANY: + // no check. + break; + case OPPONENT: + if (!game.getOpponents(getControllerId()).contains(playerId)) { + return false; + } + break; + default: + throw new IllegalArgumentException("Wrong code usage. targetController not yet supported: " + targetController); + } + Card card = game.getCard(event.getTargetId()); + return card != null && filter.match(card, getControllerId(), this, game); + } + + private String generateTriggerPhrase() { + String text = "Whenever "; + switch (targetController) { + case ANY: + text += "a player "; + break; + case OPPONENT: + text += "an opponent "; + break; + default: + throw new IllegalArgumentException("Wrong code usage. targetController not yet supported: " + targetController); + } + return text + "mills a " + filter.getMessage() + ", "; + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/SourceInGraveyardCondition.java b/Mage/src/main/java/mage/abilities/condition/common/SourceInGraveyardCondition.java new file mode 100644 index 00000000000..700da660e0b --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/SourceInGraveyardCondition.java @@ -0,0 +1,21 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.constants.Zone; +import mage.game.Game; + +/** + * If {this} is in your graveyard + * + * @author Susucr + */ + +public enum SourceInGraveyardCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD; + } +}