PaidLists now store if the CostPart source is linked to host

This commit is contained in:
tool4EvEr
2023-03-29 17:33:51 +02:00
parent 99f8cca233
commit 101306e61c
20 changed files with 125 additions and 113 deletions

View File

@@ -172,7 +172,7 @@ public class PlayAi extends SpellAbilityAi {
abCost = new Cost(sa.getParam("PlayCost"), false);
}
spell = (Spell) spell.copyWithDefinedCost(abCost);
spell = (Spell) spell.copyWithManaCostReplaced(spell.getActivatingPlayer(), abCost);
}
if (AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !(isOptional || sa.hasParam("Optional")), true)) {
// Before accepting, see if the spell has a valid number of targets (it should at this point).

View File

@@ -144,7 +144,7 @@ public class AbilityUtils {
c = hostCard.getEnchantingCard();
if (c == null && sa instanceof SpellAbility) {
SpellAbility root = ((SpellAbility)sa).getRootAbility();
CardCollection sacrificed = root.getPaidList("Sacrificed");
CardCollection sacrificed = root.getPaidList("Sacrificed", true);
if (sacrificed != null && !sacrificed.isEmpty()) {
c = sacrificed.getFirst().getEnchantingCard();
}
@@ -742,29 +742,29 @@ public class AbilityUtils {
} else { // these ones only for handling lists
Iterable<Card> list = null;
if (calcX[0].startsWith("Sacrificed")) {
list = sa.getRootAbility().getPaidList("Sacrificed");
list = sa.getRootAbility().getPaidList("Sacrificed", true);
}
else if (calcX[0].startsWith("Discarded")) {
final SpellAbility root = sa.getRootAbility();
list = root.getPaidList("Discarded");
list = root.getPaidList("Discarded", true);
if (null == list && root.isTrigger()) {
list = root.getHostCard().getSpellPermanent().getPaidList("Discarded");
list = root.getHostCard().getSpellPermanent().getPaidList("Discarded", true);
}
}
else if (calcX[0].startsWith("Exiled")) {
list = sa.getRootAbility().getPaidList("Exiled");
list = sa.getRootAbility().getPaidList("Exiled", true);
}
else if (calcX[0].startsWith("Milled")) {
list = sa.getRootAbility().getPaidList("Milled");
list = sa.getRootAbility().getPaidList("Milled", true);
}
else if (calcX[0].startsWith("Tapped")) {
list = sa.getRootAbility().getPaidList("Tapped");
list = sa.getRootAbility().getPaidList("Tapped", true);
}
else if (calcX[0].startsWith("Revealed")) {
list = sa.getRootAbility().getPaidList("Revealed");
list = sa.getRootAbility().getPaidList("Revealed", true);
}
else if (calcX[0].startsWith("Returned")) {
list = sa.getRootAbility().getPaidList("Returned");
list = sa.getRootAbility().getPaidList("Returned", true);
}
else if (calcX[0].startsWith("Targeted")) {
list = sa.findTargetedCards();
@@ -1606,40 +1606,40 @@ public class AbilityUtils {
if (sa.hasParam("RememberCostCards") && !sa.getPaidHash().isEmpty()) {
List <Card> noList = Lists.newArrayList();
Map<String, CardCollection> paidLists = sa.getPaidHash();
Table<String, Boolean, CardCollection> paidLists = sa.getPaidHash();
if (sa.hasParam("RememberCostExcept")) {
noList.addAll(AbilityUtils.getDefinedCards(host, sa.getParam("RememberCostExcept"), sa));
}
if (paidLists.containsKey("Exiled")) {
final CardCollection paidListExiled = sa.getPaidList("Exiled");
if (paidLists.contains("Exiled", true)) {
final CardCollection paidListExiled = sa.getPaidList("Exiled", true);
for (final Card exiledAsCost : paidListExiled) {
if (!noList.contains(exiledAsCost)) {
host.addRemembered(exiledAsCost);
}
}
} else if (paidLists.containsKey("Sacrificed")) {
final CardCollection paidListSacrificed = sa.getPaidList("Sacrificed");
} else if (paidLists.contains("Sacrificed", true)) {
final CardCollection paidListSacrificed = sa.getPaidList("Sacrificed", true);
for (final Card sacrificedAsCost : paidListSacrificed) {
if (!noList.contains(sacrificedAsCost)) {
host.addRemembered(sacrificedAsCost);
}
}
} else if (paidLists.containsKey("Tapped")) {
final CardCollection paidListTapped = sa.getPaidList("Tapped");
} else if (paidLists.contains("Tapped", true)) {
final CardCollection paidListTapped = sa.getPaidList("Tapped", true);
for (final Card tappedAsCost : paidListTapped) {
if (!noList.contains(tappedAsCost)) {
host.addRemembered(tappedAsCost);
}
}
} else if (paidLists.containsKey("Unattached")) {
final CardCollection paidListUnattached = sa.getPaidList("Unattached");
} else if (paidLists.contains("Unattached", true)) {
final CardCollection paidListUnattached = sa.getPaidList("Unattached", true);
for (final Card unattachedAsCost : paidListUnattached) {
if (!noList.contains(unattachedAsCost)) {
host.addRemembered(unattachedAsCost);
}
}
} else if (paidLists.containsKey("Discarded")) {
final CardCollection paidListDiscarded = sa.getPaidList("Discarded");
} else if (paidLists.contains("Discarded", true)) {
final CardCollection paidListDiscarded = sa.getPaidList("Discarded", true);
for (final Card discardedAsCost : paidListDiscarded) {
if (!noList.contains(discardedAsCost)) {
host.addRemembered(discardedAsCost);
@@ -3860,29 +3860,29 @@ public class AbilityUtils {
SpellAbility root = ((SpellAbility)sa).getRootAbility();
// TODO do we really need these checks?
if (defined.startsWith("SacrificedCards")) {
list = root.getPaidList("SacrificedCards");
list = root.getPaidList("SacrificedCards", true);
} else if (defined.startsWith("Sacrificed")) {
list = root.getPaidList("Sacrificed");
list = root.getPaidList("Sacrificed", true);
} else if (defined.startsWith("Revealed")) {
list = root.getPaidList("Revealed");
list = root.getPaidList("Revealed", true);
} else if (defined.startsWith("DiscardedCards")) {
list = root.getPaidList("DiscardedCards");
list = root.getPaidList("DiscardedCards", true);
} else if (defined.startsWith("Discarded")) {
list = root.getPaidList("Discarded");
list = root.getPaidList("Discarded", true);
} else if (defined.startsWith("ExiledCards")) {
list = root.getPaidList("ExiledCards");
list = root.getPaidList("ExiledCards", true);
} else if (defined.startsWith("Exiled")) {
list = root.getPaidList("Exiled");
list = root.getPaidList("Exiled", true);
} else if (defined.startsWith("Milled")) {
list = root.getPaidList("Milled");
list = root.getPaidList("Milled", true);
} else if (defined.startsWith("TappedCards")) {
list = root.getPaidList("TappedCards");
list = root.getPaidList("TappedCards", true);
} else if (defined.startsWith("Tapped")) {
list = root.getPaidList("Tapped");
list = root.getPaidList("Tapped", true);
} else if (defined.startsWith("UntappedCards")) {
list = root.getPaidList("UntappedCards");
list = root.getPaidList("UntappedCards", true);
} else if (defined.startsWith("Untapped")) {
list = root.getPaidList("Untapped");
list = root.getPaidList("Untapped", true);
}
}
return list;

View File

@@ -696,7 +696,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
if (sa.isNinjutsu()) {
// Ninjutsu need to get the Defender of the Returned Creature
final Card returned = sa.getPaidList("Returned").getFirst();
final Card returned = sa.getPaidList("Returned", true).getFirst();
final GameEntity defender = game.getCombat().getDefenderByAttacker(returned);
game.getCombat().addAttacker(movedCard, defender);
game.getCombat().getBandOfAttacker(movedCard).setBlocked(false);

View File

@@ -371,7 +371,7 @@ public class PlayEffect extends SpellAbilityEffect {
abCost = new Cost(cost, false);
}
tgtSA = tgtSA.copyWithDefinedCost(abCost);
tgtSA = tgtSA.copyWithManaCostReplaced(tgtSA.getActivatingPlayer(), abCost);
}
if (!optional) {

View File

@@ -5847,7 +5847,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
timesCrewedThisTurn += 1;
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Vehicle, this);
runParams.put(AbilityKey.Crew, sa.getPaidList("TappedCards"));
runParams.put(AbilityKey.Crew, sa.getPaidList("TappedCards", true));
game.getTriggerHandler().runTrigger(TriggerType.BecomesCrewed, runParams, false);
}

View File

@@ -866,9 +866,9 @@ public class CardProperty {
} else if (restriction.equals("MovedToGrave")) {
if (!(spellAbility instanceof SpellAbility)) {
final SpellAbility root = ((SpellAbility) spellAbility).getRootAbility();
if (root != null && (root.getPaidList("MovedToGrave") != null)
&& !root.getPaidList("MovedToGrave").isEmpty()) {
final CardCollectionView cards = root.getPaidList("MovedToGrave");
if (root != null && (root.getPaidList("MovedToGrave", true) != null)
&& !root.getPaidList("MovedToGrave", true).isEmpty()) {
final CardCollectionView cards = root.getPaidList("MovedToGrave", true);
for (final Card c : cards) {
String name = c.getName();
if (StringUtils.isEmpty(name)) {

View File

@@ -210,6 +210,10 @@ public class Cost implements Serializable {
costParts.add(new CostPartMana(cost, null));
}
public Cost(String parse, final boolean bAbility) {
this(parse, bAbility, true);
}
/**
* <p>
* Constructor for Cost.
@@ -219,7 +223,7 @@ public class Cost implements Serializable {
* @param bAbility
* a boolean.
*/
public Cost(String parse, final boolean bAbility) {
public Cost(String parse, final boolean bAbility, final boolean intrinsic) {
this.isAbility = bAbility;
// when adding new costs for cost string, place them here
@@ -249,6 +253,9 @@ public class Cost implements Serializable {
if (cp instanceof CostPartMana) {
parsedMana = (CostPartMana) cp;
} else {
if (cp instanceof CostPartWithList) {
((CostPartWithList)cp).setIntrinsic(intrinsic);
}
this.costParts.add(cp);
}
else

View File

@@ -161,7 +161,7 @@ public class CostAdjustment {
count = 1;
}
if (count > 0) {
Cost part = new Cost(scost, sa.isAbility());
Cost part = new Cost(scost, sa.isAbility(), sa.getHostCard().equals(hostCard));
cost.mergeTo(part, count);
}
}

View File

@@ -105,7 +105,7 @@ public class CostExileFromStack extends CostPart {
public final boolean payAsDecided(final Player ai, final PaymentDecision decision, SpellAbility ability, final boolean effect) {
Game game = ai.getGame();
for (final SpellAbility sa : decision.sp) {
ability.addCostToHashList(CardUtil.getLKICopy(sa.getHostCard()), "Exiled");
ability.addCostToHashList(CardUtil.getLKICopy(sa.getHostCard()), "Exiled", true);
SpellAbilityStackInstance si = game.getStack().getInstanceFromSpellAbility(sa);
if (si != null) {
game.getStack().remove(si);

View File

@@ -94,7 +94,7 @@ public class CostMill extends CostPart {
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
moveParams.put(AbilityKey.LastStateBattlefield, ai.getGame().getLastStateBattlefield());
moveParams.put(AbilityKey.LastStateGraveyard, ai.getGame().getLastStateGraveyard());
ability.getPaidHash().put("Milled", (CardCollection) ai.mill(decision.c, ZoneType.Graveyard, false, ability, table, moveParams));
ability.getPaidHash().put("Milled", true, (CardCollection) ai.mill(decision.c, ZoneType.Graveyard, false, ability, table, moveParams));
table.triggerChangesZoneAll(ai.getGame(), ability);
return true;
}

View File

@@ -127,9 +127,9 @@ public class CostPartMana extends CostPart {
}
public ManaCost getManaCostFor(SpellAbility sa) {
if (isExiledCreatureCost() && sa.getPaidList(CostExile.HashLKIListKey)!= null && !sa.getPaidList(CostExile.HashLKIListKey).isEmpty()) {
if (isExiledCreatureCost() && sa.getPaidList(CostExile.HashLKIListKey, true)!= null && !sa.getPaidList(CostExile.HashLKIListKey, true).isEmpty()) {
// back from the brink
return sa.getPaidList(CostExile.HashLKIListKey).get(0).getManaCost();
return sa.getPaidList(CostExile.HashLKIListKey, true).get(0).getManaCost();
}
if (isEnchantedCreatureCost() && sa.getHostCard().isEnchantingCard()) {
return sa.getHostCard().getEnchantingCard().getManaCost();

View File

@@ -38,6 +38,8 @@ public abstract class CostPartWithList extends CostPart {
private final CardCollection lkiList = new CardCollection();
protected final CardCollection cardList = new CardCollection();
private boolean intrinsic = true;
protected final CardZoneTable table = new CardZoneTable();
// set is here because executePayment() adds card to list, while ai's decide payment does the same thing.
// set allows to avoid duplication
@@ -50,6 +52,10 @@ public abstract class CostPartWithList extends CostPart {
return cardList;
}
public final void setIntrinsic(boolean b) {
intrinsic = b;
}
/**
* Reset list.
*/
@@ -71,11 +77,11 @@ public abstract class CostPartWithList extends CostPart {
}
final String lkiPaymentMethod = getHashForLKIList();
for (final Card card : lkiList) {
sa.addCostToHashList(card, lkiPaymentMethod);
sa.addCostToHashList(card, lkiPaymentMethod, intrinsic);
}
final String cardPaymentMethod = getHashForCardList();
for (final Card card : cardList) {
sa.addCostToHashList(card, cardPaymentMethod);
sa.addCostToHashList(card, cardPaymentMethod, intrinsic);
}
}

View File

@@ -147,7 +147,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private List<SpellAbility> paidAbilities = Lists.newArrayList();
private Integer xManaCostPaid = null;
private HashMap<String, CardCollection> paidLists = Maps.newHashMap();
private TreeBasedTable<String, Boolean, CardCollection> paidLists = TreeBasedTable.create();
private EnumMap<AbilityKey, Object> triggeringObjects = AbilityKey.newMap();
@@ -688,22 +688,29 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
}
// Combined PaidLists
public Map<String, CardCollection> getPaidHash() {
public TreeBasedTable<String, Boolean, CardCollection> getPaidHash() {
return paidLists;
}
public void setPaidHash(final Map<String, CardCollection> hash) {
paidLists = Maps.newHashMap(hash);
public void setPaidHash(final TreeBasedTable<String, Boolean, CardCollection> hash) {
paidLists = TreeBasedTable.create(hash);
}
public CardCollection getPaidList(final String str) {
return paidLists.get(str);
// use if it doesn't matter if payment was caused by extrinsic cost modifier
public Iterable<Card> getPaidList(final String str) {
return Iterables.concat(paidLists.row(str).values());
}
public void addCostToHashList(final Card c, final String str) {
if (!paidLists.containsKey(str)) {
paidLists.put(str, new CardCollection());
public CardCollection getPaidList(final String str, final boolean intrinsic) {
return paidLists.get(str, intrinsic);
}
paidLists.get(str).add(c);
public void addCostToHashList(final Card c, final String str, final boolean intrinsic) {
if (!paidLists.contains(str, intrinsic)) {
paidLists.put(str, intrinsic, new CardCollection());
}
paidLists.get(str, intrinsic).add(c);
}
public void resetPaidHash() {
paidLists.clear();
}
@@ -1364,67 +1371,56 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
soFar += c.getNetPower();
}
if (soFar > tr.getMaxTotalPower(getHostCard(),this)) {
if (soFar > tr.getMaxTotalPower(getHostCard(), this)) {
return false;
}
}
if (tr.isSameController()) {
if (tr.isSameController() && entity instanceof Card) {
Player newController;
if (entity instanceof Card) {
newController = ((Card) entity).getController();
for (final Card c : targetChosen.getTargetCards()) {
if (entity != c && !c.getController().equals(newController))
return false;
}
}
}
if (tr.isDifferentControllers()) {
if (tr.isDifferentControllers() && entity instanceof Card) {
Player newController;
if (entity instanceof Card) {
newController = ((Card) entity).getController();
for (final Card c : targetChosen.getTargetCards()) {
if (entity != c && c.getController().equals(newController))
return false;
}
}
}
if (tr.isWithoutSameCreatureType()) {
if (entity instanceof Card) {
if (tr.isWithoutSameCreatureType() && entity instanceof Card) {
for (final Card c : targetChosen.getTargetCards()) {
if (entity != c && c.sharesCreatureTypeWith((Card) entity)) {
return false;
}
}
}
}
if (tr.isWithSameCreatureType()) {
if (entity instanceof Card) {
if (tr.isWithSameCreatureType() && entity instanceof Card) {
for (final Card c : targetChosen.getTargetCards()) {
if (entity != c && !c.sharesCreatureTypeWith((Card) entity)) {
return false;
}
}
}
}
if (tr.isWithSameCardType()) {
if (entity instanceof Card) {
if (tr.isWithSameCardType() && entity instanceof Card) {
for (final Card c : targetChosen.getTargetCards()) {
if (entity != c && !c.sharesCardTypeWith((Card) entity)) {
return false;
}
}
}
}
if (entity instanceof GameEntity) {
String[] validTgt = tr.getValidTgts();
GameEntity e = (GameEntity)entity;
if (!e.isValid(validTgt, getActivatingPlayer(), getHostCard(), this)) {
if (!e.isValid(tr.getValidTgts(), getActivatingPlayer(), getHostCard(), this)) {
return false;
}
if (hasParam("TargetType") && !e.isValid(getParam("TargetType").split(","), getActivatingPlayer(), getHostCard(), this)) {

View File

@@ -27,6 +27,7 @@ import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.TreeBasedTable;
import forge.game.GameObject;
import forge.game.IIdentifiable;
@@ -85,7 +86,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView {
private Integer xManaPaid = null;
// Other Paid things
private final Map<String, CardCollection> paidHash;
private final TreeBasedTable<String, Boolean, CardCollection> paidHash;
// Additional info
// is Kicked, is Buyback
@@ -108,7 +109,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView {
activatingPlayer = sa.getActivatingPlayer();
// Payment info
paidHash = Maps.newHashMap(ability.getPaidHash());
paidHash = TreeBasedTable.create(ability.getPaidHash());
ability.resetPaidHash();
splicedCards = sa.getSplicedCards();

View File

@@ -961,7 +961,7 @@ public final class StaticAbilityContinuous {
AbilityUtils.getDefinedPlayers(affectedCard, params.get("MayPlayPlayer"), stAb).get(0) :
controller;
affectedCard.setMayPlay(mayPlayController, mayPlayWithoutManaCost,
mayPlayAltCost != null ? new Cost(mayPlayAltCost, false) : null, additional, mayPlayWithFlash,
mayPlayAltCost != null ? new Cost(mayPlayAltCost, false, affectedCard.equals(hostCard)) : null, additional, mayPlayWithFlash,
mayPlayGrantZonePermissions, stAb);
// If the MayPlay effect only affected itself, check if it is in graveyard and give other player who cast Shaman's Trance MayPlay

View File

@@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -404,8 +405,7 @@ public abstract class Trigger extends TriggerReplacementBase {
}
} else if ("Sacrificed".equals(condition)) {
final SpellAbility trigSA = (SpellAbility) runParams.get(AbilityKey.CastSA);
if (trigSA != null &&
(trigSA.getPaidList("Sacrificed") == null || trigSA.getPaidList("Sacrificed").isEmpty())) {
if (trigSA != null && Iterables.isEmpty(trigSA.getPaidList("Sacrificed"))) {
return false;
}
} else if ("AttackedPlayerWithMostLife".equals(condition)) {

View File

@@ -7,6 +7,7 @@ import java.util.Set;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.TreeBasedTable;
import forge.card.mana.ManaCost;
import forge.game.Game;
@@ -108,23 +109,23 @@ public class WrappedAbility extends Ability {
}
@Override
public void setPaidHash(final Map<String, CardCollection> hash) {
public void setPaidHash(final TreeBasedTable<String, Boolean, CardCollection> hash) {
sa.setPaidHash(hash);
}
@Override
public Map<String, CardCollection> getPaidHash() {
public TreeBasedTable<String, Boolean, CardCollection> getPaidHash() {
return sa.getPaidHash();
}
@Override
public CardCollection getPaidList(final String str) {
return sa.getPaidList(str);
public CardCollection getPaidList(final String str, boolean intrinsic) {
return sa.getPaidList(str, intrinsic);
}
@Override
public void addCostToHashList(final Card c, final String str) {
sa.addCostToHashList(c, str);
public void addCostToHashList(final Card c, final String str, final boolean intrinsic) {
sa.addCostToHashList(c, str, intrinsic);
}
@Override

View File

@@ -362,7 +362,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
if (sp.hasParam("Crew")) {
// Trigger crews!
runParams.put(AbilityKey.Vehicle, sp.getHostCard());
runParams.put(AbilityKey.Crew, sp.getPaidList("TappedCards"));
runParams.put(AbilityKey.Crew, sp.getPaidList("TappedCards", true));
game.getTriggerHandler().runTrigger(TriggerType.Crewed, runParams, false);
}
} else {

View File

@@ -1347,10 +1347,10 @@ public final class CMatchUI
enchantedEntityView = enchantedEntity.getView();
numSmallImages++;
} else if ((sa.getRootAbility() != null)
&& (sa.getRootAbility().getPaidList("Sacrificed") != null)
&& !sa.getRootAbility().getPaidList("Sacrificed").isEmpty()) {
&& (sa.getRootAbility().getPaidList("Sacrificed", true) != null)
&& !sa.getRootAbility().getPaidList("Sacrificed", true).isEmpty()) {
// If the player activated its ability by sacrificing the enchantment, the enchantment has not anything attached anymore and the ex-enchanted card has to be searched in other ways.. for example, the green enchantment "Carapace"
enchantedEntity = sa.getRootAbility().getPaidList("Sacrificed").get(0).getEnchantingCard();
enchantedEntity = sa.getRootAbility().getPaidList("Sacrificed", true).get(0).getEnchantingCard();
if (enchantedEntity != null) {
enchantedEntityView = enchantedEntity.getView();
numSmallImages++;

View File

@@ -1,8 +1,9 @@
Name:Barrin's Spite
ManaCost:2 U B
Types:Sorcery
A:SP$ Pump | Cost$ 2 U B | ValidTgts$ Creature | TgtPrompt$ Choose two target creatures controlled by the same player | TargetMin$ 2 | TargetMax$ 2 | TargetUnique$ True | TargetsWithSameController$ True | IsCurse$ True | SubAbility$ DBChooseSac | StackDescription$ SpellDescription | SpellDescription$ Choose two target creatures controlled by the same player. Their controller chooses and sacrifices one of them. Return the other to its owner's hand.
SVar:DBChooseSac:DB$ ChooseCard | DefinedCards$ Targeted | Defined$ TargetedController | ChoiceTitle$ Choose one to sacrifice | SubAbility$ DBSac | StackDescription$ None | AILogic$ WorstCard
A:SP$ Pump | Cost$ 2 U B | ValidTgts$ Creature | TgtPrompt$ Choose two target creatures controlled by the same player | TargetMin$ 2 | TargetMax$ 2 | TargetUnique$ True | TargetsWithSameController$ True | RememberTargets$ True | IsCurse$ True | SubAbility$ DBChooseSac | StackDescription$ SpellDescription | SpellDescription$ Choose two target creatures controlled by the same player. Their controller chooses and sacrifices one of them. Return the other to its owner's hand.
SVar:DBChooseSac:DB$ ChooseCard | Choices$ Card.IsRemembered | Defined$ TargetedController | ChoiceTitle$ Choose one to sacrifice | ForgetChosen$ True | SubAbility$ DBSac | StackDescription$ None | AILogic$ WorstCard
SVar:DBSac:DB$ Destroy | Defined$ ChosenCard | Sacrifice$ True | SubAbility$ DBBounce | StackDescription$ None
SVar:DBBounce:DB$ ChangeZone | Defined$ Targeted | Origin$ Battlefield | Destination$ Hand | StackDescription$ None
SVar:DBBounce:DB$ ChangeZone | Defined$ Remembered | Origin$ Battlefield | Destination$ Hand | SubAbility$ DBCleanup | StackDescription$ None
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
Oracle:Choose two target creatures controlled by the same player. Their controller chooses and sacrifices one of them. Return the other to its owner's hand.