From 0612431ab28e3396cff451fd963b19ff0c281cbf Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 8 Feb 2025 02:00:43 +0400 Subject: [PATCH] [PIP] Implement Struggle for Project Purity (part of #11324) --- .../cards/s/StruggleForProjectPurity.java | 156 ++++++++++++++++++ Mage.Sets/src/mage/sets/Fallout.java | 8 +- .../pip/StruggleForProjectPurityTest.java | 127 ++++++++++++++ 3 files changed, 287 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/pip/StruggleForProjectPurityTest.java diff --git a/Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java b/Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java new file mode 100644 index 00000000000..1599ab4bb2c --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java @@ -0,0 +1,156 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.condition.common.ModeChoiceSourceCondition; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.ControllerIdPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; + +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author JayDi85 + */ +public final class StruggleForProjectPurity extends CardImpl { + + private static final String ruleTrigger1 = "&bull Brotherhood — At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way."; + private static final String ruleTrigger2 = "&bull Enclave — Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters."; + + public StruggleForProjectPurity(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{U}"); + + // As Struggle for Project Purity enters, choose Brotherhood or Enclave. + this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Brotherhood or Enclave?", "Brotherhood", "Enclave"), null, + "As {this} enters, choose Brotherhood or Enclave.", "")); + + // * Brotherhood - At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way. + Ability ability = new ConditionalTriggeredAbility( + new BeginningOfUpkeepTriggeredAbility(new StruggleForProjectDrawEffect()), + new ModeChoiceSourceCondition("Brotherhood"), + ruleTrigger1); + this.addAbility(ability); + + // * Enclave - Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters. + ability = new ConditionalTriggeredAbility( + new StruggleForProjectRadCountersTriggeredAbility(), + new ModeChoiceSourceCondition("Enclave"), + ruleTrigger2); + this.addAbility(ability); + } + + private StruggleForProjectPurity(final StruggleForProjectPurity card) { + super(card); + } + + @Override + public StruggleForProjectPurity copy() { + return new StruggleForProjectPurity(this); + } +} + +class StruggleForProjectDrawEffect extends OneShotEffect { + + StruggleForProjectDrawEffect() { + super(Outcome.DrawCard); + this.staticText = "Each opponent draws a card. You draw a card for each card drawn this way."; + } + + private StruggleForProjectDrawEffect(final StruggleForProjectDrawEffect effect) { + super(effect); + } + + @Override + public StruggleForProjectDrawEffect copy() { + return new StruggleForProjectDrawEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + int count = game + .getOpponents(source.getControllerId(), true) + .stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .mapToInt(player -> player.drawCards(1, source, game)) + .sum(); + if (count > 0) { + controller.drawCards(count, source, game); + return true; + } + return false; + } +} + +class StruggleForProjectRadCountersTriggeredAbility extends TriggeredAbilityImpl { + + public StruggleForProjectRadCountersTriggeredAbility() { + super(Zone.BATTLEFIELD, null); + setTriggerPhrase("Whenever a player attacks you with one or more creatures, "); + } + + private StruggleForProjectRadCountersTriggeredAbility(final StruggleForProjectRadCountersTriggeredAbility ability) { + super(ability); + } + + @Override + public StruggleForProjectRadCountersTriggeredAbility copy() { + return new StruggleForProjectRadCountersTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DECLARED_ATTACKERS; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Player attackingPlayer = game.getPlayer(event.getPlayerId()); + if (attackingPlayer == null) { + return false; + } + + Set attackersOnYou = game.getCombat().getGroups().stream() + .filter(g -> Objects.equals(g.getDefenderId(), getControllerId())) + .flatMap(g -> g.getAttackers().stream()) + .collect(Collectors.toSet()); + if (attackersOnYou.isEmpty()) { + return false; + } + + this.getEffects().clear(); + FilterCreaturePermanent filter = new FilterCreaturePermanent(); + filter.add(new ControllerIdPredicate(event.getPlayerId())); + Effect effect = new AddCountersTargetEffect( + CounterType.RAD.createInstance(), + StaticValue.get(attackersOnYou.size() * 2) + ); + effect.setTargetPointer(new FixedTarget(attackingPlayer.getId())); + this.getEffects().add(effect); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/Fallout.java b/Mage.Sets/src/mage/sets/Fallout.java index ac626b3e735..8a977e570c8 100644 --- a/Mage.Sets/src/mage/sets/Fallout.java +++ b/Mage.Sets/src/mage/sets/Fallout.java @@ -839,10 +839,10 @@ public final class Fallout extends ExpansionSet { //cards.add(new SetCardInfo("Strong, the Brutish Thespian", 612, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Strong, the Brutish Thespian", 84, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Strong, the Brutish Thespian", 931, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Struggle for Project Purity", 380, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Struggle for Project Purity", 39, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Struggle for Project Purity", 567, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Struggle for Project Purity", 908, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Struggle for Project Purity", 380, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Struggle for Project Purity", 39, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Struggle for Project Purity", 567, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Struggle for Project Purity", 908, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sulfur Falls", 1040, Rarity.RARE, mage.cards.s.SulfurFalls.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sulfur Falls", 294, Rarity.RARE, mage.cards.s.SulfurFalls.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sulfur Falls", 512, Rarity.RARE, mage.cards.s.SulfurFalls.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/StruggleForProjectPurityTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/StruggleForProjectPurityTest.java new file mode 100644 index 00000000000..115fcb99474 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/StruggleForProjectPurityTest.java @@ -0,0 +1,127 @@ +package org.mage.test.cards.single.pip; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +/** + * @author JayDi85 + */ +public class StruggleForProjectPurityTest extends CardTestCommander4Players { + + /** + * {@link mage.cards.s.StruggleForProjectPurity Struggle for Project Purity} + * {3}{U} + * Enchantment + * As Struggle for Project Purity enters, choose Brotherhood or Enclave. + * • Brotherhood — At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way. + * • Enclave — Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters. + */ + private static final String struggle = "Struggle for Project Purity"; + + private void checkRadCounters(String info, int needA, int needB, int needC, int needD) { + Assert.assertEquals(info + ", rad counter on playerA", needA, playerA.getCountersCount(CounterType.RAD)); + Assert.assertEquals(info + ", rad counter on playerB", needB, playerB.getCountersCount(CounterType.RAD)); + Assert.assertEquals(info + ", rad counter on playerC", needC, playerC.getCountersCount(CounterType.RAD)); + Assert.assertEquals(info + ", rad counter on playerD", needD, playerD.getCountersCount(CounterType.RAD)); + } + + @Test + public void test_Brotherhood() { + // Player order: A -> D -> C -> B + + // Brotherhood — At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way. + addCard(Zone.HAND, playerA, struggle); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + checkHandCount("before cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // struggle + starting draw + + // turn 1 - A - prepare brotherhood, no triggers + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, struggle); + setChoice(playerA, "Brotherhood"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // turn 2 - D - no triggers + checkHandCount("no draws on turn 2", 2, PhaseStep.PRECOMBAT_MAIN, playerA, 1); + + // turn 3 - C - no triggers + checkHandCount("no draws on turn 3", 3, PhaseStep.PRECOMBAT_MAIN, playerA, 1); + + // turn 4 - B - no triggers + checkHandCount("no draws on turn 4", 4, PhaseStep.PRECOMBAT_MAIN, playerA, 1); + + // turn 5 - A - trigger + // opponent draw: +1 + // you draw: +3 + checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerA, 1 + 1 + 3); // draw 1 + draw 5 + draw trigger + checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerB, 2); // opponent turn + trigger + checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerC, 2); // opponent turn + trigger + checkHandCount("draws trigger", 5, PhaseStep.PRECOMBAT_MAIN, playerD, 2); // opponent turn + trigger + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, struggle, 1); + } + + @Test + public void test_Enclave() { + // Player order: A -> D -> C -> B + + // Enclave — Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters. + addCard(Zone.HAND, playerA, struggle); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + // + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 2); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 2); + addCard(Zone.BATTLEFIELD, playerC, "Grizzly Bears", 2); + addCard(Zone.BATTLEFIELD, playerD, "Grizzly Bears", 2); + + checkHandCount("before cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // struggle + starting draw + runCode("rad count playerA turn 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 0)); + + // turn 1 + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, struggle); + setChoice(playerA, "Enclave"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // turn 1 + attack(1, playerA, "Grizzly Bears", playerD); + attack(1, playerA, "Grizzly Bears", playerD); + runCode("A attacked D on turn 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 0)); + + // turn 2 + attack(2, playerD, "Grizzly Bears", playerA); // <<< trigger for D + attack(2, playerD, "Grizzly Bears", playerA); + runCode("D attacked A on turn 2", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 2 * 2)); + + // turn 3 + attack(3, playerC, "Grizzly Bears", playerB); + attack(3, playerC, "Grizzly Bears", playerB); + runCode("B attacked B on turn 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 0, 0, 2 * 2)); + + // turn 4 + attack(4, playerB, "Grizzly Bears", playerA); // <<< trigger for B + attack(4, playerB, "Grizzly Bears", playerA); + runCode("B attacked A on turn 4", 4, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 2 * 2, 0, 2 * 2)); + + // turn 5 + attack(5, playerA, "Grizzly Bears", playerD); + attack(5, playerA, "Grizzly Bears", playerD); + runCode("A attacked D on turn 5", 5, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> checkRadCounters(info, 0, 2 * 2, 0, 2 * 2)); + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, struggle, 1); + assertLife(playerA, 20 - 2 * 2 - 2 * 2); // from D and B + assertLife(playerB, 20 - 2 * 2); // from C + assertLife(playerC, 20); // no attackers + assertLife(playerD, 20 - 2 * 2 - 2 * 2); // from A and A + } +}