From 697586a552d9bee77680b57ec600450f5de839e9 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 29 Aug 2022 18:51:13 -0400 Subject: [PATCH] [DMU] Implement Enlist ability (#9431) * implement enlist ability * remove skips for enlist * [DMU] Implemented Guardian of New Benalia * add test for enlist --- .../mage/cards/g/GuardianOfNewBenalia.java | 90 ++++++++++++ Mage.Sets/src/mage/sets/DominariaUnited.java | 6 +- .../cards/abilities/keywords/EnlistTest.java | 132 ++++++++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 2 +- .../mage/abilities/keyword/EnlistAbility.java | 92 +++++++++++- .../permanent/SummoningSicknessPredicate.java | 4 +- .../main/java/mage/game/events/GameEvent.java | 8 ++ 7 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/g/GuardianOfNewBenalia.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EnlistTest.java diff --git a/Mage.Sets/src/mage/cards/g/GuardianOfNewBenalia.java b/Mage.Sets/src/mage/cards/g/GuardianOfNewBenalia.java new file mode 100644 index 0000000000..d4bdcbb94b --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GuardianOfNewBenalia.java @@ -0,0 +1,90 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.DiscardCardCost; +import mage.abilities.effects.common.TapSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.effects.keyword.ScryEffect; +import mage.abilities.keyword.EnlistAbility; +import mage.abilities.keyword.IndestructibleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GuardianOfNewBenalia extends CardImpl { + + public GuardianOfNewBenalia(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Enlist + this.addAbility(new EnlistAbility()); + + // Whenever Guardian of New Benalia enlists a creature, scry 2. + this.addAbility(new GuardianOfNewBenaliaTriggeredAbility()); + + // Discard a card: Guardian of New Benalia gains indestructible until end of turn. Tap it. + Ability ability = new SimpleActivatedAbility(new GainAbilitySourceEffect( + IndestructibleAbility.getInstance(), Duration.EndOfTurn + ), new DiscardCardCost()); + ability.addEffect(new TapSourceEffect().setText("tap it")); + this.addAbility(ability); + } + + private GuardianOfNewBenalia(final GuardianOfNewBenalia card) { + super(card); + } + + @Override + public GuardianOfNewBenalia copy() { + return new GuardianOfNewBenalia(this); + } +} + +class GuardianOfNewBenaliaTriggeredAbility extends TriggeredAbilityImpl { + + GuardianOfNewBenaliaTriggeredAbility() { + super(Zone.BATTLEFIELD, new ScryEffect(2)); + } + + private GuardianOfNewBenaliaTriggeredAbility(final GuardianOfNewBenaliaTriggeredAbility ability) { + super(ability); + } + + @Override + public GuardianOfNewBenaliaTriggeredAbility copy() { + return new GuardianOfNewBenaliaTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CREATURE_ENLISTED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return getSourceId().equals(event.getSourceId()); + } + + @Override + public String getRule() { + return "Whenever {this} enlists a creature, scry 2."; + } +} diff --git a/Mage.Sets/src/mage/sets/DominariaUnited.java b/Mage.Sets/src/mage/sets/DominariaUnited.java index f989f6369a..3b384b4847 100644 --- a/Mage.Sets/src/mage/sets/DominariaUnited.java +++ b/Mage.Sets/src/mage/sets/DominariaUnited.java @@ -4,16 +4,11 @@ import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ public final class DominariaUnited extends ExpansionSet { - private static final List unfinished = Arrays.asList("Argivian Cavalier", "Balduvian Berserker", "Barkweave Crusher", "Benalish Faithbonder", "Coalition Skyknight", "Coalition Warbrute", "Guardian of New Benalia", "Hexbane Tortoise", "Keldon Flamesage", "Linebreaker Baloth", "Yavimaya Steelcrusher"); - private static final DominariaUnited instance = new DominariaUnited(); public static DominariaUnited getInstance() { @@ -105,6 +100,7 @@ public final class DominariaUnited extends ExpansionSet { cards.add(new SetCardInfo("Gibbering Barricade", 95, Rarity.COMMON, mage.cards.g.GibberingBarricade.class)); cards.add(new SetCardInfo("Goblin Picker", 128, Rarity.COMMON, mage.cards.g.GoblinPicker.class)); cards.add(new SetCardInfo("Griffin Protector", 18, Rarity.COMMON, mage.cards.g.GriffinProtector.class)); + cards.add(new SetCardInfo("Guardian of New Benalia", 19, Rarity.RARE, mage.cards.g.GuardianOfNewBenalia.class)); cards.add(new SetCardInfo("Hammerhand", 129, Rarity.COMMON, mage.cards.h.Hammerhand.class)); cards.add(new SetCardInfo("Haughty Djinn", 52, Rarity.RARE, mage.cards.h.HaughtyDjinn.class)); cards.add(new SetCardInfo("Haunted Mire", 248, Rarity.COMMON, mage.cards.h.HauntedMire.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EnlistTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EnlistTest.java new file mode 100644 index 0000000000..9598f5ffaf --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EnlistTest.java @@ -0,0 +1,132 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class EnlistTest extends CardTestPlayerBase { + + private static final String crusher = "Barkweave Crusher"; + private static final String lion = "Silvercoat Lion"; + private static final String goblin = "Raging Goblin"; + private static final String angel = "Serra Angel"; + + @Test + public void testRegularChooseYes() { + addCard(Zone.BATTLEFIELD, playerA, crusher); + addCard(Zone.BATTLEFIELD, playerA, lion); + + attack(1, playerA, crusher); + setChoice(playerA, true); + setChoice(playerA, lion); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, crusher, 2 + 2, 5); + assertTapped(crusher, true); + assertTapped(lion, true); + assertLife(playerB, 20 - 2 - 2); + } + + @Test + public void testRegularChooseNo() { + addCard(Zone.BATTLEFIELD, playerA, crusher); + addCard(Zone.BATTLEFIELD, playerA, lion); + + attack(1, playerA, crusher); + setChoice(playerA, false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, crusher, 2, 5); + assertTapped(crusher, true); + assertTapped(lion, false); + assertLife(playerB, 20 - 2); + } + + @Test + public void testSummoningSick() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + addCard(Zone.BATTLEFIELD, playerA, crusher); + addCard(Zone.HAND, playerA, lion); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lion); + + attack(1, playerA, crusher); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, crusher, 2, 5); + assertTapped(crusher, true); + assertTapped(lion, false); + assertLife(playerB, 20 - 2); + } + + @Test + public void testAttackWithBoth() { + addCard(Zone.BATTLEFIELD, playerA, crusher); + addCard(Zone.BATTLEFIELD, playerA, lion); + + attack(1, playerA, crusher); + attack(1, playerA, lion); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, crusher, 2, 5); + assertTapped(crusher, true); + assertTapped(lion, true); + assertLife(playerB, 20 - 2 - 2); + } + + @Test + public void testHaste() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, crusher); + addCard(Zone.HAND, playerA, goblin); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblin); + + attack(1, playerA, crusher); + setChoice(playerA, true); + setChoice(playerA, goblin); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, crusher, 2 + 1, 5); + assertTapped(crusher, true); + assertTapped(goblin, true); + assertLife(playerB, 20 - 2 - 1); + } + + @Test + public void testVigilance() { + addCard(Zone.BATTLEFIELD, playerA, crusher); + addCard(Zone.BATTLEFIELD, playerA, angel); + + attack(1, playerA, crusher); + attack(1, playerA, angel); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, crusher, 2, 5); + assertTapped(crusher, true); + assertTapped(angel, false); + assertLife(playerB, 20 - 2 - 4); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 72717bf0d9..f36fc81980 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -1733,7 +1733,7 @@ public class TestPlayer implements Player { // Second check to filter creature for combat - less strict to workaround issue in #3038 FilterCreatureForCombat secondFilter = new FilterCreatureForCombat(); // secondFilter.add(Predicates.not(AttackingPredicate.instance)); - secondFilter.add(Predicates.not(new SummoningSicknessPredicate())); + secondFilter.add(Predicates.not(SummoningSicknessPredicate.instance)); // TODO: Cannot enforce legal attackers multiple times per combat. See issue #3038 Permanent attacker = findPermanent(secondFilter, groups[0], computerPlayer.getId(), game, false); if (attacker != null && attacker.canAttack(defenderId, game)) { diff --git a/Mage/src/main/java/mage/abilities/keyword/EnlistAbility.java b/Mage/src/main/java/mage/abilities/keyword/EnlistAbility.java index ce9c79e788..d56cf9b2d4 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EnlistAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EnlistAbility.java @@ -1,16 +1,33 @@ package mage.abilities.keyword; +import mage.abilities.Ability; import mage.abilities.StaticAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.constants.Duration; +import mage.constants.Outcome; import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.filter.predicate.permanent.AttackingPredicate; +import mage.filter.predicate.permanent.SummoningSicknessPredicate; +import mage.filter.predicate.permanent.TappedPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; /** * @author TheElk801 - * TODO: Implement this */ public class EnlistAbility extends StaticAbility { public EnlistAbility() { - super(Zone.BATTLEFIELD, null); + super(Zone.BATTLEFIELD, new EnlistEffect()); } private EnlistAbility(final EnlistAbility ability) { @@ -28,3 +45,74 @@ public class EnlistAbility extends StaticAbility { "without summoning sickness. When you do, add its power to this creature's until end of turn.)"; } } + +class EnlistEffect extends ReplacementEffectImpl { + + private static final FilterPermanent filter = new FilterControlledCreaturePermanent( + "another untapped nonattacking creature you control without summoning sickness" + ); + + static { + filter.add(AnotherPredicate.instance); + filter.add(TappedPredicate.UNTAPPED); + filter.add(Predicates.not(SummoningSicknessPredicate.instance)); + filter.add(Predicates.not(AttackingPredicate.instance)); + } + + public EnlistEffect() { + super(Duration.WhileOnBattlefield, Outcome.Detriment); + } + + public EnlistEffect(EnlistEffect effect) { + super(effect); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ATTACKER_DECLARED; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return event.getSourceId().equals(source.getSourceId()); + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Permanent creature = game.getPermanent(event.getSourceId()); + Player controller = game.getPlayer(source.getControllerId()); + if (creature == null || controller == null + || !game.getBattlefield().contains(filter, source, game, 1) + || !controller.chooseUse(outcome, "Enlist a creature for " + creature.getLogName() + '?', source, game)) { + return false; + } + TargetPermanent target = new TargetPermanent(filter); + target.setNotTarget(true); + controller.choose(outcome, target, source, game); + Permanent permanent = game.getPermanent(target.getFirstTarget()); + if (permanent == null || !permanent.tap(source, game)) { + return false; + } + game.fireReflexiveTriggeredAbility(new ReflexiveTriggeredAbility( + new BoostSourceEffect( + permanent.getPower().getValue(), + 0, Duration.EndOfTurn + ), false + ), source); + game.fireEvent(GameEvent.getEvent( + GameEvent.EventType.CREATURE_ENLISTED, + permanent.getId(), source, source.getControllerId() + )); + return false; + } + + @Override + public EnlistEffect copy() { + return new EnlistEffect(this); + } +} diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/SummoningSicknessPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/SummoningSicknessPredicate.java index d5404fd97f..ac932de5d9 100644 --- a/Mage/src/main/java/mage/filter/predicate/permanent/SummoningSicknessPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/permanent/SummoningSicknessPredicate.java @@ -5,10 +5,10 @@ import mage.game.Game; import mage.game.permanent.Permanent; /** - * * @author LevelX2 */ -public class SummoningSicknessPredicate implements Predicate { +public enum SummoningSicknessPredicate implements Predicate { + instance; @Override public boolean apply(Permanent input, Game game) { diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 985ae9448f..be0dfdd2fe 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -354,6 +354,14 @@ public class GameEvent implements Serializable { BECOMES_EXERTED, BECOMES_RENOWNED, GAINS_CLASS_LEVEL, + /* CREATURE_ENLISTED + targetId id of the enlisted creature + sourceId id of the creature that enlisted + playerId player who controls the creatures + amount not used for this event + flag not used for this event + */ + CREATURE_ENLISTED, /* BECOMES_MONARCH targetId playerId of the player that becomes the monarch sourceId id of the source object that created that effect, if no effect exist it's null