Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;

/**
Expand Down Expand Up @@ -120,4 +121,96 @@ public void RetraceCastFromGraveyard() {
assertGraveyardCount(playerA, "Mountain", 1);

}

@Test
public void test_Retrace_WithEvokeAlternativeCost() {
setStrictChooseMode(true);

addCard(Zone.BATTLEFIELD, playerA, "Six");
addCard(Zone.GRAVEYARD, playerA, "Solitude");
addCard(Zone.HAND, playerA, "Plains");
addCard(Zone.HAND, playerA, "Silvercoat Lion");

castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Solitude with retrace");
setChoice(playerA, "Cast with Evoke alternative cost");
setChoice(playerA, "Silvercoat Lion"); // exile for evoke
setChoice(playerA, "Plains"); // discard for retrace
setChoice(playerA, "When this permanent enters, if its evoke cost was paid, its controller sacrifices it");
addTarget(playerA, TestPlayer.TARGET_SKIP); // no target for Solitude's enters ability

setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

assertExileCount(playerA, "Silvercoat Lion", 1);
assertGraveyardCount(playerA, "Plains", 1);
assertGraveyardCount(playerA, "Solitude", 1); // sacrificed after being evoked
}

@Test
public void test_Retrace_DoesNotUseOtherGraveyardPermission() {
setStrictChooseMode(true);

addCard(Zone.BATTLEFIELD, playerA, "Six");
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
addCard(Zone.GRAVEYARD, playerA, "Demilich");
addCard(Zone.GRAVEYARD, playerA, "Opt", 4);
addCard(Zone.HAND, playerA, "Plains");

castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Demilich with retrace");
setChoice(playerA, "Plains"); // discard for retrace

setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

assertPermanentCount(playerA, "Demilich", 1);
assertGraveyardCount(playerA, "Plains", 1);
assertGraveyardCount(playerA, "Opt", 4);
}

@Test
public void test_Retrace_DemonicEmbraceDoesNotUseOwnGraveyardPermission() {
setStrictChooseMode(true);

addCard(Zone.BATTLEFIELD, playerA, "Six");
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears");
addCard(Zone.GRAVEYARD, playerA, "Demonic Embrace");
addCard(Zone.HAND, playerA, "Plains");
addCard(Zone.HAND, playerA, "Silvercoat Lion");

castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Demonic Embrace with retrace", "Grizzly Bears");
setChoice(playerA, "Plains"); // discard for retrace

setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

assertPermanentCount(playerA, "Demonic Embrace", 1);
assertGraveyardCount(playerA, "Plains", 1);
assertHandCount(playerA, "Silvercoat Lion", 1);
assertLife(playerA, 20);
}

@Test
public void test_Retrace_RonaDoesNotUseOwnGraveyardPermission() {
setStrictChooseMode(true);

addCard(Zone.BATTLEFIELD, playerA, "Six");
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2);
addCard(Zone.GRAVEYARD, playerA, "Rona, Sheoldred's Faithful");
addCard(Zone.HAND, playerA, "Plains");
addCard(Zone.HAND, playerA, "Silvercoat Lion");
addCard(Zone.HAND, playerA, "Grizzly Bears");

castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rona, Sheoldred's Faithful with retrace");
setChoice(playerA, "Plains"); // discard for retrace

setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

assertPermanentCount(playerA, "Rona, Sheoldred's Faithful", 1);
assertGraveyardCount(playerA, "Plains", 1);
assertHandCount(playerA, "Silvercoat Lion", 1);
assertHandCount(playerA, "Grizzly Bears", 1);
}
}
3 changes: 3 additions & 0 deletions Mage/src/main/java/mage/abilities/AbilityImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,9 @@ public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, Set<M
alternativeChosen.activateAlternativeCosts(this, game);
}
// 2. ADDITIONAL COST
if (this instanceof MandatoryAdditionalSourceCosts) {
((MandatoryAdditionalSourceCosts) this).addMandatoryAdditionalCosts(this, game);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smells bed -- why you need special engine change for retrace instead using existing one with OptionalAdditionalSourceCosts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, i read it as Optional always, whereas retrace is required.

}
for (Ability ability : abilities) {
if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) {
((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package mage.abilities.costs;

import mage.abilities.Ability;
import mage.game.Game;

/**
* Interface for abilities that impose a mandatory additional cost on the source.
*/
public interface MandatoryAdditionalSourceCosts extends Ability {

void addMandatoryAdditionalCosts(Ability ability, Game game);
}
70 changes: 58 additions & 12 deletions Mage/src/main/java/mage/abilities/keyword/RetraceAbility.java
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
package mage.abilities.keyword;

import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.MandatoryAdditionalSourceCosts;
import mage.abilities.costs.common.DiscardTargetCost;
import mage.cards.Card;
import mage.constants.SpellAbilityType;
import mage.constants.Zone;
import mage.filter.common.FilterLandCard;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetCardInHand;

import java.util.UUID;

/**
*
* 702.81. Retrace
* 702.81a Retrace is a static ability that functions while the card with retrace is in a player’s
* graveyard. “Retrace” means “You may cast this card from your graveyard by discarding a land
* card as an additional cost to cast it.” Casting a spell using its retrace ability follows the rules for
* paying additional costs in rules 601.2b and 601.2f–h.
*
* @author LevelX2
*/
public class RetraceAbility extends SpellAbility {
public class RetraceAbility extends StaticAbility {

public RetraceAbility(Card card) {
super(card.getSpellAbility());
this.newId();
this.setCardName(card.getName() + " with retrace");
zone = Zone.GRAVEYARD;
spellAbilityType = SpellAbilityType.BASE_ALTERNATE;

Cost cost = new DiscardTargetCost(new TargetCardInHand(new FilterLandCard()));
cost.setText("");
this.addCost(cost);

super(Zone.GRAVEYARD, null);
this.addSubAbility(new RetraceSpellAbility(card));
this.setRuleAtTheTop(true);
}

Expand All @@ -47,3 +52,44 @@ public String getRule() {
return "Retrace <i>(You may cast this card from your graveyard by discarding a land card in addition to paying its other costs.)</i>";
}
}

class RetraceSpellAbility extends SpellAbility implements MandatoryAdditionalSourceCosts {

private static final FilterLandCard filter = new FilterLandCard();

RetraceSpellAbility(Card card) {
super(card.getSpellAbility());
this.newId();
this.setCardName(card.getName() + " with retrace");
this.zone = Zone.GRAVEYARD;
this.setRuleVisible(false);
}

private RetraceSpellAbility(final RetraceSpellAbility ability) {
super(ability);
}

@Override
public RetraceSpellAbility copy() {
return new RetraceSpellAbility(this);
}

@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
ActivationStatus status = super.canActivate(playerId, game);
if (!status.canActivate()) {
return status;
}
Player player = game.getPlayer(playerId);
return player != null && player.getHand().count(filter, game) > 0
? ActivationStatus.withoutApprovingObject(true)
: ActivationStatus.getFalse();
}

@Override
public void addMandatoryAdditionalCosts(Ability ability, Game game) {
Cost cost = new DiscardTargetCost(new TargetCardInHand(filter));
cost.setText("");
ability.addCost(cost);
}
}