diff --git a/Mage.Tests/src/test/java/org/mage/test/rollback/DemonicCollusionTest.java b/Mage.Tests/src/test/java/org/mage/test/rollback/DemonicCollusionTest.java new file mode 100644 index 0000000000..24c5253243 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/rollback/DemonicCollusionTest.java @@ -0,0 +1,78 @@ +package org.mage.test.rollback; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class DemonicCollusionTest extends CardTestPlayerBase { + + // https://github.com/magefree/mage/issues/5835 + + // Demonic Collusion {3}{B}{B} + // Buyback—Discard two cards. (You may discard two cards in addition to any other costs as you cast this spell. + // If you do, put this card into your hand as it resolves.) + + // Search your library for a card and put that card into your hand. Then shuffle your library. + + @Test + public void test_BuybackNormal() { + addCard(Zone.HAND, playerA, "Demonic Collusion", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.HAND, playerA, "Forest", 2); + + checkHandCardCount("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Demonic Collusion", 1); + checkHandCardCount("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest", 2); + + // cast with buyback + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Demonic Collusion"); + setChoice(playerA, "Yes"); // use buyback + setChoice(playerA, "Forest"); // pay + setChoice(playerA, "Forest"); // pay + addTarget(playerA, "Mountain"); // return from lib + + checkHandCardCount("after", 1, PhaseStep.BEGIN_COMBAT, playerA, "Demonic Collusion", 1); + checkHandCardCount("after", 1, PhaseStep.BEGIN_COMBAT, playerA, "Forest", 0); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_BuybackAfterRollback() { + addCard(Zone.HAND, playerA, "Demonic Collusion", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.HAND, playerA, "Forest", 2); + + // turn 1 + checkHandCardCount("before roll", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Demonic Collusion", 1); + checkHandCardCount("before roll", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest", 2); + + // turn 3 - rollback at the end (to the start) + rollbackTurns(3, PhaseStep.PRECOMBAT_MAIN, playerA, 0); + + // turn 5 - check + checkHandCardCount("after roll", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Demonic Collusion", 1); + checkHandCardCount("after roll", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest", 2); + + // cast with buyback + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Demonic Collusion"); + setChoice(playerA, "Yes"); // use buyback + setChoice(playerA, "Forest"); // pay + setChoice(playerA, "Forest"); // pay + addTarget(playerA, "Mountain"); // return from lib + + checkHandCardCount("after buy", 5, PhaseStep.BEGIN_COMBAT, playerA, "Demonic Collusion", 1); + checkHandCardCount("after buy", 5, PhaseStep.BEGIN_COMBAT, playerA, "Forest", 0); + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/BuybackCondition.java b/Mage/src/main/java/mage/abilities/condition/common/BuybackCondition.java index da4b47cf08..f61f4ead82 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/BuybackCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/BuybackCondition.java @@ -1,4 +1,3 @@ - package mage.abilities.condition.common; import mage.abilities.Ability; @@ -20,7 +19,7 @@ public enum BuybackCondition implements Condition { if (card != null) { return card.getAbilities().stream() .filter(a -> a instanceof BuybackAbility) - .anyMatch(Ability::isActivated); + .anyMatch(a -> ((BuybackAbility) a).isBuybackActivated(game)); } return false; } diff --git a/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalCostImpl.java b/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalCostImpl.java index 969f52b0e0..1462e2e40a 100644 --- a/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalCostImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalCostImpl.java @@ -1,8 +1,6 @@ - package mage.abilities.costs; /** - * * @author LevelX2 */ public class OptionalAdditionalCostImpl extends CostsImpl implements OptionalAdditionalCost { @@ -32,9 +30,10 @@ public class OptionalAdditionalCostImpl extends CostsImpl implements Optio super(cost); this.name = cost.name; this.reminderText = cost.reminderText; + this.delimiter = cost.delimiter; this.activated = cost.activated; this.activatedCounter = cost.activatedCounter; - this.delimiter = cost.delimiter; + this.repeatable = cost.repeatable; } @Override @@ -77,7 +76,7 @@ public class OptionalAdditionalCostImpl extends CostsImpl implements Optio * message. * * @param position - if there are multiple costs, it's the postion the cost - * is set (starting with 0) + * is set (starting with 0) * @return */ @Override @@ -95,7 +94,6 @@ public class OptionalAdditionalCostImpl extends CostsImpl implements Optio /** * If the player intends to pay the cost, the cost will be activated - * */ @Override public void activate() { @@ -105,7 +103,6 @@ public class OptionalAdditionalCostImpl extends CostsImpl implements Optio /** * Reset the activate and count information - * */ @Override public void reset() { @@ -145,6 +142,7 @@ public class OptionalAdditionalCostImpl extends CostsImpl implements Optio /** * Returns the number of times the cost was activated + * * @return */ @Override diff --git a/Mage/src/main/java/mage/abilities/keyword/BuybackAbility.java b/Mage/src/main/java/mage/abilities/keyword/BuybackAbility.java index 4cc80bd106..32853c708a 100644 --- a/Mage/src/main/java/mage/abilities/keyword/BuybackAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/BuybackAbility.java @@ -1,15 +1,9 @@ - package mage.abilities.keyword; -import java.util.Iterator; import mage.abilities.Ability; import mage.abilities.SpellAbility; import mage.abilities.StaticAbility; -import mage.abilities.costs.Cost; -import mage.abilities.costs.Costs; -import mage.abilities.costs.OptionalAdditionalCost; -import mage.abilities.costs.OptionalAdditionalCostImpl; -import mage.abilities.costs.OptionalAdditionalSourceCosts; +import mage.abilities.costs.*; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.ReplacementEffectImpl; @@ -22,9 +16,11 @@ import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; import mage.players.Player; +import java.util.Iterator; + /** * 702.25. Buyback - * + *

* 702.25a Buyback appears on some instants and sorceries. It represents two * static abilities that function while the spell is on the stack. "Buyback * [cost]" means "You may pay an additional [cost] as you cast this spell" and @@ -57,7 +53,8 @@ public class BuybackAbility extends StaticAbility implements OptionalAdditionalS public BuybackAbility(final BuybackAbility ability) { super(ability); - buybackCost = ability.buybackCost; + buybackCost = new OptionalAdditionalCostImpl((OptionalAdditionalCostImpl) ability.buybackCost); + amountToReduceBy = ability.amountToReduceBy; } @Override @@ -67,9 +64,7 @@ public class BuybackAbility extends StaticAbility implements OptionalAdditionalS @Override public void addCost(Cost cost) { - if (buybackCost != null) { - ((Costs) buybackCost).add(cost); - } + ((OptionalAdditionalCostImpl) buybackCost).add(cost); } public void resetReduceCost() { @@ -80,24 +75,23 @@ public class BuybackAbility extends StaticAbility implements OptionalAdditionalS public int reduceCost(int genericManaToReduce) { int amountToReduce = genericManaToReduce; boolean foundCostToReduce = false; - if (buybackCost != null) { - for (Object cost : ((Costs) buybackCost)) { - if (cost instanceof ManaCostsImpl) { - for (Object c : (ManaCostsImpl) cost) { - if (c instanceof GenericManaCost) { - int newCostCMC = ((GenericManaCost) c).convertedManaCost() - amountToReduceBy - genericManaToReduce; - foundCostToReduce = true; - if (newCostCMC > 0) { - amountToReduceBy += genericManaToReduce; - } else { - amountToReduce = ((GenericManaCost) c).convertedManaCost() - amountToReduceBy; - amountToReduceBy = ((GenericManaCost) c).convertedManaCost(); - } + for (Object cost : ((Costs) buybackCost)) { + if (cost instanceof ManaCostsImpl) { + for (Object c : (ManaCostsImpl) cost) { + if (c instanceof GenericManaCost) { + int newCostCMC = ((GenericManaCost) c).convertedManaCost() - amountToReduceBy - genericManaToReduce; + foundCostToReduce = true; + if (newCostCMC > 0) { + amountToReduceBy += genericManaToReduce; + } else { + amountToReduce = ((GenericManaCost) c).convertedManaCost() - amountToReduceBy; + amountToReduceBy = ((GenericManaCost) c).convertedManaCost(); } } } } } + if (foundCostToReduce) { return amountToReduce; } @@ -106,36 +100,45 @@ public class BuybackAbility extends StaticAbility implements OptionalAdditionalS @Override public boolean isActivated() { - if (buybackCost != null) { - return buybackCost.isActivated(); - } - resetReduceCost(); - return false; + return buybackCost.isActivated(); } - public void resetBuyback() { - if (buybackCost != null) { + private void resetBuyback(Game game) { + activateBuyback(game, false); + resetReduceCost(); + buybackCost.reset(); + } + + private void activateBuyback(Game game, Boolean isActivated) { + // xmage uses copies, all statuses must be saved to game state, not abilities + game.getState().setValue(this.getSourceId().toString() + "_activatedBuyback", isActivated); + + // for extra info in cast message + if (isActivated) { + buybackCost.activate(); + } else { buybackCost.reset(); - resetReduceCost(); } } + public boolean isBuybackActivated(Game game) { + return (Boolean) game.getState().getValue(this.getSourceId().toString() + "_activatedBuyback"); + } + @Override public void addOptionalAdditionalCosts(Ability ability, Game game) { if (ability instanceof SpellAbility) { Player player = game.getPlayer(ability.getControllerId()); if (player != null) { - this.resetBuyback(); - if (buybackCost != null) { - if (player.chooseUse(Outcome.Benefit, "Pay " + buybackCost.getText(false) + " ?", ability, game)) { - buybackCost.activate(); - for (Iterator it = ((Costs) buybackCost).iterator(); it.hasNext();) { - Cost cost = (Cost) it.next(); - if (cost instanceof ManaCostsImpl) { - ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy()); - } else { - ability.getCosts().add(cost.copy()); - } + this.resetBuyback(game); + if (player.chooseUse(Outcome.Benefit, "Pay " + buybackCost.getText(false) + " ?", ability, game)) { + activateBuyback(game, true); + for (Iterator it = ((Costs) buybackCost).iterator(); it.hasNext(); ) { + Cost cost = (Cost) it.next(); + if (cost instanceof ManaCostsImpl) { + ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy()); + } else { + ability.getCosts().add(cost.copy()); } } } @@ -145,29 +148,12 @@ public class BuybackAbility extends StaticAbility implements OptionalAdditionalS @Override public String getRule() { - StringBuilder sb = new StringBuilder(); - if (buybackCost != null) { - sb.append(buybackCost.getText(false)); - sb.append(' ').append(buybackCost.getReminderText()); - } - return sb.toString(); + return buybackCost.getText(false) + ' ' + buybackCost.getReminderText(); } @Override public String getCastMessageSuffix() { - if (buybackCost != null) { - return buybackCost.getCastSuffixMessage(0); - } else { - return ""; - } - } - - public String getReminderText() { - if (buybackCost != null) { - return buybackCost.getReminderText(); - } else { - return ""; - } + return buybackCost.getCastSuffixMessage(0); } } @@ -196,10 +182,9 @@ class BuybackEffect extends ReplacementEffectImpl { public boolean applies(GameEvent event, Ability source, Game game) { if (event.getTargetId().equals(source.getSourceId())) { ZoneChangeEvent zEvent = (ZoneChangeEvent) event; - if (zEvent.getFromZone() == Zone.STACK && zEvent.getToZone() == Zone.GRAVEYARD - && source.getSourceId().equals(event.getSourceId())) { // if spell fizzled, the sourceId is null - return true; - } + // if spell fizzled, the sourceId is null + return zEvent.getFromZone() == Zone.STACK && zEvent.getToZone() == Zone.GRAVEYARD + && source.getSourceId().equals(event.getSourceId()); } return false; } @@ -213,7 +198,7 @@ class BuybackEffect extends ReplacementEffectImpl { public boolean replaceEvent(GameEvent event, Ability source, Game game) { Card card = game.getCard(source.getSourceId()); if (card != null && source instanceof BuybackAbility) { - if (((BuybackAbility) source).isActivated()) { + if (((BuybackAbility) source).isBuybackActivated(game)) { return card.moveToZone(Zone.HAND, source.getSourceId(), game, true, event.getAppliedEffects()); } }