* Shifting Shadows - Fixed not proper handling of gained triggered abilities during step resolution of Shifting Shadows effect (fixes #6571).

This commit is contained in:
LevelX2 2020-06-03 15:07:52 +02:00
parent f65f4a4344
commit 07386cce8d
6 changed files with 316 additions and 7 deletions

View file

@ -46,10 +46,12 @@ public final class MairsilThePretender extends CardImpl {
this.power = new MageInt(4); this.power = new MageInt(4);
this.toughness = new MageInt(4); this.toughness = new MageInt(4);
// When Mairsil, the Pretender enters the battlefield, you may exile an artifact or creature card from your hand or graveyard and put a cage counter on it. // When Mairsil, the Pretender enters the battlefield, you may exile an artifact or creature card from your hand
// or graveyard and put a cage counter on it.
this.addAbility(new EntersBattlefieldTriggeredAbility(new MairsilThePretenderExileEffect(), true)); this.addAbility(new EntersBattlefieldTriggeredAbility(new MairsilThePretenderExileEffect(), true));
// Mairsil, the Pretender has all activated abilities of all cards you own in exile with cage counters on them. You may activate each of those abilities only once each turn. // Mairsil, the Pretender has all activated abilities of all cards you own in exile with cage counters on them.
// You may activate each of those abilities only once each turn.
Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new MairsilThePretenderGainAbilitiesEffect()); Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new MairsilThePretenderGainAbilitiesEffect());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -71,12 +71,12 @@ public final class ShiftingShadow extends CardImpl {
class ShiftingShadowEffect extends OneShotEffect { class ShiftingShadowEffect extends OneShotEffect {
private UUID auraId; private final UUID auraId;
public ShiftingShadowEffect(UUID auraId) { public ShiftingShadowEffect(UUID auraId) {
super(Outcome.PutCreatureInPlay); super(Outcome.PutCreatureInPlay);
this.staticText = "destroy this creature. Reveal cards from the top of your library until you reveal a creature card. " this.staticText = "destroy this creature. Reveal cards from the top of your library until you reveal a creature card. "
+ "Put that card onto the battlefield and attach Shifting Shadow to it, then put all other cards revealed this way on the bottom of your library in a random order"; + "Put that card onto the battlefield and attach {this} to it, then put all other cards revealed this way on the bottom of your library in a random order";
this.auraId = auraId; this.auraId = auraId;
} }
@ -107,6 +107,11 @@ class ShiftingShadowEffect extends OneShotEffect {
} }
if (aura != null) { if (aura != null) {
enchanted.destroy(source.getSourceId(), game, false); enchanted.destroy(source.getSourceId(), game, false);
// Because this effect has two steps, we have to call the processAction method here, so that triggered effects of the target going to graveyard go to the stack
// If we don't do it here, gained triggered effects to the target will be removed from the following moveCards method and the applyEffcts done there.
// Example: {@link org.mage.test.commander.duel.MairsilThePretenderTest#MairsilThePretenderTest Test}
game.getState().processAction(game);
Cards revealed = new CardsImpl(); Cards revealed = new CardsImpl();
Cards otherCards = new CardsImpl(); Cards otherCards = new CardsImpl();
for (Card card : controller.getLibrary().getCards(game)) { for (Card card : controller.getLibrary().getCards(game)) {

View file

@ -0,0 +1,90 @@
1 [DGM:11] Aetherling
1 [JUD:77] Anger
1 [10E:66] Arcanis the Omnipotent
1 [SOM:28] Argent Sphinx
1 [CHK:52] Azami, Lady of Scrolls
1 [AVR:47] Deadeye Navigator
1 [DRK:44] Eater of the Dead
1 [EXO:33] Ertai, Wizard Adept
1 [ALA:43] Fatestitcher
1 [SOM:64] Geth, Lord of the Vault
1 [EVE:55] Hateflayer
1 [DKA:139] Havengul Lich
1 [10E:87] Horseshoe Crab
1 [ICE:136] Infernal Denizen
1 [M12:59] Jace's Archivist
1 [SHM:42] Knacksaw Clique
1 [OGW:4] Kozilek, the Great Distortion
1 [PLC:43] Magus of the Bazaar
1 [ICE:150] Minion of Leshrac
1 [SOM:72] Necrotic Ooze
1 [SHM:258] Pili-Pala
1 [MRD:47] Quicksilver Elemental
1 [SOK:53] Sakashima the Impostor
1 [MIR:142] Shauku, Endbringer
1 [M15:231] Soul of New Phyrexia
1 [PLC:110] Torchling
1 [EMN:109] Tree of Perdition
1 [M20:64] Leyline of Anticipation
1 [9ED:152] Phyrexian Arena
1 [PCY:45] Rhystic Study
1 [RNA:245] Blood Crypt
1 [KTK:230] Bloodstained Mire
1 [WWK:132] Bojuka Bog
1 [ELD:333] Command Tower
1 [ALA:222] Crumbling Necropolis
1 [RAV:276] Dimir Aqueduct
1 [CON:142] Exotic Orchard
1 [CHK:277] Hall of the Bandit Lord
8 [IKO:263] Island
1 [GPT:159] Izzet Boilerworks
1 [CHK:279] Minamo, School at Water's Edge
3 [IKO:269] Mountain
1 [C20:298] Path of Ancestry
1 [KTK:239] Polluted Delta
1 [M19:254] Reliquary Tower
1 [ONS:322] Riptide Laboratory
1 [ORI:251] Shivan Reef
1 [GRN:257] Steam Vents
1 [10E:359] Sulfurous Springs
1 [BFZ:249] Sunken Hollow
4 [IKO:266] Swamp
1 [10E:362] Underground River
1 [GRN:259] Watery Grave
1 [ODY:118] Buried Alive
1 [3ED:105] Demonic Tutor
1 [USG:188] Gamble
1 [AVR:151] Reforge the Soul
1 [EMA:108] Toxic Deluge
1 [MB1:1603] Mana Crypt
1 [5ED:388] Mana Vault
1 [HOU:165] Mirage Mirror
1 [5ED:391] Nevinyrral's Disk
1 [CHK:268] Sensei's Divining Top
1 [3ED:274] Sol Ring
1 [5DN:156] Staff of Domination
1 [LRW:263] Thousand-Year Elixir
1 [UDS:139] Thran Dynamo
1 [5DN:163] Vedalken Orrery
1 [TMP:55] Capsize
1 [C20:146] Chaos Warp
1 [RTR:35] Cyclonic Rift
1 [ODY:132] Entomb
1 [INV:57] Fact or Fiction
1 [TMP:70] Intuition
1 [THS:65] Swan Song
1 [RTR:111] Vandalblast
1 [3ED:185] Wheel of Fortune
1 [GTC:207] Whispering Madness
1 [USG:111] Windfall
1 [SHM:248] Cauldron of Souls
1 [RAV:260] Dimir Signet
1 [9ED:297] Fellwar Stone
1 [DOM:215] Gilded Lotus
1 [ULG:126] Grim Monolith
1 [GTC:231] Illusionist's Bracers
1 [GPT:152] Izzet Signet
1 [MRD:199] Lightning Greaves
SB: 1 [C17:41] Mairsil, the Pretender
LAYOUT MAIN:(1,7)(CARD_TYPE,false,50)|([SHM:258],[M15:231])([SHM:248],[RAV:260],[9ED:297],[DOM:215],[ULG:126],[GTC:231],[GPT:152],[MRD:199],[MB1:1603],[5ED:388],[HOU:165],[5ED:391],[CHK:268],[3ED:274],[5DN:156],[LRW:263],[UDS:139],[5DN:163])([DGM:11],[JUD:77],[10E:66],[SOM:28],[CHK:52],[AVR:47],[DRK:44],[EXO:33],[ALA:43],[SOM:64],[EVE:55],[DKA:139],[10E:87],[ICE:136],[M12:59],[SHM:42],[OGW:4],[PLC:43],[ICE:150],[SOM:72],[MRD:47],[SOK:53],[MIR:142],[PLC:110],[EMN:109])([M20:64],[9ED:152],[PCY:45])([TMP:55],[C20:146],[RTR:35],[ODY:132],[INV:57],[TMP:70],[THS:65])([RNA:245],[KTK:230],[WWK:132],[ELD:333],[ALA:222],[RAV:276],[CON:142],[CHK:277],[IKO:263],[IKO:263],[IKO:263],[IKO:263],[IKO:263],[IKO:263],[IKO:263],[IKO:263],[GPT:159],[CHK:279],[IKO:269],[IKO:269],[IKO:269],[C20:298],[KTK:239],[M19:254],[ONS:322],[ORI:251],[GRN:257],[10E:359],[BFZ:249],[IKO:266],[IKO:266],[IKO:266],[IKO:266],[10E:362],[GRN:259])([ODY:118],[3ED:105],[USG:188],[AVR:151],[EMA:108],[RTR:111],[3ED:185],[GTC:207],[USG:111])
LAYOUT SIDEBOARD:(1,1)(NONE,false,50)|([C17:41])

View file

@ -0,0 +1,203 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.mage.test.commander.duel;
import java.io.FileNotFoundException;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.GameException;
import org.junit.Test;
import static org.mage.test.player.TestPlayer.NO_TARGET;
import org.mage.test.serverside.base.CardTestCommanderDuelBase;
/**
*
* @author LevelX2
*/
public class MairsilThePretenderTest extends CardTestCommanderDuelBase {
@Override
protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException {
// When Mairsil, the Pretender enters the battlefield, you may exile an artifact or creature card from your hand
// or graveyard and put a cage counter on it.
// Mairsil, the Pretender has all activated abilities of all cards you own in exile with cage counters on them.
// You may activate each of those abilities only once each turn.
setDecknamePlayerA("CommanderDuel_Mairisil_UBR.dck"); // Commander = Mairsil, the Pretender {1}{U}{B}{R}
return super.createNewGameAndPlayers();
}
/**
* Basic Test
*/
@Test
public void useShifitingShadowTest() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
// Enchant creature
// Enchanted creature has haste and At the beginning of your upkeep, destroy this creature.
// Reveal cards from the top of your library until you reveal a creature card.
// Put that card onto the battlefield and attach Shifting Shadow to it,
// then put all other cards revealed this way on the bottom of your library in a random order.
addCard(Zone.HAND, playerA, "Shifting Shadow", 1); // {2}{R}
// Tap: Draw three cards.
// {2}{U}{U}: Return Arcanis the Omnipotent to its owner's hand.
addCard(Zone.HAND, playerA, "Arcanis the Omnipotent", 1); // Creature {3}{U}{U}{U}
setChoice(playerA, "Yes");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mairsil, the Pretender");
setChoice(playerA, "Yes"); // Exile a card
setChoice(playerA, "Arcanis the Omnipotent");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Shifting Shadow", "Mairsil, the Pretender");
setChoice(playerA, "Yes"); // Move commander to command zone
setStopAt(5, PhaseStep.PRECOMBAT_MAIN);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
assertLife(playerA, 40);
assertLife(playerB, 40);
assertExileCount("Arcanis the Omnipotent", 1);
assertCommandZoneCount(playerA, "Mairsil, the Pretender", 1);
assertPermanentCount(playerA, "Shifting Shadow", 1);
}
/**
* I tried playing it in Mairsil the Pretender commander deck and found 2
* cases where the creature is not properly destroyed:
*
* Using Arcanis the Omnipotent ability to return my commander to hand while
* trigger is on the stack I got Mairsil to hand then got asked whether I
* want to put him in gy so I answered yes assuming it cannot be destroyed
* while in my hand. He got put in graveyard straight from my hand.
* According to Oracle rules I should get a creature from top of the deck
* and still have my commander in hand. After giving my commander undying
* with shifting shadow trigger on the stack he got put in gy with undying
* not triggering.
*
* All this points to the card being hardcoded to put the creature into
* graveyard instead of simply destroying it
*/
@Test
public void useShifitingShadowAndArcanisTest() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 3);
skipInitShuffling();
// Enchant creature
// Enchanted creature has haste and At the beginning of your upkeep, destroy this creature.
// Reveal cards from the top of your library until you reveal a creature card.
// Put that card onto the battlefield and attach Shifting Shadow to it,
// then put all other cards revealed this way on the bottom of your library in a random order.
addCard(Zone.HAND, playerA, "Shifting Shadow", 1); // {2}{R}
// Tap: Draw three cards.
// {2}{U}{U}: Return Arcanis the Omnipotent to its owner's hand.
addCard(Zone.HAND, playerA, "Arcanis the Omnipotent", 1); // Creature {3}{U}{U}{U}
setChoice(playerA, "Yes");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mairsil, the Pretender");
setChoice(playerA, "Yes"); // Exile a card
setChoice(playerA, "Arcanis the Omnipotent");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Shifting Shadow", "Mairsil, the Pretender");
activateAbility(5, PhaseStep.UPKEEP, playerA, "{2}{U}{U}: Return", NO_TARGET, "At the beginning of your upkeep");
setChoice(playerA, "No"); // Don't move commander to command zone because it goes to hand
setStopAt(5, PhaseStep.PRECOMBAT_MAIN);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
assertLife(playerA, 40);
assertLife(playerB, 40);
assertExileCount("Arcanis the Omnipotent", 1);
assertCommandZoneCount(playerA, "Mairsil, the Pretender", 0);
assertHandCount(playerA, "Mairsil, the Pretender", 1);
assertGraveyardCount(playerA, "Shifting Shadow", 1); // Goes to graveyard because commander left battlefield to hand from Arcanis ability
assertPermanentCount(playerA, "Silvercoat Lion", 1);
}
@Test
public void useShifitingShadowAndEndlingTest() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 3);
skipInitShuffling();
// Enchant creature
// Enchanted creature has haste and At the beginning of your upkeep, destroy this creature.
// Reveal cards from the top of your library until you reveal a creature card.
// Put that card onto the battlefield and attach Shifting Shadow to it,
// then put all other cards revealed this way on the bottom of your library in a random order.
addCard(Zone.HAND, playerA, "Shifting Shadow", 1); // {2}{R}
// Tap: Draw three cards.
// {2}{U}{U}: Return Arcanis the Omnipotent to its owner's hand.
addCard(Zone.HAND, playerA, "Arcanis the Omnipotent", 1); // Creature {3}{U}{U}{U}
// {B}: Endling gains menace until end of turn.
// {B}: Endling gains deathtouch until end of turn.
// {B}: Endling gains undying until end of turn.
// {1}: Endling gets +1/-1 or -1/+1 until end of turn.
addCard(Zone.HAND, playerA, "Endling", 1); // Creature {3}{U}{U}{U}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mairsil, the Pretender");
setChoice(playerA, "Yes"); // Exile a card
setChoice(playerA, "Yes"); // Exile from Hand
setChoice(playerA, "Endling");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Shifting Shadow", "Mairsil, the Pretender");
activateAbility(5, PhaseStep.UPKEEP, playerA, "{B}: {this} gains Undying", NO_TARGET, "At the beginning of your upkeep");
setChoice(playerA, "No"); // Don't move commander to command zone because can come back by Undying
setChoice(playerA, "Yes"); // Exile a card (Mairsil comes back from Undying)
setChoice(playerA, "Yes"); // Exile from hand
setChoice(playerA, "Arcanis the Omnipotent");
setStopAt(5, PhaseStep.PRECOMBAT_MAIN);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
assertLife(playerA, 40);
assertLife(playerB, 40);
assertExileCount("Endling", 1);
assertExileCount("Arcanis the Omnipotent", 1);
assertCommandZoneCount(playerA, "Mairsil, the Pretender", 0);
assertGraveyardCount(playerA, "Mairsil, the Pretender", 0);
assertPermanentCount(playerA, "Mairsil, the Pretender", 1); // Returns from Undying
assertPowerToughness(playerA, "Mairsil, the Pretender", 5, 5);
assertPermanentCount(playerA, "Shifting Shadow", 1); // Enchants Silvercoat Lion
assertPermanentCount(playerA, "Silvercoat Lion", 1);
}
}

View file

@ -1600,6 +1600,15 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
player.addAction(turnNum, step, ACTIVATE_ABILITY + ability + "$target=" + String.join("^", targetNames)); player.addAction(turnNum, step, ACTIVATE_ABILITY + ability + "$target=" + String.join("^", targetNames));
} }
/**
*
* @param turnNum
* @param step
* @param player
* @param ability
* @param targetName use NO_TARGET if there is no target to set
* @param spellOnStack
*/
public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, String targetName, String spellOnStack) { public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, String targetName, String spellOnStack) {
// TODO: it's uses computerPlayer to execute, only ability target will work, but choices and targets commands aren't // TODO: it's uses computerPlayer to execute, only ability target will work, but choices and targets commands aren't
this.activateAbility(turnNum, step, player, ability, targetName, spellOnStack, StackClause.WHILE_ON_STACK); this.activateAbility(turnNum, step, player, ability, targetName, spellOnStack, StackClause.WHILE_ON_STACK);

View file

@ -91,9 +91,9 @@ public class CommanderReplacementEffect extends ReplacementEffectImpl {
public boolean applies(GameEvent event, Ability source, Game game) { public boolean applies(GameEvent event, Ability source, Game game) {
ZoneChangeEvent zEvent = (ZoneChangeEvent) event; ZoneChangeEvent zEvent = (ZoneChangeEvent) event;
if (!game.isSimulation() && commanderId.equals(zEvent.getTargetId())) { // if (!game.isSimulation() && commanderId.equals(zEvent.getTargetId())) {
//System.out.println("applies " + game.getTurnNum() + ": " + game.getObject(event.getTargetId()).getName() + ": " + zEvent.getFromZone() + " -> " + zEvent.getToZone() + "; " + game.getObject(zEvent.getSourceId())); // System.out.println("applies " + game.getTurnNum() + ": " + game.getObject(event.getTargetId()).getName() + ": " + zEvent.getFromZone() + " -> " + zEvent.getToZone() + "; " + game.getObject(zEvent.getSourceId()));
} // }
if (zEvent.getToZone().equals(Zone.HAND) && !alsoHand) { if (zEvent.getToZone().equals(Zone.HAND) && !alsoHand) {
return false; return false;