From 39008586d052ed6d2a98855c4b05cbefc1e5e187 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:45:28 +0200 Subject: [PATCH] [OTJ] Implement Kambal, Profiteering Mayor This is slightly bugged due to effect creating different tokens creating them not at the same time. See Bestial Menace unit test & #10811 for more info --- .../mage/cards/k/KambalProfiteeringMayor.java | 152 ++++++++++++++++++ .../mage/sets/OutlawsOfThunderJunction.java | 1 + .../otj/KambalProfiteeringMayorTest.java | 89 ++++++++++ 3 files changed, 242 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/k/KambalProfiteeringMayor.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/otj/KambalProfiteeringMayorTest.java diff --git a/Mage.Sets/src/mage/cards/k/KambalProfiteeringMayor.java b/Mage.Sets/src/mage/cards/k/KambalProfiteeringMayor.java new file mode 100644 index 00000000000..fc9169f919b --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KambalProfiteeringMayor.java @@ -0,0 +1,152 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.EntersBattlefieldOneOrMoreTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenCopyTargetEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.predicate.permanent.TokenPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeBatchEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentToken; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author Susucr + */ +public final class KambalProfiteeringMayor extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("tokens"); + + static { + filter.add(TokenPredicate.TRUE); + } + + public KambalProfiteeringMayor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ADVISOR); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Whenever one or more tokens enter the battlefield under your opponents' control, for each of them, create a tapped token that's a copy of it. This ability triggers only once each turn. + this.addAbility(new KambalProfiteeringMayorTriggeredAbility()); + + // Whenever one or more tokens enter the battlefield under your control, each opponent loses 1 life and you gain 1 life. + Ability ability = new EntersBattlefieldOneOrMoreTriggeredAbility( + new LoseLifeOpponentsEffect(1), filter, TargetController.YOU + ); + ability.addEffect(new GainLifeEffect(1).concatBy("and")); + this.addAbility(ability); + } + + private KambalProfiteeringMayor(final KambalProfiteeringMayor card) { + super(card); + } + + @Override + public KambalProfiteeringMayor copy() { + return new KambalProfiteeringMayor(this); + } +} + +class KambalProfiteeringMayorTriggeredAbility extends TriggeredAbilityImpl { + + KambalProfiteeringMayorTriggeredAbility() { + super(Zone.BATTLEFIELD, null); + this.setTriggersOnceEachTurn(true); + } + + private KambalProfiteeringMayorTriggeredAbility(final KambalProfiteeringMayorTriggeredAbility effect) { + super(effect); + } + + @Override + public KambalProfiteeringMayorTriggeredAbility copy() { + return new KambalProfiteeringMayorTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event; + Player controller = game.getPlayer(this.controllerId); + List tokensIds = zEvent.getEvents() + .stream() + .filter(zce -> zce.getToZone() == Zone.BATTLEFIELD // keep enter the battlefield + && controller.hasOpponent(zce.getPlayerId(), game)) // & under your opponent's control + .map(ZoneChangeEvent::getTarget) + .filter(Objects::nonNull) + .filter(p -> p instanceof PermanentToken) // collect only tokens + .map(Permanent::getId) + .collect(Collectors.toList()); + if (tokensIds.isEmpty()) { + return false; + } + this.getEffects().clear(); + this.addEffect(new KambalProfiteeringMayorEffect(tokensIds)); + return true; + } + + @Override + public String getRule() { + return "Whenever one or more tokens enter the battlefield under your opponents' control, " + + "for each of them, create a tapped token that's a copy of it. " + + "This ability triggers only once each turn."; + } + +} + +class KambalProfiteeringMayorEffect extends OneShotEffect { + + private final List tokensIds; + + KambalProfiteeringMayorEffect(List tokensIds) { + super(Outcome.PutCreatureInPlay); + this.tokensIds = new ArrayList<>(tokensIds); + } + + private KambalProfiteeringMayorEffect(final KambalProfiteeringMayorEffect effect) { + super(effect); + this.tokensIds = new ArrayList<>(effect.tokensIds); + } + + @Override + public KambalProfiteeringMayorEffect copy() { + return new KambalProfiteeringMayorEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (UUID tokenId : tokensIds) { + new CreateTokenCopyTargetEffect(source.getControllerId(), null, false, 1, true, false) + .setTargetPointer(new FixedTarget(tokenId)) + .apply(game, source); + } + return true; + } + +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java b/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java index 59ad633b21f..26255e95a54 100644 --- a/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java +++ b/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java @@ -145,6 +145,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("Jem Lightfoote, Sky Explorer", 209, Rarity.UNCOMMON, mage.cards.j.JemLightfooteSkyExplorer.class)); cards.add(new SetCardInfo("Jolene, Plundering Pugilist", 210, Rarity.UNCOMMON, mage.cards.j.JolenePlunderingPugilist.class)); cards.add(new SetCardInfo("Kaervek, the Punisher", 92, Rarity.RARE, mage.cards.k.KaervekThePunisher.class)); + cards.add(new SetCardInfo("Kambal, Profiteering Mayor", 211, Rarity.RARE, mage.cards.k.KambalProfiteeringMayor.class)); cards.add(new SetCardInfo("Kellan Joins Up", 212, Rarity.RARE, mage.cards.k.KellanJoinsUp.class)); cards.add(new SetCardInfo("Kraum, Violent Cacophony", 214, Rarity.UNCOMMON, mage.cards.k.KraumViolentCacophony.class)); cards.add(new SetCardInfo("Lassoed by the Law", 18, Rarity.UNCOMMON, mage.cards.l.LassoedByTheLaw.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/KambalProfiteeringMayorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/KambalProfiteeringMayorTest.java new file mode 100644 index 00000000000..5866e8a215f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/KambalProfiteeringMayorTest.java @@ -0,0 +1,89 @@ +package org.mage.test.cards.single.otj; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class KambalProfiteeringMayorTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.k.KambalProfiteeringMayor Kambal, Profiteering Mayor} {1}{W}{B} + * Legendary Creature — Human Advisor + * Whenever one or more tokens enter the battlefield under your opponents’ control, for each of them, create a tapped token that’s a copy of it. This ability triggers only once each turn. + * Whenever one or more tokens enter the battlefield under your control, each opponent loses 1 life and you gain 1 life. + * 2/4 + */ + private static final String kambal = "Kambal, Profiteering Mayor"; + + @Test + public void Test_KrenkosCommand() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, kambal); + // Create two 1/1 red Goblin creature tokens. + addCard(Zone.HAND, playerA, "Krenko's Command"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Krenko's Command"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 1); // Second Trigger happened + assertLife(playerB, 20 + 1); // Second Trigger happened + assertPermanentCount(playerA, "Goblin Token", 2); + assertPermanentCount(playerB, "Goblin Token", 2); + } + + @Test + public void Test_BestialMenace() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, kambal); + // Create a 1/1 green Snake creature token, a 2/2 green Wolf creature token, and a 3/3 green Elephant creature token. + addCard(Zone.HAND, playerA, "Bestial Menace"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bestial Menace"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 1); // Second Trigger happened + assertLife(playerB, 20 + 1); // Second Trigger happened + assertPermanentCount(playerA, "Snake Token", 1); + assertPermanentCount(playerA, "Wolf Token", 1); + assertPermanentCount(playerA, "Elephant Token", 1); + assertPermanentCount(playerB, "Snake Token", 1); + assertPermanentCount(playerB, "Wolf Token", 0); // TODO: this is a bug, should be 1, see #10811 + assertPermanentCount(playerB, "Elephant Token", 0); // TODO: this is a bug, should be 1, see #10811 + } + + @Test + public void Test_Chatterfang() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, kambal); + // Create two 1/1 red Goblin creature tokens. + addCard(Zone.HAND, playerA, "Krenko's Command"); + // If one or more tokens would be created under your control, those tokens plus that many 1/1 green Squirrel creature tokens are created instead. + addCard(Zone.BATTLEFIELD, playerA, "Chatterfang, Squirrel General"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Krenko's Command"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 1); // Second Trigger happened + assertLife(playerB, 20 + 1); // Second Trigger happened + assertPermanentCount(playerA, "Goblin Token", 2); + assertPermanentCount(playerA, "Squirrel Token", 2); + assertPermanentCount(playerB, "Goblin Token", 2); + assertPermanentCount(playerB, "Squirrel Token", 2); + } +}