[LTR] Implement Radagast the Brown; also resolves #9419 (#10568)

* Fix #9419 bug with EntersBattlefieldThisOrAnotherTriggeredAbility
* Add test for Radagast as well as Risen Reef and Caldaia Guardian
* Minor associated cleanup for EntersBattlefieldAllTriggeredAbility
* New PermanentEnteringBattlefieldManaValue
This commit is contained in:
Bobby McCann 2023-07-10 03:04:45 +01:00 committed by GitHub
parent 39904d0ca3
commit 73104f6705
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 344 additions and 46 deletions

View file

@ -0,0 +1,27 @@
package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.game.Game;
import mage.game.permanent.Permanent;
public enum PermanentEnteringBattlefieldManaValue implements DynamicValue {
instance;
@Override
public int calculate(Game game, Ability source, Effect effect) {
Permanent permanent = (Permanent) effect.getValue("permanentEnteringBattlefield");
return permanent == null ? 0 : permanent.getManaValue();
}
@Override
public DynamicValue copy() {
return instance;
}
@Override
public String getMessage() {
return "that creature's mana value";
}
}

View file

@ -0,0 +1,83 @@
package mage.cards.r;
import mage.MageInt;
import mage.abilities.common.EntersBattlefieldThisOrAnotherTriggeredAbility;
import mage.abilities.dynamicvalue.common.PermanentEnteringBattlefieldManaValue;
import mage.abilities.effects.common.LookLibraryAndPickControllerEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.PutCards;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.filter.StaticFilters;
import mage.filter.common.FilterCreatureCard;
import mage.filter.predicate.Predicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import java.util.List;
import java.util.UUID;
/**
*
* @author bobby-mccann
*/
public final class RadagastTheBrown extends CardImpl {
static final FilterCreatureCard cardFilter = new FilterCreatureCard("creature card that doesn't share a creature type with a creature you control");
static {
cardFilter.add(RadagastTheBrownPredicate.instance);
}
public RadagastTheBrown(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}{G}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.AVATAR);
this.subtype.add(SubType.WIZARD);
this.power = new MageInt(2);
this.toughness = new MageInt(5);
// Whenever Radagast the Brown or another nontoken creature enters the battlefield under your control, look at the top X cards of your library, where X is that creature's mana value. You may reveal a creature card from among them that doesn't share a creature type with a creature you control and put it into your hand. Put the rest on the bottom of your library in a random order.
this.addAbility(new EntersBattlefieldThisOrAnotherTriggeredAbility(
new LookLibraryAndPickControllerEffect(
PermanentEnteringBattlefieldManaValue.instance, 1,
cardFilter,
PutCards.HAND, PutCards.BOTTOM_RANDOM
),
StaticFilters.FILTER_CREATURE_NON_TOKEN,
false,
true
));
}
private RadagastTheBrown(final RadagastTheBrown card) {
super(card);
}
@Override
public RadagastTheBrown copy() {
return new RadagastTheBrown(this);
}
}
enum RadagastTheBrownPredicate implements Predicate<Card> {
instance;
public boolean apply(Card card, Game game) {
UUID playerId = card.getOwnerId();
List<Permanent> creaturesYouControl = game.getBattlefield().getActivePermanents(
StaticFilters.FILTER_CONTROLLED_CREATURE,
playerId,
game
);
for (Permanent creature : creaturesYouControl) {
if (creature.shareCreatureTypes(game, card)) {
return false;
}
}
return true;
}
}

View file

