From e39e5ee1b088a9530e2af1d0653ebcda24492b66 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sun, 27 Aug 2023 01:33:52 +0200 Subject: [PATCH] [WOC] Implement Alela, Cunning Conqueror (#10870) Add new batch event `DAMAGED_PLAYER_BATCH_ONE_PLAYER` --- .../mage/cards/a/AlelaCunningConqueror.java | 129 ++++++++++++++++++ .../mage/sets/WildsOfEldraineCommander.java | 1 + .../single/woe/AlelaCunningConquerorTest.java | 113 +++++++++++++++ Mage/src/main/java/mage/game/GameState.java | 52 +++++-- .../DamagedPlayerBatchOnePlayerEvent.java | 14 ++ .../main/java/mage/game/events/GameEvent.java | 5 + 6 files changed, 305 insertions(+), 9 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/a/AlelaCunningConqueror.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/woe/AlelaCunningConquerorTest.java create mode 100644 Mage/src/main/java/mage/game/events/DamagedPlayerBatchOnePlayerEvent.java diff --git a/Mage.Sets/src/mage/cards/a/AlelaCunningConqueror.java b/Mage.Sets/src/mage/cards/a/AlelaCunningConqueror.java new file mode 100644 index 00000000000..567ef54d6ef --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AlelaCunningConqueror.java @@ -0,0 +1,129 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.FirstSpellOpponentsTurnTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.combat.GoadTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.ControllerIdPredicate; +import mage.game.Game; +import mage.game.events.DamagedBatchEvent; +import mage.game.events.DamagedEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.FaerieRogueToken; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author Susucr + */ +public final class AlelaCunningConqueror extends CardImpl { + + public AlelaCunningConqueror(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.FAERIE); + this.subtype.add(SubType.WARLOCK); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever you cast your first spell during each opponent's turn, create a 1/1 black Faerie Rogue creature token with flying. + this.addAbility(new FirstSpellOpponentsTurnTriggeredAbility( + new CreateTokenEffect(new FaerieRogueToken()), false + )); + + // Whenever one or more Faeries you control deal combat damage to a player, goad target creature that player controls. + TriggeredAbility trigger = new AlelaCunningConquerorTriggeredAbility(); + this.addAbility(trigger); + } + + private AlelaCunningConqueror(final AlelaCunningConqueror card) { + super(card); + } + + @Override + public AlelaCunningConqueror copy() { + return new AlelaCunningConqueror(this); + } +} + + +class AlelaCunningConquerorTriggeredAbility extends TriggeredAbilityImpl { + + AlelaCunningConquerorTriggeredAbility() { + super(Zone.BATTLEFIELD, new GoadTargetEffect(), false); + } + + private AlelaCunningConquerorTriggeredAbility(final AlelaCunningConquerorTriggeredAbility ability) { + super(ability); + } + + @Override + public AlelaCunningConquerorTriggeredAbility copy() { + return new AlelaCunningConquerorTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_PLAYER_BATCH_ONE_PLAYER; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + DamagedBatchEvent dEvent = (DamagedBatchEvent) event; + if (dEvent == null) { + return false; + } + + List events = dEvent + .getEvents() + .stream() + .filter(DamagedEvent::isCombatDamage) + .filter(e -> { + Permanent permanent = game.getPermanentOrLKIBattlefield(e.getSourceId()); + return permanent != null + && permanent.hasSubtype(SubType.FAERIE, game) + && permanent.isControlledBy(this.getControllerId()); + }) + .collect(Collectors.toList()); + + if (events.isEmpty()) { + return false; + } + + Player opponent = game.getPlayer(dEvent.getPlayerId()); + if (opponent == null) { + return false; + } + + FilterCreaturePermanent filter = new FilterCreaturePermanent("creature " + opponent.getLogName() + " controls"); + filter.add(new ControllerIdPredicate(opponent.getId())); + this.getTargets().clear(); + this.addTarget(new TargetCreaturePermanent(filter)); + return true; + } + + @Override + public String getRule() { + return "Whenever one or more Faeries you control " + + "deal combat damage to a player, goad target creature that player controls."; + } +} diff --git a/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java b/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java index 567eca64eac..0ddf96ca5df 100644 --- a/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java +++ b/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java @@ -20,6 +20,7 @@ public final class WildsOfEldraineCommander extends ExpansionSet { this.hasBasicLands = false; cards.add(new SetCardInfo("Ajani's Chosen", 59, Rarity.RARE, mage.cards.a.AjanisChosen.class)); + cards.add(new SetCardInfo("Alela, Cunning Conqueror", 3, Rarity.MYTHIC, mage.cards.a.AlelaCunningConqueror.class)); cards.add(new SetCardInfo("Ancestral Mask", 119, Rarity.COMMON, mage.cards.a.AncestralMask.class)); cards.add(new SetCardInfo("Angelic Destiny", 60, Rarity.MYTHIC, mage.cards.a.AngelicDestiny.class)); cards.add(new SetCardInfo("Arcane Denial", 84, Rarity.COMMON, mage.cards.a.ArcaneDenial.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/woe/AlelaCunningConquerorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/woe/AlelaCunningConquerorTest.java new file mode 100644 index 00000000000..053db728e39 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/woe/AlelaCunningConquerorTest.java @@ -0,0 +1,113 @@ +package org.mage.test.cards.single.woe; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.multiplayer.MultiplayerTriggerTest; +import org.mage.test.player.TestPlayer; + +/** + * @author Susucr + */ +public class AlelaCunningConquerorTest extends MultiplayerTriggerTest { + + /** + * Alela, Cunning Conqueror + * {2}{U}{B} + * Legendary Creature — Faerie Warlock + *

+ * Flying + *

+ * Whenever you cast your first spell during each opponent’s turn, create a 1/1 black Faerie Rogue creature token with flying. + *

+ * Whenever one or more Faeries you control deal combat damage to a player, goad target creature that player controls. + */ + private static final String alela = "Alela, Cunning Conqueror"; + + // 2/1 Faerie + private static final String pestermite = "Pestermite"; + // not a Faerie + private static final String stormCrow = "Storm Crow"; + + // Bears for playerB + private static final String bears = "Grizzly Bears"; + + // Carp for playerC + private static final String carp = "Ancient Carp"; + + // Devoted for playerD + private static final String devoted = "Devoted Hero"; + + + @Test + public void attackSinglePlayer() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, alela); + addCard(Zone.BATTLEFIELD, playerA, pestermite); + addCard(Zone.BATTLEFIELD, playerB, bears); + addCard(Zone.BATTLEFIELD, playerC, carp); + addCard(Zone.BATTLEFIELD, playerD, devoted); + + attack(1, playerA, alela, playerC); + attack(1, playerA, pestermite, playerC); + addTarget(playerA, carp); // One trigger to goad a creature for playerC + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertGoadedByPlayer(carp, playerA); + assertLife(playerC, 40 - 2 - 2); + } + + @Test + public void attackTwoPlayers() { + //setStrictChooseMode(true); // did not succesfully differentiate the two very similar triggers to order them in the stack. + addCard(Zone.BATTLEFIELD, playerA, alela); + addCard(Zone.BATTLEFIELD, playerA, pestermite); + addCard(Zone.BATTLEFIELD, playerB, bears); + addCard(Zone.BATTLEFIELD, playerC, carp); + addCard(Zone.BATTLEFIELD, playerD, devoted); + + attack(1, playerA, alela, playerB); + attack(1, playerA, pestermite, playerD); + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertGoadedByPlayer(bears, playerA); + assertGoadedByPlayer(devoted, playerA); + assertLife(playerB, 40 - 2); + assertLife(playerD, 40 - 2); + } + + @Test + public void attackWithNotAFaerie() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, alela); + addCard(Zone.BATTLEFIELD, playerA, stormCrow); + addCard(Zone.BATTLEFIELD, playerB, bears); + addCard(Zone.BATTLEFIELD, playerC, carp); + addCard(Zone.BATTLEFIELD, playerD, devoted); + + attack(1, playerA, alela, playerB); + attack(1, playerA, stormCrow, playerD); + addTarget(playerA, bears); // One trigger to goad a creature for playerB + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertGoadedByPlayer(bears, playerA); + assertLife(playerB, 40 - 2); + assertLife(playerD, 40 - 1); + } + + private void assertGoadedByPlayer(String attacker, TestPlayer player) { + Permanent permanent = getPermanent(attacker); + Assert.assertTrue( + "Creature should be goaded by " + player.getName(), + permanent.getGoadingPlayers().contains(player.getId()) + ); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 4587b95cbbb..8118b40bc72 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -1,5 +1,6 @@ package mage.game; +import static java.util.Collections.emptyList; import mage.MageObject; import mage.MageObjectReference; import mage.abilities.*; @@ -44,8 +45,6 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; -import static java.util.Collections.emptyList; - /** * @author BetaSteward_at_googlemail.com *

@@ -817,21 +816,56 @@ public class GameState implements Serializable, Copyable { } public void addSimultaneousDamage(DamagedEvent damagedEvent, Game game) { - // combine damages per type (player or permanent) - boolean flag = false; + // This method does look for Batch Event in simultaneousEvents to batch + // damagedEvent with. For each kind of batch, either there is a batch + // of the proper class fitting the damagedEvent, or there is not. + // + // If there is not one of the batched event (i.e. if the respective flag is + // at false after the loop), then a batched event is created for future + // events to be batched with. + + // All damage from any source to anything + // the batch is of class DamagedBatchEvent + boolean flagBatchAll = false; + + // All damage from any source to a specific player (damagedEvent.getPlayerId()) + // the batch is of class DamagedPlayerBatchOnePlayerEvent + boolean flagBatchForPlayer = false; + for (GameEvent event : simultaneousEvents) { + if ((event instanceof DamagedBatchEvent) && ((DamagedBatchEvent) event).getDamageClazz().isInstance(damagedEvent)) { - // old batch + + // existing batch for damage of that damage class. ((DamagedBatchEvent) event).addEvent(damagedEvent); - flag = true; - break; + flagBatchAll = true; } + + if (event instanceof DamagedPlayerBatchOnePlayerEvent) { + DamagedPlayerBatchOnePlayerEvent eventForPlayer = (DamagedPlayerBatchOnePlayerEvent) event; + if (eventForPlayer.getDamageClazz().isInstance(damagedEvent) + && event.getPlayerId().equals(damagedEvent.getPlayerId())) { + + // existing batch for damage of that damage class to the same player + eventForPlayer.addEvent(damagedEvent); + flagBatchForPlayer = true; + } + } + } - if (!flag) { - // new batch + + if (!flagBatchAll) { + // new batch for any kind of damage, creating a fresh one with damagedEvent inside. addSimultaneousEvent(DamagedBatchEvent.makeEvent(damagedEvent), game); } + if (!flagBatchForPlayer && damagedEvent.getPlayerId() != null) { + // new batch for damage from any source to the specific damaged player, + // creating a fresh one with damagedEvent inside. + DamagedBatchEvent event = new DamagedPlayerBatchOnePlayerEvent(damagedEvent.getPlayerId()); + event.addEvent(damagedEvent); + addSimultaneousEvent(event, game); + } } public void handleEvent(GameEvent event, Game game) { diff --git a/Mage/src/main/java/mage/game/events/DamagedPlayerBatchOnePlayerEvent.java b/Mage/src/main/java/mage/game/events/DamagedPlayerBatchOnePlayerEvent.java new file mode 100644 index 00000000000..5df4a71e7a3 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/DamagedPlayerBatchOnePlayerEvent.java @@ -0,0 +1,14 @@ +package mage.game.events; + +import java.util.UUID; + +/** + * @author Susucr + */ +public class DamagedPlayerBatchOnePlayerEvent extends DamagedBatchEvent { + + public DamagedPlayerBatchOnePlayerEvent(UUID playerId) { + super(EventType.DAMAGED_PLAYER_BATCH_ONE_PLAYER, DamagedPlayerEvent.class); + this.setPlayerId(playerId); + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index f26e7f5afd2..572b93cf243 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -123,6 +123,11 @@ public class GameEvent implements Serializable { */ DAMAGED_PLAYER_BATCH, + /* DAMAGED_PLAYER_BATCH_ONE_PLAYER + combines all player damaged events for a single player in one single event + */ + DAMAGED_PLAYER_BATCH_ONE_PLAYER, + /* DAMAGE_CAUSES_LIFE_LOSS, targetId the id of the damaged player sourceId sourceId of the ability which caused the damage, can be null for default events like combat