[OTJ] Implement Satoru, the Infiltrator and Freestrider Commando (#12052)

This commit is contained in:
Susucre 2024-04-05 00:17:14 +02:00 committed by GitHub
parent d1de8b8cd3
commit 62279bbe6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 589 additions and 0 deletions

View file

@ -0,0 +1,78 @@
package mage.cards.f;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.condition.Condition;
import mage.abilities.decorator.ConditionalOneShotEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.keyword.PlotAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.stack.Spell;
import java.util.UUID;
/**
* @author Susucr
*/
public final class FreestriderCommando extends CardImpl {
public FreestriderCommando(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}");
this.subtype.add(SubType.CENTAUR);
this.subtype.add(SubType.MERCENARY);
this.power = new MageInt(3);
this.toughness = new MageInt(3);
// Freestrider Commando enters the battlefield with two +1/+1 counters on it if it wasn't cast or no mana was spent to cast it.
this.addAbility(new EntersBattlefieldAbility(
new ConditionalOneShotEffect(
new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)),
FreestriderCommandoCondition.instance, ""
), "with two +1/+1 counters on it if it wasn't cast or no mana was spent to cast it"
));
// Plot {3}{G}
this.addAbility(new PlotAbility("{3}{G}"));
}
private FreestriderCommando(final FreestriderCommando card) {
super(card);
}
@Override
public FreestriderCommando copy() {
return new FreestriderCommando(this);
}
}
enum FreestriderCommandoCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanentEntering(source.getSourceId());
if (permanent == null) {
return false;
}
// Check if the spell exists on the stack
Spell spell = game.getStack().getSpell(source.getSourceId());
if (spell == null) {
return true; // cannot find the spell, so it wasn't cast.
}
// spell was found, did it cost mana?
return 0 == spell.getStackAbility().getManaCostsToPay().getUsedManaToPay().count();
}
@Override
public String toString() {
return "if it wasn't cast or no mana was spent to cast it";
}
}

View file

@ -0,0 +1,114 @@
package mage.cards.s;
import mage.MageInt;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.keyword.MenaceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeBatchEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentToken;
import mage.game.stack.Spell;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author Susucr
*/
public final class SatoruTheInfiltrator extends CardImpl {
public SatoruTheInfiltrator(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.ROGUE);
this.power = new MageInt(2);
this.toughness = new MageInt(3);
// Menace
this.addAbility(new MenaceAbility());
// Whenever Satoru, the Infiltrator and/or one or more other nontoken creatures enter the battlefield under your control, if none of them were cast or no mana was spent to cast them, draw a card.
this.addAbility(new SatoruTheInfiltratorTriggeredAbility());
}
private SatoruTheInfiltrator(final SatoruTheInfiltrator card) {
super(card);
}
@Override
public SatoruTheInfiltrator copy() {
return new SatoruTheInfiltrator(this);
}
}
class SatoruTheInfiltratorTriggeredAbility extends TriggeredAbilityImpl {
public SatoruTheInfiltratorTriggeredAbility() {
super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false);
this.setTriggerPhrase("Whenever {this} and/or one or more other nontoken creatures "
+ "enter the battlefield under your control, if none of them were cast or no mana was spent to cast them, ");
}
protected SatoruTheInfiltratorTriggeredAbility(final SatoruTheInfiltratorTriggeredAbility ability) {
super(ability);
}
@Override
public SatoruTheInfiltratorTriggeredAbility copy() {
return new SatoruTheInfiltratorTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH;
}
// event is GameEvent.EventType.ENTERS_THE_BATTLEFIELD
@Override
public boolean checkTrigger(GameEvent event, Game game) {
ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event;
List<ZoneChangeEvent> moved = zEvent.getEvents()
.stream()
.filter(e -> e.getToZone() == Zone.BATTLEFIELD) // Keep only to the battlefield
.filter(e -> {
Permanent permanent = e.getTarget();
if (permanent == null) {
return false;
}
return permanent.isControlledBy(getControllerId()) // under your control
&& (permanent.getId().equals(getSourceId()) // {this}
|| (permanent.isCreature(game) && !(permanent instanceof PermanentToken)) // other nontoken Creature
);
})
.collect(Collectors.toList());
if (moved.isEmpty()) {
return false;
}
// At this point, we have at least one event matching
// "Whenever {this} and/or one or more other nontoken creatures enter the battlefield under your control"
// Now to check that none were cast using mana
for (ZoneChangeEvent zce : moved) {
if (zce.getFromZone() != Zone.STACK) {
continue; // not from stack, we good for this one event
}
Spell spell = game.getSpellOrLKIStack(zce.getTargetId());
if (spell != null && spell.getStackAbility().getManaCostsToPay().getUsedManaToPay().count() > 0) {
return false; // found one that did use mana, so no triggering.
}
}
return true; // all relevant permanents passed the spell mana check, so triggering.
}
}

View file

@ -113,6 +113,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet {
cards.add(new SetCardInfo("Form a Posse", 204, Rarity.UNCOMMON, mage.cards.f.FormAPosse.class));
cards.add(new SetCardInfo("Forsaken Miner", 88, Rarity.UNCOMMON, mage.cards.f.ForsakenMiner.class));
cards.add(new SetCardInfo("Fortune, Loyal Steed", 12, Rarity.RARE, mage.cards.f.FortuneLoyalSteed.class));
cards.add(new SetCardInfo("Freestrider Commando", 162, Rarity.COMMON, mage.cards.f.FreestriderCommando.class));
cards.add(new SetCardInfo("Freestrider Lookout", 163, Rarity.RARE, mage.cards.f.FreestriderLookout.class));
cards.add(new SetCardInfo("Frontier Seeker", 13, Rarity.UNCOMMON, mage.cards.f.FrontierSeeker.class));
cards.add(new SetCardInfo("Full Steam Ahead", 164, Rarity.UNCOMMON, mage.cards.f.FullSteamAhead.class));
@ -235,6 +236,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet {
cards.add(new SetCardInfo("Rustler Rampage", 27, Rarity.UNCOMMON, mage.cards.r.RustlerRampage.class));
cards.add(new SetCardInfo("Ruthless Lawbringer", 229, Rarity.UNCOMMON, mage.cards.r.RuthlessLawbringer.class));
cards.add(new SetCardInfo("Sandstorm Verge", 263, Rarity.UNCOMMON, mage.cards.s.SandstormVerge.class));
cards.add(new SetCardInfo("Satoru, the Infiltrator", 230, Rarity.RARE, mage.cards.s.SatoruTheInfiltrator.class));
cards.add(new SetCardInfo("Scalestorm Summoner", 144, Rarity.UNCOMMON, mage.cards.s.ScalestormSummoner.class));
cards.add(new SetCardInfo("Scorching Shot", 145, Rarity.UNCOMMON, mage.cards.s.ScorchingShot.class));
cards.add(new SetCardInfo("Seize the Secrets", 64, Rarity.COMMON, mage.cards.s.SeizeTheSecrets.class));

View file

@ -0,0 +1,127 @@
package org.mage.test.cards.single.otj;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class FreestriderCommandoTest extends CardTestPlayerBase {
/**
* {@link mage.cards.f.FreestriderCommando Freestrider Commando} {2}{G}
* Creature Centaur Mercenary
* Freestrider Commando enters the battlefield with two +1/+1 counters on it if it wasnt cast or no mana was spent to cast it.
* Plot {3}{G} (You may pay {3}{G} and exile this card from your hand. Cast it as a sorcery on a later turn without paying its mana cost. Plot only as a sorcery.)
* 3/3
*/
private static final String commando = "Freestrider Commando";
@Test
public void test_RegularCast() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
addCard(Zone.HAND, playerA, commando);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
// No +1/+1 as cast with mana
assertPowerToughness(playerA, commando, 3, 3);
}
@Test
public void test_PlotCast() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4);
addCard(Zone.HAND, playerA, commando);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando + " using Plot");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
// 2 +1/+1 as cast free with plot
assertPowerToughness(playerA, commando, 3 + 2, 3 + 2);
}
@Test
public void test_OmniscienceCast() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Omniscience");
addCard(Zone.HAND, playerA, commando);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando);
setChoice(playerA, true); // Omniscience asks for confirmation to cast to avoid missclick?
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
// 2 +1/+1 as cast free with Omniscience
assertPowerToughness(playerA, commando, 3 + 2, 3 + 2);
}
@Test
public void test_PlotCast_WithTax() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4);
addCard(Zone.HAND, playerA, commando);
addCard(Zone.BATTLEFIELD, playerA, "Sphere of Resistance"); // Spells cost {1} more to cast
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando + " using Plot");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
// no +1/+1 as cast free with plot, but tax make mana being paid
assertPowerToughness(playerA, commando, 3, 3);
}
@Test
public void test_Blink() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Savannah", 4);
addCard(Zone.HAND, playerA, commando);
addCard(Zone.HAND, playerA, "Ephemerate");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ephemerate", "Freestrider Commando");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
// 2 +1/+1 as cast free with plot, as re-enter without being cast
assertPowerToughness(playerA, commando, 3 + 2, 3 + 2);
}
@Test
public void test_DoubleMajor() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 6);
addCard(Zone.HAND, playerA, commando);
addCard(Zone.HAND, playerA, "Double Major");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Freestrider Commando");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 2); // resolve Double Major + the copy
checkPT("double major copy enters as a 5/5", 1, PhaseStep.PRECOMBAT_MAIN, playerA, commando, 3 + 2, 3 + 2);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, commando, 2); // real + copy
}
}

View file

