mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
Recover abilities - fixed that it doesn't ask to pay a cost on multiple triggers;
This commit is contained in:
parent
6d55e4b9e6
commit
57ef74da90
3 changed files with 120 additions and 38 deletions
|
|
@ -61,15 +61,16 @@ public class ExploitTest extends CardTestPlayerBase {
|
||||||
addCard(Zone.HAND, playerB, "Lightning Bolt", 1);
|
addCard(Zone.HAND, playerB, "Lightning Bolt", 1);
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Thundering Giant"); // 4/3
|
addCard(Zone.BATTLEFIELD, playerB, "Thundering Giant"); // 4/3
|
||||||
|
|
||||||
setStrictChooseMode(true);
|
|
||||||
|
|
||||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Silumgar Butcher");
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Silumgar Butcher");
|
||||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
|
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
|
||||||
setChoice(playerA, true); // Choose to exploit
|
setChoice(playerA, true); // Choose to exploit
|
||||||
setChoice(playerA, "Silvercoat Lion"); // sacrifice to Exploit
|
setChoice(playerA, "Silvercoat Lion"); // sacrifice to Exploit
|
||||||
|
|
||||||
|
// kill butcher before exploit trigger resolve, so no exploits trigger with target
|
||||||
|
// if you failed here then something wrong with isInUseableZone
|
||||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silumgar Butcher");
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silumgar Butcher");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
package org.mage.test.cards.abilities.keywords;
|
package org.mage.test.cards.abilities.keywords;
|
||||||
|
|
||||||
import mage.constants.PhaseStep;
|
import mage.constants.PhaseStep;
|
||||||
|
|
@ -7,8 +6,7 @@ import org.junit.Test;
|
||||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @author LevelX2, JayDi85
|
||||||
* @author LevelX2
|
|
||||||
*/
|
*/
|
||||||
public class RecoverTest extends CardTestPlayerBase {
|
public class RecoverTest extends CardTestPlayerBase {
|
||||||
|
|
||||||
|
|
@ -20,7 +18,7 @@ public class RecoverTest extends CardTestPlayerBase {
|
||||||
* Otherwise, exile this card.”
|
* Otherwise, exile this card.”
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testReturnToHand() {
|
public void test_Normal_ToHand() {
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
|
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
|
||||||
// You gain 4 life.
|
// You gain 4 life.
|
||||||
// Recover {1}{W}
|
// Recover {1}{W}
|
||||||
|
|
@ -35,6 +33,7 @@ public class RecoverTest extends CardTestPlayerBase {
|
||||||
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion");
|
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion");
|
||||||
setChoice(playerA, true);
|
setChoice(playerA, true);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
setStopAt(1, PhaseStep.END_TURN);
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
|
|
@ -49,7 +48,7 @@ public class RecoverTest extends CardTestPlayerBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGoingToExile() {
|
public void test_Normal_ToExile() {
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
|
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
|
||||||
// You gain 4 life.
|
// You gain 4 life.
|
||||||
// Recover {1}{W}
|
// Recover {1}{W}
|
||||||
|
|
@ -64,6 +63,7 @@ public class RecoverTest extends CardTestPlayerBase {
|
||||||
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion");
|
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion");
|
||||||
setChoice(playerA, false);
|
setChoice(playerA, false);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
setStopAt(1, PhaseStep.END_TURN);
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
|
|
@ -74,6 +74,111 @@ public class RecoverTest extends CardTestPlayerBase {
|
||||||
assertGraveyardCount(playerA, "Silvercoat Lion", 1);
|
assertGraveyardCount(playerA, "Silvercoat Lion", 1);
|
||||||
|
|
||||||
assertLife(playerA, 24);
|
assertLife(playerA, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_DieOther_Single_CanRecover() {
|
||||||
|
addCustomEffect_TargetDestroy(playerA, 1);
|
||||||
|
|
||||||
|
// Recover—Pay half your life, rounded up.
|
||||||
|
addCard(Zone.GRAVEYARD, playerA, "Garza's Assassin");
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1);
|
||||||
|
|
||||||
|
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target destroy");
|
||||||
|
addTarget(playerA, "Silvercoat Lion");
|
||||||
|
setChoice(playerA, true); // pay half life
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertHandCount(playerA, "Garza's Assassin", 1); // after recover
|
||||||
|
assertLife(playerA, 20 / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_DieOther_Multiple_CanRecover() {
|
||||||
|
// ruling from wiki:
|
||||||
|
// If multiple creatures are put into your graveyard from the battlefield at the same time, the recover
|
||||||
|
// ability of a card already in your graveyard triggers that many times. Only the first one to resolve
|
||||||
|
// will cause the card to move somewhere. By the time any of the other triggers resolve, the card won't be
|
||||||
|
// in your graveyard anymore. You can still pay the recover cost, but nothing else will happen.
|
||||||
|
|
||||||
|
addCustomEffect_TargetDestroy(playerA, 2);
|
||||||
|
|
||||||
|
// Recover—Pay half your life, rounded up.
|
||||||
|
addCard(Zone.GRAVEYARD, playerA, "Garza's Assassin");
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
|
||||||
|
|
||||||
|
// raise 2 recover triggers, pay second trigger - it will be fizzled
|
||||||
|
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target destroy");
|
||||||
|
addTarget(playerA, "Silvercoat Lion^Grizzly Bears");
|
||||||
|
setChoice(playerA, "Recover—Pay half your life"); // x2 triggers order
|
||||||
|
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
|
||||||
|
checkStackObject("on recover triggers", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Recover—Pay half your life", 2);
|
||||||
|
setChoice(playerA, false); // first trigger resolve - do not pay and exile
|
||||||
|
setChoice(playerA, true); // second trigger resolve - pay and fizzle
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertExileCount(playerA, "Garza's Assassin", 1); // after first unpayed trigger
|
||||||
|
assertLife(playerA, 20 / 2); // after second unpayed trigger
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_DieItself_MustNotWork() {
|
||||||
|
// ruling from wiki:
|
||||||
|
// If a creature with recover is put into your graveyard from the battlefield, it doesn't cause its
|
||||||
|
// own recover ability to trigger. Similarly, if another creature is put into your graveyard from
|
||||||
|
// the battlefield at the same time that a card with recover is put there, it won't cause that
|
||||||
|
// recover ability to trigger.
|
||||||
|
|
||||||
|
addCustomEffect_TargetDestroy(playerA, 1);
|
||||||
|
|
||||||
|
// Recover—Pay half your life, rounded up.
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Garza's Assassin");
|
||||||
|
|
||||||
|
// no recover
|
||||||
|
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target destroy");
|
||||||
|
addTarget(playerA, "Garza's Assassin");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, "Garza's Assassin", 1);
|
||||||
|
assertLife(playerA, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_DieItselfAndMultiple_MustNotWork() {
|
||||||
|
// ruling from wiki:
|
||||||
|
// If a creature with recover is put into your graveyard from the battlefield, it doesn't cause its
|
||||||
|
// own recover ability to trigger. Similarly, if another creature is put into your graveyard from
|
||||||
|
// the battlefield at the same time that a card with recover is put there, it won't cause that
|
||||||
|
// recover ability to trigger.
|
||||||
|
|
||||||
|
// reason: it's leaves-the-battlefield trigger and look back in time (source was on battlefield in that time, so no trigger)
|
||||||
|
|
||||||
|
addCustomEffect_TargetDestroy(playerA, 2);
|
||||||
|
|
||||||
|
// Recover—Pay half your life, rounded up.
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Garza's Assassin");
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1);
|
||||||
|
|
||||||
|
// no recover (if you catch recover dialog then something wrong with isInUseableZone)
|
||||||
|
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target destroy");
|
||||||
|
addTarget(playerA, "Garza's Assassin^Silvercoat Lion");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, "Garza's Assassin", 1);
|
||||||
|
assertGraveyardCount(playerA, "Silvercoat Lion", 1);
|
||||||
|
assertLife(playerA, 20);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
|
|
||||||
package mage.abilities.keyword;
|
package mage.abilities.keyword;
|
||||||
|
|
||||||
import mage.abilities.Ability;
|
|
||||||
import mage.abilities.TriggeredAbilityImpl;
|
import mage.abilities.TriggeredAbilityImpl;
|
||||||
import mage.abilities.costs.Cost;
|
import mage.abilities.costs.Cost;
|
||||||
import mage.abilities.costs.mana.ManaCost;
|
import mage.abilities.costs.mana.ManaCost;
|
||||||
import mage.abilities.effects.OneShotEffect;
|
import mage.abilities.effects.common.DoIfCostPaid;
|
||||||
import mage.abilities.effects.common.ExileSourceEffect;
|
import mage.abilities.effects.common.ExileSourceEffect;
|
||||||
import mage.abilities.effects.common.ReturnToHandSourceEffect;
|
import mage.abilities.effects.common.ReturnToHandSourceEffect;
|
||||||
import mage.cards.Card;
|
import mage.cards.Card;
|
||||||
import mage.constants.Outcome;
|
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
import mage.game.Game;
|
import mage.game.Game;
|
||||||
import mage.game.events.GameEvent;
|
import mage.game.events.GameEvent;
|
||||||
import mage.game.events.ZoneChangeEvent;
|
import mage.game.events.ZoneChangeEvent;
|
||||||
import mage.players.Player;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 702.58a Recover is a triggered ability that functions only while the card
|
* 702.58a Recover is a triggered ability that functions only while the card
|
||||||
|
|
@ -28,7 +24,8 @@ import mage.players.Player;
|
||||||
public class RecoverAbility extends TriggeredAbilityImpl {
|
public class RecoverAbility extends TriggeredAbilityImpl {
|
||||||
|
|
||||||
public RecoverAbility(Cost cost, Card card) {
|
public RecoverAbility(Cost cost, Card card) {
|
||||||
super(Zone.GRAVEYARD, new RecoverEffect(cost, card.isCreature()), false);
|
super(Zone.GRAVEYARD, new RecoverEffect(cost, card), false);
|
||||||
|
setLeavesTheBattlefieldTrigger(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected RecoverAbility(final RecoverAbility ability) {
|
protected RecoverAbility(final RecoverAbility ability) {
|
||||||
|
|
@ -64,19 +61,15 @@ public class RecoverAbility extends TriggeredAbilityImpl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RecoverEffect extends OneShotEffect {
|
class RecoverEffect extends DoIfCostPaid {
|
||||||
|
|
||||||
protected Cost cost;
|
public RecoverEffect(Cost cost, Card card) {
|
||||||
|
super(new ReturnToHandSourceEffect(), new ExileSourceEffect(), cost);
|
||||||
public RecoverEffect(Cost cost, boolean creature) {
|
this.staticText = setText(cost, card.isCreature());
|
||||||
super(Outcome.ReturnToHand);
|
|
||||||
this.cost = cost;
|
|
||||||
this.staticText = setText(cost, creature);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected RecoverEffect(final RecoverEffect effect) {
|
protected RecoverEffect(final RecoverEffect effect) {
|
||||||
super(effect);
|
super(effect);
|
||||||
this.cost = effect.cost.copy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -84,23 +77,6 @@ class RecoverEffect extends OneShotEffect {
|
||||||
return new RecoverEffect(this);
|
return new RecoverEffect(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean apply(Game game, Ability source) {
|
|
||||||
Player controller = game.getPlayer(source.getControllerId());
|
|
||||||
Card sourceCard = game.getCard(source.getSourceId());
|
|
||||||
if (controller != null && sourceCard != null
|
|
||||||
&& game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) {
|
|
||||||
if (controller.chooseUse(Outcome.Damage, "Pay " + cost.getText() + " to recover " + sourceCard.getLogName() + "? (Otherwise the card will be exiled)", source, game)) {
|
|
||||||
cost.clearPaid();
|
|
||||||
if (cost.pay(source, game, source, controller.getId(), false, null)) {
|
|
||||||
return new ReturnToHandSourceEffect().apply(game, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new ExileSourceEffect().apply(game, source);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String setText(Cost cost, boolean creature) {
|
private String setText(Cost cost, boolean creature) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("Recover");
|
sb.append("Recover");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue