From 419030b681f654a849f58d69ad8fc58cc77b3302 Mon Sep 17 00:00:00 2001
From: Grath <1895280+Grath@users.noreply.github.com>
Date: Wed, 26 Feb 2025 23:12:07 -0500
Subject: [PATCH] [NCC] Partially fix Rain of Riches and add tests.
Two tests are failing and ignored because this is only a partial fix, as we will still need to process actions between the last mana being paid and the spell being cast.
---
Mage.Sets/src/mage/cards/r/RainOfRiches.java | 95 +++++---------
.../cards/single/ncc/RainOfRichesTest.java | 118 ++++++++++++++++++
.../GainAbilityControlledSpellsEffect.java | 12 +-
3 files changed, 155 insertions(+), 70 deletions(-)
create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/RainOfRichesTest.java
diff --git a/Mage.Sets/src/mage/cards/r/RainOfRiches.java b/Mage.Sets/src/mage/cards/r/RainOfRiches.java
index b15dcdaee2f..c87c6ea05d3 100644
--- a/Mage.Sets/src/mage/cards/r/RainOfRiches.java
+++ b/Mage.Sets/src/mage/cards/r/RainOfRiches.java
@@ -1,22 +1,24 @@
package mage.cards.r;
import mage.MageObjectReference;
-import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
-import mage.abilities.condition.Condition;
-import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.common.CreateTokenEffect;
+import mage.abilities.effects.common.continuous.GainAbilityControlledSpellsEffect;
import mage.abilities.keyword.CascadeAbility;
+import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
+import mage.filter.common.FilterNonlandCard;
+import mage.filter.predicate.ObjectSourcePlayer;
+import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
+import mage.game.permanent.Permanent;
import mage.game.permanent.token.TreasureToken;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;
-import mage.players.Player;
import mage.watchers.Watcher;
import mage.watchers.common.ManaPaidSourceWatcher;
@@ -25,10 +27,18 @@ import java.util.Map;
import java.util.UUID;
/**
- * @author Alex-Vasile
+ * @author Alex-Vasile, Susucr
*/
public class RainOfRiches extends CardImpl {
+
+ private static final FilterNonlandCard filter =
+ new FilterNonlandCard("The first spell you cast each turn that mana from a Treasure was spent to cast");
+
+ static {
+ filter.add(RainOfRichesPredicate.instance);
+ }
+
public RainOfRiches(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{R}{R}");
@@ -40,7 +50,7 @@ public class RainOfRiches extends CardImpl {
// You may cast it without paying its mana cost.
// Put the exiled cards on the bottom of your library in a random order.)
this.addAbility(
- new SimpleStaticAbility(new RainOfRichesGainsCascadeEffect()),
+ new SimpleStaticAbility(new GainAbilityControlledSpellsEffect(new CascadeAbility(false), filter)),
new RainOfRichesWatcher()
);
}
@@ -55,65 +65,20 @@ public class RainOfRiches extends CardImpl {
}
}
-class RainOfRichesGainsCascadeEffect extends ContinuousEffectImpl {
-
- private final Ability cascadeAbility = new CascadeAbility();
-
- RainOfRichesGainsCascadeEffect() {
- super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
- this.staticText =
- "The first spell you cast each turn that mana from a Treasure was spent to cast has cascade. " +
- "(When you cast the spell, exile cards from the top of your library until you exile a nonland card that costs less. " +
- "You may cast it without paying its mana cost. " +
- "Put the exiled cards on the bottom of your library in a random order.)";
- }
-
- private RainOfRichesGainsCascadeEffect(final RainOfRichesGainsCascadeEffect effect) {
- super(effect);
- }
-
- @Override
- public boolean apply(Game game, Ability source) {
- Player controller = game.getPlayer(source.getControllerId());
- RainOfRichesWatcher watcher = game.getState().getWatcher(RainOfRichesWatcher.class);
- if (controller == null || watcher == null) {
- return false;
- }
-
- for (StackObject stackObject : game.getStack()) {
- // Only spells cast, so no copies of spells
- if ((stackObject instanceof Spell)
- && !stackObject.isCopy()
- && stackObject.isControlledBy(source.getControllerId())) {
- Spell spell = (Spell) stackObject;
-
- if (FirstSpellCastWithTreasureCondition.instance.apply(game, source)) {
- game.getState().addOtherAbility(spell.getCard(), cascadeAbility);
- return true; // TODO: I think this should return here as soon as it finds the first one.
- // If it should, change WildMageSorcerer to also return early.
- }
- }
- }
- return false;
- }
-
- @Override
- public RainOfRichesGainsCascadeEffect copy() {
- return new RainOfRichesGainsCascadeEffect(this);
- }
-}
-
-enum FirstSpellCastWithTreasureCondition implements Condition {
+enum RainOfRichesPredicate implements ObjectSourcePlayerPredicate {
instance;
@Override
- public boolean apply(Game game, Ability source) {
- if (game.getStack().isEmpty()) {
+ public boolean apply(ObjectSourcePlayer input, Game game) {
+ Permanent sourcePermanent = input.getSource().getSourcePermanentOrLKI(game);
+ if (sourcePermanent == null || !sourcePermanent.getControllerId().equals(input.getPlayerId())) {
return false;
}
RainOfRichesWatcher watcher = game.getState().getWatcher(RainOfRichesWatcher.class);
- StackObject so = game.getStack().getFirst();
- return watcher != null && RainOfRichesWatcher.checkSpell(so, game);
+ Card card = input.getObject();
+ return watcher != null
+ && card instanceof StackObject
+ && watcher.checkSpell((Spell) card, game);
}
}
@@ -127,7 +92,7 @@ class RainOfRichesWatcher extends Watcher {
@Override
public void watch(GameEvent event, Game game) {
- if (event.getType() != GameEvent.EventType.CAST_SPELL) {
+ if (event.getType() != GameEvent.EventType.SPELL_CAST) {
return;
}
Spell spell = game.getSpell(event.getSourceId());
@@ -148,13 +113,15 @@ class RainOfRichesWatcher extends Watcher {
super.reset();
}
- static boolean checkSpell(StackObject stackObject, Game game) {
+ boolean checkSpell(StackObject stackObject, Game game) {
if (stackObject.isCopy()
|| !(stackObject instanceof Spell)) {
return false;
}
- RainOfRichesWatcher watcher = game.getState().getWatcher(RainOfRichesWatcher.class);
- return watcher.playerMap.containsKey(stackObject.getControllerId())
- && watcher.playerMap.get(stackObject.getControllerId()).refersTo(((Spell) stackObject).getMainCard(), game);
+ if (playerMap.containsKey(stackObject.getControllerId())) {
+ return playerMap.get(stackObject.getControllerId()).refersTo(((Spell) stackObject).getMainCard(), game);
+ } else {
+ return ManaPaidSourceWatcher.getTreasurePaid(stackObject.getId(), game) >= 1;
+ }
}
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/RainOfRichesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/RainOfRichesTest.java
new file mode 100644
index 00000000000..2a65b1976db
--- /dev/null
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/RainOfRichesTest.java
@@ -0,0 +1,118 @@
+package org.mage.test.cards.single.ncc;
+
+import mage.constants.PhaseStep;
+import mage.constants.Zone;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mage.test.serverside.base.CardTestPlayerBase;
+
+/**
+ * @author Susucr
+ */
+public class RainOfRichesTest extends CardTestPlayerBase {
+
+ /**
+ * {@link mage.cards.r.RainOfRiches Rain of Riches} {3}{R}{R}
+ * Enchantment
+ * When Rain of Riches enters the battlefield, create two Treasure tokens.
+ * The first spell you cast each turn that mana from a Treasure was spent to cast has cascade.
+ */
+ private static final String rain = "Rain of Riches";
+
+ @Test
+ public void test_Using_Treasures() {
+ setStrictChooseMode(true);
+
+ addCard(Zone.HAND, playerA, rain, 1);
+ addCard(Zone.HAND, playerA, "Goblin Piker", 1);
+ addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
+ addCard(Zone.LIBRARY, playerA, "Elite Vanguard", 1);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rain, true);
+
+ castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Goblin Piker");
+ setChoice(playerA, "Red"); // choice for treasure mana
+ setChoice(playerA, "Red"); // choice for treasure mana
+ setChoice(playerA, true); // yes to Cascade
+
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertPermanentCount(playerA, "Goblin Piker", 1);
+ assertPermanentCount(playerA, "Elite Vanguard", 1);
+ }
+
+ @Test
+ public void test_Not_Using_Treasures() {
+ setStrictChooseMode(true);
+
+ addCard(Zone.HAND, playerA, rain, 1);
+ addCard(Zone.HAND, playerA, "Goblin Piker", 1);
+ addCard(Zone.BATTLEFIELD, playerA, "Mountain", 7);
+ addCard(Zone.LIBRARY, playerA, "Elite Vanguard", 1);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rain, true);
+
+ castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Goblin Piker");
+
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertPermanentCount(playerA, "Goblin Piker", 1);
+ assertPermanentCount(playerA, "Elite Vanguard", 0);
+ }
+
+ @Test
+ @Ignore("Does not work until actions are processed between the last mana being paid and the spell being cast.")
+ public void test_Cast_Two_Using_Treasures() {
+ setStrictChooseMode(true);
+ skipInitShuffling();
+
+ addCard(Zone.HAND, playerA, rain, 1);
+ addCard(Zone.HAND, playerA, "Raging Goblin", 2); // {R}
+ addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
+ addCard(Zone.LIBRARY, playerA, "Memnite", 2);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rain, true);
+
+ castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Raging Goblin");
+ setChoice(playerA, "Red"); // choice for treasure mana
+ setChoice(playerA, true); // yes to Cascade
+ waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN);
+
+ castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Raging Goblin");
+ setChoice(playerA, "Red"); // choice for treasure mana
+
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertPermanentCount(playerA, "Raging Goblin", 2);
+ assertPermanentCount(playerA, "Memnite", 1);
+ }
+
+ @Test
+ @Ignore("Does not work until actions are processed between the last mana being paid and the spell being cast.")
+ public void test_Cast_SomethingElse_Then_Cast_Using_Treasure() {
+ setStrictChooseMode(true);
+
+ addCard(Zone.HAND, playerA, rain, 1);
+ addCard(Zone.HAND, playerA, "Raging Goblin", 2); // {R}
+ addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
+ addCard(Zone.LIBRARY, playerA, "Memnite", 2);
+
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rain, true);
+
+ activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Add");
+ castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Raging Goblin", true);
+
+ castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Raging Goblin");
+ setChoice(playerA, "Red"); // choice for treasure mana
+ setChoice(playerA, true); // yes to Cascade
+
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+
+ assertPermanentCount(playerA, "Raging Goblin", 2);
+ assertPermanentCount(playerA, "Memnite", 1);
+ }
+}
\ No newline at end of file
diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java
index d2243674730..7cef0dc05df 100644
--- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java
@@ -45,22 +45,22 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl {
}
for (Card card : game.getExile().getAllCardsByRange(game, source.getControllerId())) {
- if (filter.match(card, game)) {
+ if (filter.match(card, player.getId(), source, game)) {
game.getState().addOtherAbility(card, ability);
}
}
for (Card card : player.getLibrary().getCards(game)) {
- if (filter.match(card, game)) {
+ if (filter.match(card, player.getId(), source, game)) {
game.getState().addOtherAbility(card, ability);
}
}
for (Card card : player.getHand().getCards(game)) {
- if (filter.match(card, game)) {
+ if (filter.match(card, player.getId(), source, game)) {
game.getState().addOtherAbility(card, ability);
}
}
for (Card card : player.getGraveyard().getCards(game)) {
- if (filter.match(card, game)) {
+ if (filter.match(card, player.getId(), source, game)) {
game.getState().addOtherAbility(card, ability);
}
}
@@ -68,7 +68,7 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl {
// workaround to gain cost reduction abilities to commanders before cast (make it playable)
game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY)
.stream()
- .filter(card -> filter.match(card, game))
+ .filter(card -> filter.match(card, player.getId(), source, game))
.forEach(card -> game.getState().addOtherAbility(card, ability));
for (StackObject stackObject : game.getStack()) {
@@ -77,7 +77,7 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl {
}
// TODO: Distinguish "you cast" to exclude copies
Card card = game.getCard(stackObject.getSourceId());
- if (card != null && filter.match((Spell) stackObject, game)) {
+ if (card != null && filter.match((Spell) stackObject, player.getId(), source, game)) {
game.getState().addOtherAbility(card, ability);
}
}