@ -0,0 +1,268 @@
package org.mage.test.cards.single.otj;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class SatoruTheInfiltratorTest extends CardTestPlayerBase {
/**
* {@link mage.cards.s.SatoruTheInfiltrator Satoru, the Infiltrator} {U}{B}
* Legendary Creature Human Ninja Rogue
* Menace
* Whenever Satoru, the Infiltrator and/or one or more other nontoken creatures enter the battlefield under your control, if none of them were cast or no mana was spent to cast them, draw a card.
*/
private static final String satoru = "Satoru, the Infiltrator";
/**
* {@link mage.cards.f.FreestriderCommando Freestrider Commando} {2}{G}
* Creature Centaur Mercenary
* Freestrider Commando enters the battlefield with two +1/+1 counters on it if it wasnt cast or no mana was spent to cast it.
* Plot {3}{G} (You may pay {3}{G} and exile this card from your hand. Cast it as a sorcery on a later turn without paying its mana cost. Plot only as a sorcery.)
* 3/3
*/
private static final String commando = "Freestrider Commando";
@Test
public void test_RegularCast_Other() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, satoru);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
addCard(Zone.HAND, playerA, commando);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, commando, 1);
assertHandCount(playerA, 0); // no card draw.
}
@Test
public void test_RegularCast_Other_Memnite() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, satoru);
addCard(Zone.HAND, playerA, "Memnite"); // Is free!
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Memnite");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Memnite", 1);
assertHandCount(playerA, 1);
}
@Test
public void test_RegularCast_Satoru() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 2);
addCard(Zone.HAND, playerA, satoru);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, satoru);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, satoru, 1);
assertHandCount(playerA, 0); // no card draw.
}
@Test
public void test_PlotCast() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, satoru);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4);
addCard(Zone.HAND, playerA, commando);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando + " using Plot");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, commando, 1);
assertHandCount(playerA, 1 + 1); // Drawn 1 from draw step + 1 from Satoru
}
@Test
public void test_Omniscience_CastOther() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, satoru);
addCard(Zone.BATTLEFIELD, playerA, "Omniscience");
addCard(Zone.HAND, playerA, commando);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando);
setChoice(playerA, true); // Omniscience asks for confirmation to cast to avoid missclick?
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, commando, 1);
assertHandCount(playerA, 1); // Drawn 1 from Satoru
}
@Test
public void test_Omniscience_CastSatoru() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Omniscience");
addCard(Zone.HAND, playerA, satoru);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, satoru);
setChoice(playerA, true); // Omniscience asks for confirmation to cast to avoid missclick?
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, satoru, 1);
assertHandCount(playerA, 1); // Drawn 1 from Satoru
}
@Test
public void test_PlotCast_WithTax() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, satoru);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4);
addCard(Zone.HAND, playerA, commando);
addCard(Zone.BATTLEFIELD, playerA, "Sphere of Resistance"); // Spells cost {1} more to cast
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando + " using Plot");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, commando, 1);
assertHandCount(playerA, 1); // Drawn 1 from draw step, 0 from Satoru
}
@Test
public void test_Blink() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, satoru);
addCard(Zone.BATTLEFIELD, playerA, "Savannah", 4);
addCard(Zone.HAND, playerA, commando);
addCard(Zone.HAND, playerA, "Ephemerate");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ephemerate", commando);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, commando, 1);
assertHandCount(playerA, 1); // Drawn 1 from Satoru
}
@Test
public void test_MultipleOtherEnter() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, satoru);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 6);
addCard(Zone.GRAVEYARD, playerA, commando, 3);
addCard(Zone.HAND, playerA, "Storm of Souls"); // Return all creature cards from your graveyard to the battlefield. Each of them is a 1/1 Spirit with flying in addition to its other types. Exile Storm of Souls.
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Storm of Souls");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, commando, 3);
assertHandCount(playerA, 1); // Drawn 1 from Satoru
}
@Test
public void test_Saturo_AndAnother_Enter() {
setStrictChooseMode(true);
addCard(Zone.GRAVEYARD, playerA, satoru);
addCard(Zone.GRAVEYARD, playerA, commando, 3);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5);
// Each player exiles all creature cards from their graveyard, then sacrifices all creatures they control,
// then puts all cards they exiled this way onto the battlefield.
addCard(Zone.HAND, playerA, "Living Death");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Living Death");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, commando, 3);
assertPermanentCount(playerA, satoru, 1);
assertHandCount(playerA, 1); // Drawn 1 from Satoru
}
@Test
public void test_CopyOnStack() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, satoru);
addCard(Zone.HAND, playerA, commando);
addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 5);
// Copy target creature spell you control, except it isnt legendary if the spell is legendary
addCard(Zone.HAND, playerA, "Double Major");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", commando);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, commando, 2);
assertPermanentCount(playerA, satoru, 1);
assertHandCount(playerA, 0); // Drawn 0 from Satoru, as token does not count.
}
@Test
public void test_Tokens() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, satoru);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
addCard(Zone.HAND, playerA, "Krenko's Command"); // Create two 1/1 red Goblin creature tokens.
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Krenko's Command");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Goblin Token", 2);
assertPermanentCount(playerA, satoru, 1);
assertHandCount(playerA, 0); // Drawn 0 from Satoru, as token does not count.
}
@Test
public void test_CopySatoruOnStack() {
setStrictChooseMode(true);
addCard(Zone.HAND, playerA, satoru);
addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 3);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
// Copy target creature spell you control, except it isnt legendary if the spell is legendary
addCard(Zone.HAND, playerA, "Double Major");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, satoru);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", satoru);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, satoru, 2);
assertHandCount(playerA, 1); // While Satoru does not trigger on other tokens, a copy of Satoru on the stack will trigger its own etb.
}
}