@ -203,6 +203,7 @@ public final class TheLordOfTheRingsTalesOfMiddleEarth extends ExpansionSet {
cards.add(new SetCardInfo("Protector of Gondor", 25, Rarity.COMMON, mage.cards.p.ProtectorOfGondor.class));
cards.add(new SetCardInfo("Quarrel's End", 141, Rarity.COMMON, mage.cards.q.QuarrelsEnd.class));
cards.add(new SetCardInfo("Quickbeam, Upstart Ent", 183, Rarity.UNCOMMON, mage.cards.q.QuickbeamUpstartEnt.class));
cards.add(new SetCardInfo("Radagast the Brown", 184, Rarity.MYTHIC, mage.cards.r.RadagastTheBrown.class));
cards.add(new SetCardInfo("Rally at the Hornburg", 142, Rarity.COMMON, mage.cards.r.RallyAtTheHornburg.class));
cards.add(new SetCardInfo("Ranger's Firebrand", 143, Rarity.UNCOMMON, mage.cards.r.RangersFirebrand.class));
cards.add(new SetCardInfo("Rangers of Ithilien", 66, Rarity.RARE, mage.cards.r.RangersOfIthilien.class));

View file

@ -0,0 +1,87 @@
package org.mage.test.cards.single.ltr;
import mage.cards.f.FeldonOfTheThirdPath;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
public class RadagastTheBrownTest extends CardTestPlayerBase {
private static final String radagast = "Radagast the Brown";
@Test
public void libraryTest() {
setStrictChooseMode(true);
skipInitShuffling();
removeAllCardsFromLibrary(playerA);
addCard(Zone.LIBRARY, playerA, "Goblin Guide"); // Goblin - 1 CMC
addCard(Zone.LIBRARY, playerA, "Amoeboid Changeling"); // Changeling - 2 CMC - should share creature types with anything
addCard(Zone.LIBRARY, playerA, "Heliod's Emissary"); // Enchantment Creature - Elk - 4 CMC
addCard(Zone.LIBRARY, playerA, "Overbeing of Myth"); // Spirit Avatar - 5 CMC
addCard(Zone.LIBRARY, playerA, "Boggart Shenanigans"); // Tribal Enchantment - Goblin
addCard(Zone.LIBRARY, playerA, "Stampeding Elk Herd"); // Elk - 5 CMC
addCard(Zone.HAND, playerA, radagast);
addCard(Zone.LIBRARY, playerB, "Swamp", 50);
addCard(Zone.BATTLEFIELD, playerA, "Savannah", 10);
addCard(Zone.BATTLEFIELD, playerB, "Amoeboid Changeling", 10);
// 4 cards revealed - choose from Stampeding Elk Herd and Heliod's Emissary:
setChoice(playerA, "Yes");
addTarget(playerA, "Stampeding Elk Herd");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, radagast);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertHandCount(playerA, 1);
assertHandCount(playerA, "Stampeding Elk Herd", 1);
assertLibraryCount(playerA, 5);
setChoice(playerA, "Yes");
addTarget(playerA, "Goblin Guide");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Stampeding Elk Herd");
setStopAt(3, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertHandCount(playerA, 2);
assertHandCount(playerA, "Goblin Guide", 1);
}
@Test
public void whenItsAToken() {
setStrictChooseMode(true);
skipInitShuffling();
removeAllCardsFromLibrary(playerA);
addCard(Zone.LIBRARY, playerA, "Goblin Guide"); // Goblin - 1 CMC
addCard(Zone.LIBRARY, playerA, "Amoeboid Changeling"); // Changeling - 2 CMC - should share creature types with anything
addCard(Zone.LIBRARY, playerA, "Heliod's Emissary"); // Enchantment Creature - Elk - 4 CMC
addCard(Zone.LIBRARY, playerA, "Overbeing of Myth"); // Spirit Avatar - 5 CMC
addCard(Zone.LIBRARY, playerA, "Boggart Shenanigans"); // Tribal Enchantment - Goblin
addCard(Zone.LIBRARY, playerA, "Stampeding Elk Herd"); // Elk - 5 CMC
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10);
addCard(Zone.GRAVEYARD, playerA, radagast);
addCard(Zone.BATTLEFIELD, playerA, "Feldon of the Third Path");
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA,
"{2}{R}, {T}: Create a token that's a copy of target creature card in your graveyard, except it's an artifact in addition to its other types. It gains haste. Sacrifice it at the beginning of the next end step.",
radagast);
// 4 cards revealed - choose from Stampeding Elk Herd and Heliod's Emissary:
setChoice(playerA, "Yes");
addTarget(playerA, "Heliod's Emissary");
setStopAt(3, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertPermanentCount(playerA, 12);
}
}

View file

@ -0,0 +1,37 @@
package org.mage.test.cards.single.m20;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
public class RisenReefTest extends CardTestPlayerBase {
private static final String risenReef = "Risen Reef";
@Test
public void croakingCounterPartCountsForTrigger() {
setStrictChooseMode(true);
removeAllCardsFromLibrary(playerA);
addCard(Zone.LIBRARY, playerA, "Mountain", 10);
addCard(Zone.BATTLEFIELD, playerA, "Breeding Pool", 10);
addCard(Zone.BATTLEFIELD, playerA, risenReef);
addCard(Zone.HAND, playerA, "Croaking Counterpart");
addCard(Zone.HAND, playerA, "Air Elemental");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Croaking Counterpart");
addTarget(playerA, risenReef);
setChoice(playerA, "Yes");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Air Elemental");
// We get two triggers, so we have to choose which one to put on the stack first (they're identical):
setChoice(playerA, "Whenever");
// Put both lands onto the battlefield:
setChoice(playerA, "Yes", 2);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Mountain", 3);
}
}

View file

@ -0,0 +1,41 @@
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.CardTestPlayerBase;
public class CaldaiaGuardianTest extends CardTestPlayerBase {
private static final String guardian = "Caldaia Guardian";
@Test
public void croakingCounterPartCountsForTrigger() {
setStrictChooseMode(true);
removeAllCardsFromLibrary(playerA);
addCard(Zone.LIBRARY, playerA, "Mountain", 10);
addCard(Zone.BATTLEFIELD, playerA, "Breeding Pool", 10);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 10);
addCard(Zone.BATTLEFIELD, playerA, guardian);
addCard(Zone.HAND, playerA, "Croaking Counterpart");
addCard(Zone.HAND, playerA, "Murder", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Croaking Counterpart");
addTarget(playerA, guardian);
// This kills the 4/3 Guardian, which should cause two triggers:
addTarget(playerA, guardian+"[no copy]");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Murder");
// Choose trigger order:
setChoice(playerA, "Whenever");
// Kill the 1/1, which should also trigger:
addTarget(playerA, guardian);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Murder");
setStopAt(3, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Citizen Token", 6);
}
}

View file

@ -21,7 +21,6 @@ public class EntersBattlefieldAllTriggeredAbility extends TriggeredAbilityImpl {
protected String rule;
protected boolean controlledText;
protected SetTargetPointer setTargetPointer;
protected final boolean thisOrAnother;
/**
* zone = BATTLEFIELD optional = false
@ -54,16 +53,11 @@ public class EntersBattlefieldAllTriggeredAbility extends TriggeredAbilityImpl {
}
public EntersBattlefieldAllTriggeredAbility(Zone zone, Effect effect, FilterPermanent filter, boolean optional, SetTargetPointer setTargetPointer, String rule, boolean controlledText) {
this(zone, effect, filter, optional, setTargetPointer, rule, controlledText, false);
}
protected EntersBattlefieldAllTriggeredAbility(Zone zone, Effect effect, FilterPermanent filter, boolean optional, SetTargetPointer setTargetPointer, String rule, boolean controlledText, boolean thisOrAnother) {
super(zone, effect, optional);
this.filter = filter;
this.rule = rule;
this.controlledText = controlledText;
this.setTargetPointer = setTargetPointer;
this.thisOrAnother = thisOrAnother;
setTriggerPhrase(generateTriggerPhrase());
}
@ -73,7 +67,6 @@ public class EntersBattlefieldAllTriggeredAbility extends TriggeredAbilityImpl {
this.rule = ability.rule;
this.controlledText = ability.controlledText;
this.setTargetPointer = ability.setTargetPointer;
this.thisOrAnother = ability.thisOrAnother;
}
@Override
@ -93,12 +86,13 @@ public class EntersBattlefieldAllTriggeredAbility extends TriggeredAbilityImpl {
switch (setTargetPointer) {
case PLAYER:
this.getEffects().setTargetPointer(new FixedTarget(permanent.getControllerId()));
return true;
break;
case PERMANENT:
this.getEffects().setTargetPointer(new FixedTarget(permanent, game));
break;
default:
return true;
}
return true;
}
@Override
@ -111,9 +105,6 @@ public class EntersBattlefieldAllTriggeredAbility extends TriggeredAbilityImpl {
protected String generateTriggerPhrase() {
StringBuilder sb = new StringBuilder("Whenever ");
if (thisOrAnother) {
sb.append("{this} or another ");
}
sb.append(filter.getMessage());
if (filter.getMessage().startsWith("one or more")) {
sb.append(" enter the battlefield");

View file

@ -59,13 +59,7 @@ public class EntersBattlefieldCastTriggeredAbility extends EntersBattlefieldAllT
public EntersBattlefieldCastTriggeredAbility(Zone zone, Effect effect, FilterPermanent filter, boolean mustCast,
boolean optional,
SetTargetPointer setTargetPointer, String rule, boolean controlledText) {
this(zone, effect, filter, mustCast, optional, setTargetPointer, rule, controlledText, false);
}
protected EntersBattlefieldCastTriggeredAbility(Zone zone, Effect effect, FilterPermanent filter, boolean mustCast,
boolean optional,
SetTargetPointer setTargetPointer, String rule, boolean controlledText, boolean thisOrAnother) {
super(zone, effect, filter, optional, setTargetPointer, rule, controlledText, thisOrAnother);
super(zone, effect, filter, optional, setTargetPointer, rule, controlledText);
this.mustCast = mustCast;
this.addWatcher(new PermanentWasCastWatcher());

View file

@ -4,17 +4,13 @@ import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.filter.FilterPermanentThisOrAnother;
/**
* @author TheElk801
*/
public class EntersBattlefieldThisOrAnotherTriggeredAbility extends EntersBattlefieldAllTriggeredAbility {
private final boolean onlyControlled;
public EntersBattlefieldThisOrAnotherTriggeredAbility(Effect effect, FilterPermanent filter) {
this(effect, filter, false, false);
}
@ -28,35 +24,17 @@ public class EntersBattlefieldThisOrAnotherTriggeredAbility extends EntersBattle
}
public EntersBattlefieldThisOrAnotherTriggeredAbility(Zone zone, Effect effect, FilterPermanent filter, boolean optional, SetTargetPointer setTargetPointer, boolean onlyControlled) {
super(zone, effect, filter, optional, setTargetPointer, null, onlyControlled, true);
this.onlyControlled = onlyControlled;
super(zone, effect,
new FilterPermanentThisOrAnother(filter, onlyControlled),
optional, setTargetPointer, null, onlyControlled);
}
private EntersBattlefieldThisOrAnotherTriggeredAbility(final EntersBattlefieldThisOrAnotherTriggeredAbility ability) {
super(ability);
this.onlyControlled = ability.onlyControlled;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!super.checkTrigger(event, game)) {
return false;
}
Permanent permanent = game.getPermanent(event.getTargetId());
if (permanent == null) {
return false;
}
if (permanent.getId().equals(getSourceId())) {
return true;
}
if (onlyControlled && !permanent.isControlledBy(this.getControllerId())) {
return false;
}
return filter.match(permanent, getControllerId(), this, game);
}
@Override
public EntersBattlefieldThisOrAnotherTriggeredAbility copy() {
return new EntersBattlefieldThisOrAnotherTriggeredAbility(this);
}
}
}

View file

@ -0,0 +1,59 @@
package mage.filter;
import mage.abilities.Ability;
import mage.game.Game;
import mage.game.permanent.Permanent;
import java.util.UUID;
public class FilterPermanentThisOrAnother extends FilterPermanent {
final FilterPermanent otherFilter;
final boolean onlyControlled;
public FilterPermanentThisOrAnother(FilterPermanent otherFilter, boolean onlyControlled) {
this(otherFilter, onlyControlled, generateFilterMessage(otherFilter));
}
public FilterPermanentThisOrAnother(FilterPermanent otherFilter, boolean onlyControlled, String name) {
super(name);
this.otherFilter = otherFilter;
this.onlyControlled = onlyControlled;
}
@Override
public boolean match(Permanent permanent, UUID playerId, Ability source, Game game) {
if (!super.match(permanent, playerId, source, game)) {
return false;
}
if (onlyControlled && !permanent.isControlledBy(source.getControllerId())) {
return false;
}
if (permanent.getId().equals(source.getSourceId())) {
return true;
} else {
return otherFilter.match(permanent, playerId, source, game);
}
}
private FilterPermanentThisOrAnother(FilterPermanentThisOrAnother filter) {
super(filter);
this.otherFilter = filter.otherFilter.copy();
this.onlyControlled = filter.onlyControlled;
}
@Override
public FilterPermanentThisOrAnother copy() {
return new FilterPermanentThisOrAnother(this);
}
protected static String generateFilterMessage(FilterPermanent otherFilter) {
// Remove the indefinite article from the beginning of the message:
String otherFilterMessage = otherFilter.getMessage();
if (otherFilterMessage.startsWith("a ")) {
otherFilterMessage = otherFilterMessage.substring(2);
} else if (otherFilterMessage.startsWith("an ")) {
otherFilterMessage = otherFilterMessage.substring(3);
}
return "{this} or another " + otherFilterMessage;
}
}