Recover abilities - fixed that it doesn't ask to pay a cost on multiple triggers;

This commit is contained in:
Oleg Agafonov 2024-11-30 03:19:24 +04:00
parent 6d55e4b9e6
commit 57ef74da90
3 changed files with 120 additions and 38 deletions

View file

@ -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();

View file

@ -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);
// RecoverPay 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);
// RecoverPay 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);
// RecoverPay 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);
// RecoverPay 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);
} }
} }

View file

@ -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");