From 98c554a59f1fa42b8530ba678d3241407a3e9045 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Mon, 4 Jul 2022 15:55:47 +0200 Subject: [PATCH] [NCC] Implemented Jolene, the Plunder Queen (#9093) --- .../mage/cards/j/JoleneThePlunderQueen.java | 199 ++++++++++++++++++ .../src/mage/sets/NewCapennaCommander.java | 2 + .../single/ncc/JoleneThePlunderQueenTest.java | 133 ++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/j/JoleneThePlunderQueen.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/JoleneThePlunderQueenTest.java diff --git a/Mage.Sets/src/mage/cards/j/JoleneThePlunderQueen.java b/Mage.Sets/src/mage/cards/j/JoleneThePlunderQueen.java new file mode 100644 index 00000000000..fc80218f93c --- /dev/null +++ b/Mage.Sets/src/mage/cards/j/JoleneThePlunderQueen.java @@ -0,0 +1,199 @@ +package mage.cards.j; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.common.FilterControlledPermanent; +import mage.game.Game; +import mage.game.combat.Combat; +import mage.game.events.CreateTokenEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.Token; +import mage.game.permanent.token.TreasureToken; +import mage.target.common.TargetControlledPermanent; +import mage.target.targetpointer.FixedTarget; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * + * @author Susucre + */ +public final class JoleneThePlunderQueen extends CardImpl { + + private static final FilterControlledPermanent filterTreasures = new FilterControlledPermanent(SubType.TREASURE, "treasures"); + + public JoleneThePlunderQueen(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Whenever a player attacks one or more of your opponents, that attacking player creates a Treasure token. + this.addAbility(new JoleneThePlunderQueenTriggeredAbility()); + + // If you would create one or more Treasure tokens, instead create those tokens plus an additional Treasure token. + this.addAbility(new SimpleStaticAbility(new JoleneThePlunderQueenReplacementEffect())); + + // Sacrifice five Treasures: Put five +1/+1 counters on Jolene. + this.addAbility(new SimpleActivatedAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance(5)), + new SacrificeTargetCost(new TargetControlledPermanent(5, filterTreasures)) + )); + } + + private JoleneThePlunderQueen(final JoleneThePlunderQueen card) { + super(card); + } + + @Override + public JoleneThePlunderQueen copy() { + return new JoleneThePlunderQueen(this); + } +} + +// Based loosely on "Breena, the Demagogue" and "Mila, Crafty Companion"'s trigger abilities +class JoleneThePlunderQueenTriggeredAbility extends TriggeredAbilityImpl { + + JoleneThePlunderQueenTriggeredAbility() { + super(Zone.BATTLEFIELD, new JoleneThePlunderQueenCreateTreasureEffect(), false); + } + + private JoleneThePlunderQueenTriggeredAbility(final JoleneThePlunderQueenTriggeredAbility ability) { + super(ability); + } + + @Override + public JoleneThePlunderQueenTriggeredAbility copy() { + return new JoleneThePlunderQueenTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DECLARED_ATTACKERS; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Combat combat = game.getCombat(); + UUID joleneController = game.getControllerId(sourceId); + Set joleneOpponents = game.getOpponents(joleneController); + + // At most one trigger per combat. + if(!combat.getAttackers() + .stream() + .anyMatch(attackerId -> { + // The trigger attempts to find at least one (attacker,defender) + // for which the defender is one of jolene's controller opponent + UUID defenderId = combat.getDefenderId(attackerId); + return joleneOpponents.contains(defenderId); + })){ + return false; + } + + getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + return true; + } + + @Override + public String getRule() { + return "Whenever a player attacks one of your opponents, " + + "that attacking player creates a Treasure token."; + } +} + +class JoleneThePlunderQueenCreateTreasureEffect extends OneShotEffect { + + JoleneThePlunderQueenCreateTreasureEffect() { + super(Outcome.Benefit); + staticText = "that attacking player creates a Treasure token"; + } + + private JoleneThePlunderQueenCreateTreasureEffect(final JoleneThePlunderQueenCreateTreasureEffect effect) { + super(effect); + } + + @Override + public JoleneThePlunderQueenCreateTreasureEffect copy() { + return new JoleneThePlunderQueenCreateTreasureEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (UUID playerId : getTargetPointer().getTargets(game, source)){ + new TreasureToken().putOntoBattlefield(1, game, source, playerId); + } + return true; + } +} + +// Identical to "Xorn"'s Replacement Effect +class JoleneThePlunderQueenReplacementEffect extends ReplacementEffectImpl { + + public JoleneThePlunderQueenReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + this.staticText = "If you would create one or more Treasure tokens, instead create those tokens plus an additional Treasure token"; + } + + private JoleneThePlunderQueenReplacementEffect(final JoleneThePlunderQueenReplacementEffect effect) { + super(effect); + } + + @Override + public JoleneThePlunderQueenReplacementEffect copy() { + return new JoleneThePlunderQueenReplacementEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CREATE_TOKEN; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (event instanceof CreateTokenEvent && source.isControlledBy(event.getPlayerId())) { + for (Token token : ((CreateTokenEvent) event).getTokens().keySet()) { + if (token.hasSubtype(SubType.TREASURE, game)) { + return true; + } + } + } + return false; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + if (event instanceof CreateTokenEvent) { + CreateTokenEvent tokenEvent = (CreateTokenEvent) event; + TreasureToken treasureToken = null; + Map tokens = tokenEvent.getTokens(); + for (Token token : tokens.keySet()) { + if (token instanceof TreasureToken) { + treasureToken = (TreasureToken) token; + break; + } + } + if (treasureToken == null) { + treasureToken = new TreasureToken(); + } + tokens.put(treasureToken, tokens.getOrDefault(treasureToken, 0) + 1); + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/NewCapennaCommander.java b/Mage.Sets/src/mage/sets/NewCapennaCommander.java index 1fcb122a8f5..c1f9ade9efd 100644 --- a/Mage.Sets/src/mage/sets/NewCapennaCommander.java +++ b/Mage.Sets/src/mage/sets/NewCapennaCommander.java @@ -168,6 +168,8 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Izzet Signet", 369, Rarity.UNCOMMON, mage.cards.i.IzzetSignet.class)); cards.add(new SetCardInfo("Jailbreak", 17, Rarity.RARE, mage.cards.j.Jailbreak.class)); cards.add(new SetCardInfo("Jenara, Asura of War", 343, Rarity.MYTHIC, mage.cards.j.JenaraAsuraOfWar.class)); + cards.add(new SetCardInfo("Jolene, the Plunder Queen", 73, Rarity.RARE, mage.cards.j.JoleneThePlunderQueen.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Jolene, the Plunder Queen", 173, Rarity.RARE, mage.cards.j.JoleneThePlunderQueen.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Jund Panorama", 408, Rarity.COMMON, mage.cards.j.JundPanorama.class)); cards.add(new SetCardInfo("Jungle Shrine", 409, Rarity.UNCOMMON, mage.cards.j.JungleShrine.class)); cards.add(new SetCardInfo("Kamiz, Obscura Oculus", 3, Rarity.MYTHIC, mage.cards.k.KamizObscuraOculus.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/JoleneThePlunderQueenTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/JoleneThePlunderQueenTest.java new file mode 100644 index 00000000000..766d16c7127 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/JoleneThePlunderQueenTest.java @@ -0,0 +1,133 @@ + +package org.mage.test.cards.single.ncc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +/** + * + * @author Susucre + */ +public class JoleneThePlunderQueenTest extends CardTestCommander4Players { + + /* + Jolene, the Plunder Queen {2}{R}{G} + Legendary Creature — Human Warrior 2/2 + + Whenever a player attacks one or more of your opponents, that attacking player creates a Treasure token. + If you would create one or more Treasure tokens, instead create those tokens plus an additional Treasure token. + Sacrifice five Treasures: Put five +1/+1 counters on Jolene. + */ + String jolene = "Jolene, the Plunder Queen"; + + /* + Elite Vanguard {W} + Creature — Human Soldier 2/1 + */ + String vanguard = "Elite Vanguard"; + + /* + Balduvian Bears {1}{G} + Creature — Bear 2/2 + */ + String bear = "Balduvian Bears"; + + /** + * test with three players: + * A with a Jolene and an Elite Vanguard + * B, C other players. + * + * A attacks both B & C, get two treasures. + */ + @Test + public void testAttackingTwoOpponents() { + addCard(Zone.BATTLEFIELD, playerA, jolene, 1); + addCard(Zone.BATTLEFIELD, playerA, vanguard, 1); + + attack(1, playerA, jolene, playerB); + attack(1, playerA, vanguard, playerC); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + assertAllCommandsUsed(); + + // 1 attack trigger, +1 Treasure with the replacement effect. + assertPermanentCount(playerA, "Treasure Token", 2); + } + + /** + * test with three players: + * A with an Elite Vanguard and a Balduvian Bears + * B with a Jolene + * C other player. + * + * A attacks both B & C, get one treasure. + */ + @Test + public void testAttackingJoleneAndAnotherOpponent() { + addCard(Zone.BATTLEFIELD, playerA, vanguard, 1); + addCard(Zone.BATTLEFIELD, playerA, bear, 1); + addCard(Zone.BATTLEFIELD, playerB, jolene, 1); + + attack(1, playerA, bear, playerB); + attack(1, playerA, vanguard, playerC); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + assertAllCommandsUsed(); + + // 1 attack trigger, not controlling Jolene so no replacement effect. + assertPermanentCount(playerA, "Treasure Token", 1); + } + + /** + * test with three players: + * A with an Elite Vanguard + * B with a Jolene + * C other player. + * + * A attacks only B, no trigger, no treasure. + */ + @Test + public void testAttackingJoleneOnly() { + addCard(Zone.BATTLEFIELD, playerA, vanguard, 1); + addCard(Zone.BATTLEFIELD, playerB, jolene, 1); + + attack(1, playerA, vanguard, playerB); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + assertAllCommandsUsed(); + + // no trigger, no token. + assertPermanentCount(playerA, "Treasure Token", 0); + } + + /** + * test with three players: + * A with a Jolene and an Elite Vanguard + * B with a Jolene. + * C with a Jolene. + * + * A attacks both B & C, 3 triggers, 6 treasures. + */ + @Test + public void testEveryoneGotAJolene() { + addCard(Zone.BATTLEFIELD, playerA, vanguard, 1); + addCard(Zone.BATTLEFIELD, playerA, jolene, 1); + addCard(Zone.BATTLEFIELD, playerB, jolene, 1); + addCard(Zone.BATTLEFIELD, playerC, jolene, 1); + + attack(1, playerA, vanguard, playerB); + attack(1, playerA, jolene, playerC); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + assertAllCommandsUsed(); + + // 3 triggers (1 for each Jolene), +1 Treasure for each with the replacement effect. + assertPermanentCount(playerA, "Treasure Token", 6); + } +}