From c36b3959a8c6112888fb8bf539837e27b3468366 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:57:16 +0200 Subject: [PATCH] implement and test ExpendTriggeredAbility --- .../src/mage/cards/b/BrambleguardVeteran.java | 9 +- .../src/mage/cards/j/JunkbladeBruiser.java | 5 +- .../src/mage/cards/w/WandertaleMentor.java | 3 +- Mage.Sets/src/mage/sets/Bloomburrow.java | 2 +- .../mage/test/cards/triggers/ExpendTest.java | 257 ++++++++++++++++++ .../common/ExpendTriggeredAbility.java | 150 +++++++++- 6 files changed, 411 insertions(+), 15 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/triggers/ExpendTest.java diff --git a/Mage.Sets/src/mage/cards/b/BrambleguardVeteran.java b/Mage.Sets/src/mage/cards/b/BrambleguardVeteran.java index 38bbce9bcab..eddb2eb38e1 100644 --- a/Mage.Sets/src/mage/cards/b/BrambleguardVeteran.java +++ b/Mage.Sets/src/mage/cards/b/BrambleguardVeteran.java @@ -33,9 +33,12 @@ public final class BrambleguardVeteran extends CardImpl { this.toughness = new MageInt(4); // Whenever you expend 4, Raccoons you control get +1/+1 and gain vigilance until end of turn. - Ability ability = new ExpendTriggeredAbility(new BoostControlledEffect( - 1, 1, Duration.EndOfTurn, filter - ).setText("Raccoons you control get +1/+1"), 4); + Ability ability = new ExpendTriggeredAbility( + new BoostControlledEffect( + 1, 1, Duration.EndOfTurn, filter + ).setText("Raccoons you control get +1/+1"), + ExpendTriggeredAbility.Expend.FOUR + ); ability.addEffect(new GainAbilityControlledEffect( VigilanceAbility.getInstance(), Duration.EndOfTurn, filter2 ).setText("and gain vigilance until end of turn")); diff --git a/Mage.Sets/src/mage/cards/j/JunkbladeBruiser.java b/Mage.Sets/src/mage/cards/j/JunkbladeBruiser.java index e868b3d4eb3..f8ec1663be4 100644 --- a/Mage.Sets/src/mage/cards/j/JunkbladeBruiser.java +++ b/Mage.Sets/src/mage/cards/j/JunkbladeBruiser.java @@ -29,7 +29,10 @@ public final class JunkbladeBruiser extends CardImpl { this.addAbility(TrampleAbility.getInstance()); // Whenever you expend 4, Junkblade Bruiser gets +2/+1 until end of turn. - this.addAbility(new ExpendTriggeredAbility(new BoostSourceEffect(2, 1, Duration.EndOfTurn), 4)); + this.addAbility(new ExpendTriggeredAbility( + new BoostSourceEffect(2, 1, Duration.EndOfTurn), + ExpendTriggeredAbility.Expend.FOUR + )); } private JunkbladeBruiser(final JunkbladeBruiser card) { diff --git a/Mage.Sets/src/mage/cards/w/WandertaleMentor.java b/Mage.Sets/src/mage/cards/w/WandertaleMentor.java index a990a8a4f33..6bc73a036d7 100644 --- a/Mage.Sets/src/mage/cards/w/WandertaleMentor.java +++ b/Mage.Sets/src/mage/cards/w/WandertaleMentor.java @@ -28,7 +28,8 @@ public final class WandertaleMentor extends CardImpl { // Whenever you expend 4, put a +1/+1 counter on Wandertale Mentor. this.addAbility(new ExpendTriggeredAbility( - new AddCountersSourceEffect(CounterType.P1P1.createInstance()), 4 + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), + ExpendTriggeredAbility.Expend.FOUR )); // {T}: Add {R} or {G}. diff --git a/Mage.Sets/src/mage/sets/Bloomburrow.java b/Mage.Sets/src/mage/sets/Bloomburrow.java index 57c6cf23f75..8c89fc968a2 100644 --- a/Mage.Sets/src/mage/sets/Bloomburrow.java +++ b/Mage.Sets/src/mage/sets/Bloomburrow.java @@ -12,7 +12,7 @@ import java.util.List; */ public final class Bloomburrow extends ExpansionSet { - private static final List unfinished = Arrays.asList("Brambleguard Veteran", "Flowerfoot Swordmaster", "Iridescent Vinelasher", "Junkblade Bruiser", "Manifold Mouse", "Muerra, Trash Tactician", "Tender Wildguide", "Thundertrap Trainer", "Wandertale Mentor", "Warren Warleader"); + private static final List unfinished = Arrays.asList("Flowerfoot Swordmaster", "Iridescent Vinelasher", "Manifold Mouse", "Tender Wildguide", "Thundertrap Trainer", "Warren Warleader"); private static final Bloomburrow instance = new Bloomburrow(); public static Bloomburrow getInstance() { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/ExpendTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/ExpendTest.java new file mode 100644 index 00000000000..7c0d418ea37 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/ExpendTest.java @@ -0,0 +1,257 @@ +package org.mage.test.cards.triggers; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class ExpendTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.s.Six Wandertale Mentor} {R}{G} + * Creature — Raccoon Bard + * Whenever you expend 4, put a +1/+1 counter on Wandertale Mentor. (You expend 4 as you spend your fourth total mana to cast spells during a turn.) + * {T}: Add {R} or {G}. + * 2/2 + */ + private static final String mentor = "Wandertale Mentor"; + + @Test + public void test_OnPayingSingleSpell_4Mana_Trigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, "Axebane Beast"); // {3}{G} vanilla + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Axebane Beast"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 1); + } + + @Test + public void test_OnPayingSingleSpell_5Mana_Trigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, "Blanchwood Treefolk"); // {4}{G} vanilla + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Blanchwood Treefolk"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 1); + } + + @Test + public void test_OnPayingSingleSpell_3Mana_NoTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, "Alpine Grizzly"); // {2}{G} vanilla + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alpine Grizzly"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 0); + } + + @Test + public void test_CaresAboutYouSpendingMana() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerB, "Island", 4); + addCard(Zone.HAND, playerB, "Cloaked Siren"); // {3}{U} flash + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Cloaked Siren"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 0); + } + + @Test + public void test_RememberManaSpentBefore_Trigger() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Taiga", 4); + addCard(Zone.HAND, playerA, "Grizzly Bears"); // {1}{G} vanilla + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mentor, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 1); + } + + @Test + public void test_RememberManaSpentBefore_NoTrigger() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Taiga", 10); + addCard(Zone.HAND, playerA, "Axebane Beast", 2); // {3}{G} vanilla + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Axebane Beast", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mentor, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Axebane Beast"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 0); + } + + @Test + public void test_ActivatedAbility_NoTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Devkarin Dissident"); // {4}{G}: Devkarin Dissident gets +2/+2 until end of turn. + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{G}: "); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 0); + } + + @Test + public void test_JustTappingForMana_NoTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 4); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 0); + } + + @Test + public void test_CastAdventure_Trigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 7); + addCard(Zone.HAND, playerA, "Flaxen Intruder"); // Adventure for {5}{G}{G}: Create three 2/2 green Bear creature tokens + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Welcome Home"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 1); + assertPermanentCount(playerA, "Bear Token", 3); + } + + @Test + public void test_Kick_Trigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.HAND, playerA, "Gnarlid Colony"); // {1}{G} Kicker {2}{G} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gnarlid Colony"); + setChoice(playerA, true); // yes for Kicker {2}{G} + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 1); + assertCounterCount(playerA, "Gnarlid Colony", CounterType.P1P1, 2); + } + + @Test + public void test_Cycling_NoTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + addCard(Zone.HAND, playerA, "Stir the Sands"); // Cycling {3}{B} + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cycling {3}{B}"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 0); + assertPermanentCount(playerA, "Zombie Token", 1); + } + + @Test + public void test_XSpell_4_Trigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, "Endless One"); // {X} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Endless One"); + setChoice(playerA, "X=4"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 1); + assertCounterCount(playerA, "Endless One", CounterType.P1P1, 4); + } + + @Test + public void test_XSpell_3_NoTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.HAND, playerA, "Endless One"); // {X} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Endless One"); + setChoice(playerA, "X=3"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 0); + assertCounterCount(playerA, "Endless One", CounterType.P1P1, 3); + } + + @Test + public void test_OneTriggerEachTurn() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, mentor); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, "Axebane Beast", 2); // {3}{G} vanilla + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Axebane Beast"); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Axebane Beast"); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mentor, CounterType.P1P1, 2); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/ExpendTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/ExpendTriggeredAbility.java index 53b0ef6367e..da2306ef69d 100644 --- a/Mage/src/main/java/mage/abilities/common/ExpendTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/ExpendTriggeredAbility.java @@ -1,31 +1,65 @@ package mage.abilities.common; +import mage.MageObject; +import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.constants.WatcherScope; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; +import mage.game.stack.Spell; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; /** - * @author TheElk801 - * TODO: Implement this + * @author Susucr */ public class ExpendTriggeredAbility extends TriggeredAbilityImpl { - private final int amount; + private static final Hint hint = new ValueHint("Mana you expended this turn", ExpendValue.instance); - public ExpendTriggeredAbility(Effect effect, int amount) { + /** + * For memory-usage purpose, we only support expend 4 and expend 8 so far. + */ + public enum Expend { + FOUR(4), + EIGHT(8); + + private final int amount; + + public int getAmount() { + return amount; + } + + Expend(int amount) { + this.amount = amount; + } + } + + private final Expend amount; + + public ExpendTriggeredAbility(Effect effect, Expend amount) { this(effect, amount, false); } - public ExpendTriggeredAbility(Effect effect, int amount, boolean optional) { + public ExpendTriggeredAbility(Effect effect, Expend amount, boolean optional) { this(Zone.BATTLEFIELD, effect, amount, optional); } - public ExpendTriggeredAbility(Zone zone, Effect effect, int amount, boolean optional) { + public ExpendTriggeredAbility(Zone zone, Effect effect, Expend amount, boolean optional) { super(zone, effect, optional); this.amount = amount; - setTriggerPhrase("Whenever you expend " + amount + ", "); + setTriggerPhrase("Whenever you expend " + amount.getAmount() + ", "); + this.addWatcher(new ExpendWatcher()); + this.addHint(hint); } private ExpendTriggeredAbility(final ExpendTriggeredAbility ability) { @@ -40,11 +74,109 @@ public class ExpendTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return false; + return event.getType() == GameEvent.EventType.MANA_PAID; } @Override public boolean checkTrigger(GameEvent event, Game game) { - return false; + return ExpendWatcher.checkExpend(getControllerId(), event, amount, game); + } +} + +enum ExpendValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return ExpendWatcher.getManaExpended(sourceAbility.getControllerId(), game); + } + + @Override + public ExpendValue copy() { + return instance; + } + + @Override + public String getMessage() { + return "mana you expended this turn"; + } +} + + +/** + * Watcher for mana expended this turn. + *

+ * Of note, to reduce memory usage, only tracks the events that did expend 4 and expend 8. + * If expend N extends to more than a couple values, storing the full list (in order) of event id is advised. + */ +class ExpendWatcher extends Watcher { + + // Player id -> number of mana expended this turn + private final Map manaExpended = new HashMap<>(); + + // Player id -> event id for the 4th mana expended this turn + private final Map eventFor4thExpend = new HashMap<>(); + // Player id -> event id for the 8th mana expended this turn + private final Map eventFor8thExpend = new HashMap<>(); + + public ExpendWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (!GameEvent.EventType.MANA_PAID.equals(event.getType())) { + return; + } + MageObject ability = game.getObject(event.getTargetId()); + if (!(ability instanceof Spell)) { + // Only cares about mana paid for casting Spells + return; + } + UUID playerId = event.getPlayerId(); + UUID eventId = event.getId(); + manaExpended.compute(playerId, CardUtil::setOrIncrementValue); + int currentValue = manaExpended.get(playerId); + switch (currentValue) { + case 4: + eventFor4thExpend.put(playerId, eventId); + break; + case 8: + eventFor8thExpend.put(playerId, eventId); + break; + } + } + + @Override + public void reset() { + super.reset(); + manaExpended.clear(); + eventFor4thExpend.clear(); + eventFor8thExpend.clear(); + } + + public static boolean checkExpend(UUID playerId, GameEvent event, ExpendTriggeredAbility.Expend expendToCheck, Game game) { + ExpendWatcher watcher = game.getState().getWatcher(ExpendWatcher.class); + if (watcher == null) { + return false; + } + switch (expendToCheck) { + case FOUR: + return watcher.eventFor4thExpend.containsKey(playerId) + && watcher.eventFor4thExpend.get(playerId).equals(event.getId()); + case EIGHT: + return watcher.eventFor8thExpend.containsKey(playerId) + && watcher.eventFor8thExpend.get(playerId).equals(event.getId()); + default: + throw new IllegalArgumentException("Unsupported expend value: " + expendToCheck.getAmount()); + } + } + + public static int getManaExpended(UUID playerId, Game game) { + ExpendWatcher watcher = game.getState().getWatcher(ExpendWatcher.class); + if (watcher == null) { + return 0; + } + return watcher.manaExpended.getOrDefault(playerId, 0); } }