mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
Reworked Suspend ability: (#13527)
* Updated Delay and Gandalf Of The Secret Fire to get the main card since they target spells * Suspend now properly lets you play either side of mdfc and spell parts from adventure/omen cards utilizing CardUtil.castSpellWithAttributesForFree method * Removed extra code in SuspendPlayCardEffect since the referenced bug for Epochrasite does not seem to appear. Removed related gainedTemporary variable also. * Added tests for Omen and Suspend With Taigam, Master Opportunists as well as an Epochrasite test for recasting after suspend.
This commit is contained in:
parent
8dd8953a85
commit
1b06813997
5 changed files with 158 additions and 43 deletions
|
|
@ -68,7 +68,7 @@ class DelayEffect extends OneShotEffect {
|
|||
if (controller != null && spell != null) {
|
||||
Effect effect = new CounterTargetWithReplacementEffect(PutCards.EXILED);
|
||||
effect.setTargetPointer(this.getTargetPointer().copy());
|
||||
Card card = game.getCard(spell.getSourceId());
|
||||
Card card = spell.getMainCard();
|
||||
if (card != null && effect.apply(game, source) && game.getState().getZone(card.getId()) == Zone.EXILED) {
|
||||
boolean hasSuspend = card.getAbilities(game).containsClass(SuspendAbility.class);
|
||||
UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game);
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class GandalfOfTheSecretFireEffect extends ReplacementEffectImpl {
|
|||
game.informPlayers(controller.getLogName() + " exiles " + sourceSpell.getLogName() + " with 3 time counters on it");
|
||||
}
|
||||
if (!sourceSpell.getAbilities(game).containsClass(SuspendAbility.class)) {
|
||||
game.addEffect(new GainSuspendEffect(new MageObjectReference(sourceSpell.getCard(), game)), source);
|
||||
game.addEffect(new GainSuspendEffect(new MageObjectReference(sourceSpell.getMainCard(), game)), source);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,40 @@ public class SuspendTest extends CardTestPlayerBase {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests bug that was mentioned in suspend ability, but does not appear to still be an issue.
|
||||
* Epochrasite being unable to be cast after casting from suspend and returning to hand.
|
||||
*/
|
||||
@Test
|
||||
public void test_Single_Epochrasite_Recast_After_Suspend() {
|
||||
// Bug was mentioned in suspend ability, but does not appear to still be an issue
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
|
||||
// Epochrasite enters the battlefield with three +1/+1 counters on it if you didn't cast it from your hand.
|
||||
// When Epochrasite dies, exile it with three time counters on it and it gains suspend.
|
||||
addCard(Zone.HAND, playerA, "Epochrasite", 1);
|
||||
addCard(Zone.HAND, playerB, "Lightning Bolt", 1);
|
||||
addCard(Zone.HAND, playerB, "Boomerang", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Island", 2);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Epochrasite");
|
||||
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Epochrasite");
|
||||
castSpell(7, PhaseStep.DRAW, playerB, "Boomerang", "Epochrasite");
|
||||
castSpell(7, PhaseStep.PRECOMBAT_MAIN, playerA, "Epochrasite");
|
||||
|
||||
|
||||
setChoice(playerA, true); // choose yes to cast
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(7, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertGraveyardCount(playerB, "Lightning Bolt", 1);
|
||||
assertPermanentCount(playerA, "Epochrasite", 1); // returned on turn 7 and cast again after going to hand
|
||||
assertPowerToughness(playerA, "Epochrasite", 1, 1);
|
||||
assertAbility(playerA, "Epochrasite", HasteAbility.getInstance(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests Jhoira of the Ghitu works (give suspend to a exiled card) {2},
|
||||
* Exile a nonland card from your hand: Put four time counters on the exiled
|
||||
|
|
@ -275,6 +309,7 @@ public class SuspendTest extends CardTestPlayerBase {
|
|||
|
||||
// 3 time counters removes on upkeep (3, 5, 7) and cast again
|
||||
setChoice(playerA, true); // choose yes to cast
|
||||
setChoice(playerA, "Cast Wear");
|
||||
addTarget(playerA, "Bident of Thassa");
|
||||
checkPermanentCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerB, "Bident of Thassa", 0);
|
||||
checkPermanentCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerB, "Bow of Nylea", 1);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
package org.mage.test.cards.single.tdm;
|
||||
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import mage.counters.CounterType;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||
|
||||
public class TaigamMasterOpportunistTest extends CardTestPlayerBase {
|
||||
|
||||
private static final String TAIGAM = "Taigam, Master Opportunist";
|
||||
private static final String ORNITHOPTER = "Ornithopter";
|
||||
private static final String TWINMAW = "Twinmaw Stormbrood";
|
||||
private static final String BITE = "Charring Bite";
|
||||
private static final String TURTLE = "Aegis Turtle";
|
||||
private static final String AKOUM = "Akoum Warrior";
|
||||
|
||||
@Test
|
||||
public void testCardWithSpellOption() {
|
||||
setStrictChooseMode(true);
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, TAIGAM);
|
||||
addCard(Zone.HAND, playerA, ORNITHOPTER);
|
||||
addCard(Zone.HAND, playerA, TWINMAW);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Plateau", 6);
|
||||
addCard(Zone.BATTLEFIELD, playerB, TURTLE, 2);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ORNITHOPTER, true);
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, BITE, TURTLE);
|
||||
checkCardCounters("time counters", 1, PhaseStep.BEGIN_COMBAT, playerA, TWINMAW, CounterType.TIME, 4);
|
||||
setChoice(playerA, true);
|
||||
setChoice(playerA, "Cast " + BITE);
|
||||
addTarget(playerA, TURTLE);
|
||||
|
||||
setStopAt(9, PhaseStep.PRECOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertLibraryCount(playerA, TWINMAW, 1);
|
||||
assertGraveyardCount(playerB, TURTLE, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMDFC() {
|
||||
setStrictChooseMode(true);
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, TAIGAM);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Plateau", 6);
|
||||
addCard(Zone.HAND, playerA, ORNITHOPTER);
|
||||
addCard(Zone.HAND, playerA, AKOUM);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ORNITHOPTER, true);
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, AKOUM);
|
||||
setChoice(playerA, true);
|
||||
setChoice(playerA, "Play Akoum Teeth");
|
||||
|
||||
setStopAt(9, PhaseStep.PRECOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, "Akoum Teeth", 1);
|
||||
assertTapped("Akoum Teeth", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMDFC2() {
|
||||
setStrictChooseMode(true);
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Plateau", 6);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Island", 2);
|
||||
addCard(Zone.HAND, playerB, "Delay");
|
||||
addCard(Zone.HAND, playerA, AKOUM);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, AKOUM);
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Delay", AKOUM);
|
||||
setChoice(playerA, true);
|
||||
setChoice(playerA, "Play Akoum Teeth");
|
||||
|
||||
setStopAt(7, PhaseStep.PRECOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, "Akoum Teeth", 1);
|
||||
assertTapped("Akoum Teeth", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
setStrictChooseMode(true);
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Plateau", 6);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Island", 2);
|
||||
addCard(Zone.BATTLEFIELD, playerB, TURTLE);
|
||||
addCard(Zone.HAND, playerB, "Delay");
|
||||
addCard(Zone.HAND, playerA, TWINMAW);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, TWINMAW);
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Delay", TWINMAW);
|
||||
setChoice(playerA, true);
|
||||
setChoice(playerA, "Cast " + BITE);
|
||||
addTarget(playerA, TURTLE);
|
||||
|
||||
setStopAt(7, PhaseStep.PRECOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package mage.abilities.keyword;
|
||||
|
||||
import mage.ApprovingObject;
|
||||
import mage.MageIdentifier;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpecialAction;
|
||||
|
|
@ -17,8 +16,11 @@ import mage.abilities.effects.ContinuousEffectImpl;
|
|||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.effects.common.counter.RemoveCounterSourceEffect;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.CardsImpl;
|
||||
import mage.cards.ModalDoubleFacedCard;
|
||||
import mage.constants.*;
|
||||
import mage.counters.CounterType;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
|
@ -26,8 +28,6 @@ import mage.players.Player;
|
|||
import mage.target.targetpointer.FixedTarget;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -112,7 +112,6 @@ import java.util.UUID;
|
|||
public class SuspendAbility extends SpecialAction {
|
||||
|
||||
private final String ruleText;
|
||||
private boolean gainedTemporary;
|
||||
|
||||
/**
|
||||
* Gives the card the SuspendAbility
|
||||
|
|
@ -177,6 +176,11 @@ public class SuspendAbility extends SpecialAction {
|
|||
* or added by Jhoira of the Ghitu
|
||||
*/
|
||||
public static void addSuspendTemporaryToCard(Card card, Ability source, Game game) {
|
||||
if (card instanceof ModalDoubleFacedCard) {
|
||||
// Need to ensure the suspend ability gets put on the left side card
|
||||
// since counters get added to this card.
|
||||
card = ((ModalDoubleFacedCard) card).getLeftHalfCard();
|
||||
}
|
||||
SuspendAbility ability = new SuspendAbility(0, null, card, false);
|
||||
ability.setSourceId(card.getId());
|
||||
ability.setControllerId(card.getOwnerId());
|
||||
|
|
@ -206,7 +210,6 @@ public class SuspendAbility extends SpecialAction {
|
|||
private SuspendAbility(final SuspendAbility ability) {
|
||||
super(ability);
|
||||
this.ruleText = ability.ruleText;
|
||||
this.gainedTemporary = ability.gainedTemporary;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -232,10 +235,6 @@ public class SuspendAbility extends SpecialAction {
|
|||
return ruleText;
|
||||
}
|
||||
|
||||
public boolean isGainedTemporary() {
|
||||
return gainedTemporary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuspendAbility copy() {
|
||||
return new SuspendAbility(this);
|
||||
|
|
@ -345,40 +344,16 @@ class SuspendPlayCardEffect extends OneShotEffect {
|
|||
if (player == null || card == null) {
|
||||
return false;
|
||||
}
|
||||
if (!player.chooseUse(Outcome.Benefit, "Play " + card.getLogName() + " without paying its mana cost?", source, game)) {
|
||||
// ensure we're getting the main card when passing to CardUtil to check all parts of card
|
||||
// MDFC points to left half card
|
||||
card = card.getMainCard();
|
||||
// cast/play the card for free
|
||||
if (!CardUtil.castSpellWithAttributesForFree(player, source, game, new CardsImpl(card),
|
||||
StaticFilters.FILTER_CARD, null, true)) {
|
||||
return true;
|
||||
}
|
||||
// remove temporary suspend ability (used e.g. for Epochrasite)
|
||||
// TODO: isGainedTemporary is not set or use in other places, so it can be deleted?!
|
||||
List<Ability> abilitiesToRemove = new ArrayList<>();
|
||||
for (Ability ability : card.getAbilities(game)) {
|
||||
if (ability instanceof SuspendAbility && (((SuspendAbility) ability).isGainedTemporary())) {
|
||||
abilitiesToRemove.add(ability);
|
||||
}
|
||||
}
|
||||
if (!abilitiesToRemove.isEmpty()) {
|
||||
for (Ability ability : card.getAbilities(game)) {
|
||||
if (ability instanceof SuspendBeginningOfUpkeepInterveningIfTriggeredAbility
|
||||
|| ability instanceof SuspendPlayCardAbility) {
|
||||
abilitiesToRemove.add(ability);
|
||||
}
|
||||
}
|
||||
// remove the abilities from the card
|
||||
// TODO: will not work with Adventure Cards and another auto-generated abilities list
|
||||
// TODO: is it work after blink or return to hand?
|
||||
/*
|
||||
bug example:
|
||||
Epochrasite bug: It comes out of suspend, is cast and enters the battlefield. THEN if it's returned to
|
||||
its owner's hand from battlefield, the bounced Epochrasite can't be cast for the rest of the game.
|
||||
*/
|
||||
card.getAbilities().removeAll(abilitiesToRemove);
|
||||
}
|
||||
// cast the card for free
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
|
||||
boolean cardWasCast = player.cast(player.chooseAbilityForCast(card, game, true),
|
||||
game, true, new ApprovingObject(source, game));
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
|
||||
if (cardWasCast && (card.isCreature(game))) {
|
||||
// creatures cast from suspend gain haste
|
||||
if ((card.isCreature(game))) {
|
||||
ContinuousEffect effect = new GainHasteEffect();
|
||||
effect.setTargetPointer(new FixedTarget(card.getId(), card.getZoneChangeCounter(game) + 1));
|
||||
game.addEffect(effect, source);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue