From 4d362d7edcd08c0313e653fa0af7e988b9595dee Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 15 Dec 2020 20:06:53 +0400 Subject: [PATCH] * Copy spells - improved combo support with other abilities like Kicker or Entwine (#7192): * Now ZCC of copied spells syncs with source card or coping spell (allows to keep ability settings that depends on ZCC); * Fixed bug that allows to lost kicked status in copied spells after counter the original spell or moves the original card (see #7192); * Test framework: improved support of targeting copy or non copy spells on stack; --- Mage.Sets/src/mage/cards/a/Absorb.java | 8 +- .../src/mage/cards/a/AgonizingDemise.java | 18 +- .../src/mage/cards/b/BeamsplitterMage.java | 2 +- Mage.Sets/src/mage/cards/b/Boomerang.java | 8 +- Mage.Sets/src/mage/cards/f/Fork.java | 7 +- .../src/mage/cards/g/GodPharaohsGift.java | 14 +- .../src/mage/cards/h/HourOfEternity.java | 3 +- .../mage/cards/m/MechanizedProduction.java | 15 +- .../src/mage/cards/p/PrototypePortal.java | 6 +- Mage.Sets/src/mage/cards/s/SpittingImage.java | 10 +- .../cards/abilities/keywords/KickerTest.java | 484 ++++++++++++------ .../single/cmr/KrarkTheThumblessTest.java | 7 +- .../cards/single/soi/PrizedAmalgamTest.java | 7 +- .../base/impl/CardTestPlayerAPIImpl.java | 28 +- .../org/mage/test/testapi/TestAliases.java | 4 +- .../condition/common/KickedCondition.java | 11 +- .../CopySpellForEachItCouldTargetEffect.java | 5 +- .../common/CreateTokenCopyTargetEffect.java | 12 +- .../abilities/effects/common/EpicEffect.java | 12 +- .../mage/abilities/keyword/EmbalmAbility.java | 4 +- .../mage/abilities/keyword/EncoreAbility.java | 2 +- .../abilities/keyword/EntwineAbility.java | 15 +- .../abilities/keyword/EternalizeAbility.java | 2 +- .../mage/abilities/keyword/KickerAbility.java | 26 +- .../ConditionalSpellManaBuilder.java | 10 +- Mage/src/main/java/mage/cards/CardImpl.java | 4 +- .../src/main/java/mage/game/ZonesHandler.java | 3 +- .../game/command/emblems/MomirEmblem.java | 2 +- .../mage/game/permanent/PermanentToken.java | 4 + Mage/src/main/java/mage/game/stack/Spell.java | 62 ++- .../util/functions/CopyTokenFunction.java | 24 +- .../java/mage/util/functions/Function.java | 5 +- 32 files changed, 522 insertions(+), 302 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/Absorb.java b/Mage.Sets/src/mage/cards/a/Absorb.java index b816956666e..d09b34c468a 100644 --- a/Mage.Sets/src/mage/cards/a/Absorb.java +++ b/Mage.Sets/src/mage/cards/a/Absorb.java @@ -1,7 +1,5 @@ - package mage.cards.a; -import java.util.UUID; import mage.abilities.effects.common.CounterTargetEffect; import mage.abilities.effects.common.GainLifeEffect; import mage.cards.CardImpl; @@ -9,15 +7,15 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.target.TargetSpell; +import java.util.UUID; + /** - * * @author Loki */ public final class Absorb extends CardImpl { public Absorb(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{W}{U}{U}"); - + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{W}{U}{U}"); // Counter target spell. You gain 3 life. this.getSpellAbility().addEffect(new CounterTargetEffect()); diff --git a/Mage.Sets/src/mage/cards/a/AgonizingDemise.java b/Mage.Sets/src/mage/cards/a/AgonizingDemise.java index dcaab970431..e7d179fb1bf 100644 --- a/Mage.Sets/src/mage/cards/a/AgonizingDemise.java +++ b/Mage.Sets/src/mage/cards/a/AgonizingDemise.java @@ -1,7 +1,5 @@ - package mage.cards.a; -import java.util.UUID; import mage.ObjectColor; import mage.abilities.condition.common.KickedCondition; import mage.abilities.decorator.ConditionalOneShotEffect; @@ -17,33 +15,35 @@ import mage.filter.predicate.Predicates; import mage.filter.predicate.mageobject.ColorPredicate; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** - * * @author FenrisulfrX */ public final class AgonizingDemise extends CardImpl { - + private static final FilterCreaturePermanent filterNonBlackCreature = new FilterCreaturePermanent("nonblack creature"); + static { filterNonBlackCreature.add(Predicates.not(new ColorPredicate(ObjectColor.BLACK))); } public AgonizingDemise(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{3}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{B}"); // Kicker {1}{R} this.addAbility(new KickerAbility("{1}{R}")); - + // Destroy target nonblack creature. It can't be regenerated. this.getSpellAbility().addEffect(new DestroyTargetEffect(true)); this.getSpellAbility().addTarget(new TargetCreaturePermanent(filterNonBlackCreature)); - - //If Agonizing Demise was kicked, it deals damage equal to that creature's power to the creature's controller. + + // If Agonizing Demise was kicked, it deals damage equal to that creature's power to the creature's controller. this.getSpellAbility().addEffect(new ConditionalOneShotEffect( new DamageTargetControllerEffect(TargetPermanentPowerCount.instance), KickedCondition.instance, "if this spell was kicked, it deals damage equal to that creature's power to the creature's controller.")); - + } public AgonizingDemise(final AgonizingDemise card) { diff --git a/Mage.Sets/src/mage/cards/b/BeamsplitterMage.java b/Mage.Sets/src/mage/cards/b/BeamsplitterMage.java index 99a2f4c0a49..8ebc51a1f19 100644 --- a/Mage.Sets/src/mage/cards/b/BeamsplitterMage.java +++ b/Mage.Sets/src/mage/cards/b/BeamsplitterMage.java @@ -166,7 +166,7 @@ class BeamsplitterMageEffect extends OneShotEffect { if (creature == null) { return false; } - Spell copy = spell.copySpell(source.getControllerId()); + Spell copy = spell.copySpell(source.getControllerId(), game); game.getStack().push(copy); setTarget: for (UUID modeId : copy.getSpellAbility().getModes().getSelectedModes()) { diff --git a/Mage.Sets/src/mage/cards/b/Boomerang.java b/Mage.Sets/src/mage/cards/b/Boomerang.java index dd2d9bd305e..5310127edba 100644 --- a/Mage.Sets/src/mage/cards/b/Boomerang.java +++ b/Mage.Sets/src/mage/cards/b/Boomerang.java @@ -1,23 +1,21 @@ - package mage.cards.b; -import java.util.UUID; import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.target.TargetPermanent; +import java.util.UUID; + /** - * * @author Loki */ public final class Boomerang extends CardImpl { public Boomerang(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{U}{U}"); + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}{U}"); - // Return target permanent to its owner's hand. this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); this.getSpellAbility().addTarget(new TargetPermanent()); diff --git a/Mage.Sets/src/mage/cards/f/Fork.java b/Mage.Sets/src/mage/cards/f/Fork.java index a26b2efd293..c7c6d8cf76f 100644 --- a/Mage.Sets/src/mage/cards/f/Fork.java +++ b/Mage.Sets/src/mage/cards/f/Fork.java @@ -1,4 +1,3 @@ - package mage.cards.f; import mage.abilities.Ability; @@ -11,7 +10,6 @@ import mage.constants.Outcome; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.events.CopiedStackObjectEvent; -import mage.game.events.GameEvent; import mage.game.stack.Spell; import mage.players.Player; import mage.target.TargetSpell; @@ -19,14 +17,13 @@ import mage.target.TargetSpell; import java.util.UUID; /** - * * @author jeffwadsworth */ public final class Fork extends CardImpl { public Fork(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{R}{R}"); + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}{R}"); // Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy. this.getSpellAbility().addEffect(new ForkEffect()); @@ -60,7 +57,7 @@ class ForkEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); Spell spell = game.getStack().getSpell(targetPointer.getFirst(game, source)); if (spell != null && controller != null) { - Spell copy = spell.copySpell(source.getControllerId()); + Spell copy = spell.copySpell(source.getControllerId(), game); copy.getColor(game).setRed(true); game.getStack().push(copy); copy.chooseNewTargets(game, controller.getId()); diff --git a/Mage.Sets/src/mage/cards/g/GodPharaohsGift.java b/Mage.Sets/src/mage/cards/g/GodPharaohsGift.java index 27d38f46938..f6ea6fa0bf4 100644 --- a/Mage.Sets/src/mage/cards/g/GodPharaohsGift.java +++ b/Mage.Sets/src/mage/cards/g/GodPharaohsGift.java @@ -1,7 +1,5 @@ - package mage.cards.g; -import java.util.UUID; import mage.MageObject; import mage.ObjectColor; import mage.abilities.Ability; @@ -13,12 +11,7 @@ import mage.abilities.keyword.HasteAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.SubType; -import mage.constants.TargetController; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; @@ -28,8 +21,9 @@ import mage.target.common.TargetCardInYourGraveyard; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; +import java.util.UUID; + /** - * * @author jeffwadsworth */ public final class GodPharaohsGift extends CardImpl { @@ -83,7 +77,7 @@ class GodPharaohsGiftEffect extends OneShotEffect { if (cardChosen != null && cardChosen.moveToExile(exileId, sourceObject.getIdName(), source, game)) { EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(cardChosen); + CardUtil.copyTo(token).from(cardChosen, game); token.removePTCDA(); token.getPower().modifyBaseValue(4); token.getToughness().modifyBaseValue(4); diff --git a/Mage.Sets/src/mage/cards/h/HourOfEternity.java b/Mage.Sets/src/mage/cards/h/HourOfEternity.java index 2fbe11f63a1..3f9d7eda110 100644 --- a/Mage.Sets/src/mage/cards/h/HourOfEternity.java +++ b/Mage.Sets/src/mage/cards/h/HourOfEternity.java @@ -1,4 +1,3 @@ - package mage.cards.h; import mage.ObjectColor; @@ -92,7 +91,7 @@ class HourOfEternityEffect extends OneShotEffect { for (Card card : cardsToExile) { if (game.getState().getZone(card.getId()) == Zone.EXILED) { EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(card); + CardUtil.copyTo(token).from(card, game); token.removePTCDA(); token.getPower().modifyBaseValue(4); token.getToughness().modifyBaseValue(4); diff --git a/Mage.Sets/src/mage/cards/m/MechanizedProduction.java b/Mage.Sets/src/mage/cards/m/MechanizedProduction.java index 54979929254..9cce8a49a83 100644 --- a/Mage.Sets/src/mage/cards/m/MechanizedProduction.java +++ b/Mage.Sets/src/mage/cards/m/MechanizedProduction.java @@ -1,11 +1,5 @@ - package mage.cards.m; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.UUID; - import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.OneShotEffect; @@ -14,8 +8,8 @@ import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.TargetController; import mage.filter.common.FilterArtifactPermanent; import mage.filter.common.FilterControlledArtifactPermanent; @@ -27,6 +21,11 @@ import mage.target.TargetPermanent; import mage.target.common.TargetControlledPermanent; import mage.util.CardUtil; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + /** * @author LevelX2 */ @@ -81,7 +80,7 @@ class MechanizedProductionEffect extends OneShotEffect { Permanent enchantedArtifact = game.getPermanentOrLKIBattlefield(sourceObject.getAttachedTo()); if (enchantedArtifact != null) { EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(enchantedArtifact); + CardUtil.copyTo(token).from(enchantedArtifact, game); token.putOntoBattlefield(1, game, source, source.getControllerId()); } Map countNames = new HashMap<>(); diff --git a/Mage.Sets/src/mage/cards/p/PrototypePortal.java b/Mage.Sets/src/mage/cards/p/PrototypePortal.java index 1085c1a975f..84bece34813 100644 --- a/Mage.Sets/src/mage/cards/p/PrototypePortal.java +++ b/Mage.Sets/src/mage/cards/p/PrototypePortal.java @@ -1,7 +1,5 @@ - package mage.cards.p; -import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; @@ -26,6 +24,8 @@ import mage.players.Player; import mage.target.TargetCard; import mage.util.CardUtil; +import java.util.UUID; + /** * @author nantuko */ @@ -140,7 +140,7 @@ class PrototypePortalCreateTokenEffect extends OneShotEffect { Card card = game.getCard(permanent.getImprinted().get(0)); if (card != null) { EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(card); + CardUtil.copyTo(token).from(card, game); token.putOntoBattlefield(1, game, source, source.getControllerId()); return true; } diff --git a/Mage.Sets/src/mage/cards/s/SpittingImage.java b/Mage.Sets/src/mage/cards/s/SpittingImage.java index ed7bce0e9b3..c9f1b287ab8 100644 --- a/Mage.Sets/src/mage/cards/s/SpittingImage.java +++ b/Mage.Sets/src/mage/cards/s/SpittingImage.java @@ -1,7 +1,5 @@ - package mage.cards.s; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect; @@ -17,15 +15,15 @@ import mage.game.permanent.token.EmptyToken; import mage.target.common.TargetCreaturePermanent; import mage.util.CardUtil; +import java.util.UUID; + /** - * * @author jeffwadsworth - * */ public final class SpittingImage extends CardImpl { public SpittingImage(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{4}{G/U}{G/U}"); + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{G/U}{G/U}"); // Create a token that's a copy of target creature. this.getSpellAbility().addEffect(new CreateTokenCopyTargetEffect()); @@ -65,7 +63,7 @@ class SpittingImageEffect extends OneShotEffect { } if (permanent != null) { EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(permanent); + CardUtil.copyTo(token).from(permanent, game); token.putOntoBattlefield(1, game, source, source.getControllerId()); return true; } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java index 0728e66cf23..41b0af1036c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java @@ -9,7 +9,7 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * @author BetaSteward + * @author BetaSteward, LevelX2, JayDi85 */ public class KickerTest extends CardTestPlayerBase { @@ -42,6 +42,7 @@ public class KickerTest extends CardTestPlayerBase { * have those targets. See rule 601.2c. * */ + /** * Aether Figment Creature — Illusion 1/1, 1U (2) Kicker {3} (You may pay an * additional {3} as you cast this spell.) Aether Figment can't be blocked. @@ -49,7 +50,7 @@ public class KickerTest extends CardTestPlayerBase { * counters on it. */ @Test - public void testUseKicker_User() { + public void test_Use_Manual() { addCard(Zone.BATTLEFIELD, playerA, "Island", 5); addCard(Zone.HAND, playerA, "Aether Figment"); @@ -69,7 +70,7 @@ public class KickerTest extends CardTestPlayerBase { @Test @Ignore // TODO: enable test after replicate ability will be supported by AI (don't forget about multikicker support too) - public void testUseKicker_AI() { + public void test_Use_AI() { addCard(Zone.BATTLEFIELD, playerA, "Island", 5); addCard(Zone.HAND, playerA, "Aether Figment"); @@ -87,7 +88,7 @@ public class KickerTest extends CardTestPlayerBase { } @Test - public void testDontUseKicker_User() { + public void test_DontUse_Manual() { addCard(Zone.BATTLEFIELD, playerA, "Island", 5); addCard(Zone.HAND, playerA, "Aether Figment"); @@ -107,7 +108,7 @@ public class KickerTest extends CardTestPlayerBase { @Test @Ignore // TODO: enable test after replicate ability will be supported by AI (don't forget about multikicker support too) - public void testDontUseKicker_AI() { + public void test_DontUse_AI() { addCard(Zone.BATTLEFIELD, playerA, "Island", 5 - 1); // haven't all mana addCard(Zone.HAND, playerA, "Aether Figment"); @@ -131,7 +132,7 @@ public class KickerTest extends CardTestPlayerBase { * time it was kicked. */ @Test - public void testUseMultikickerOnce() { + public void test_Multikicker_UseOnce() { addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); addCard(Zone.HAND, playerA, "Apex Hawks"); @@ -139,8 +140,10 @@ public class KickerTest extends CardTestPlayerBase { setChoice(playerA, "Yes"); setChoice(playerA, "No"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, "Apex Hawks", 1); assertCounterCount("Apex Hawks", CounterType.P1P1, 1); @@ -149,7 +152,7 @@ public class KickerTest extends CardTestPlayerBase { } @Test - public void testUseMultikickerTwice() { + public void test_Multikicker_UseTwice() { addCard(Zone.BATTLEFIELD, playerA, "Plains", 7); addCard(Zone.HAND, playerA, "Apex Hawks"); @@ -158,39 +161,327 @@ public class KickerTest extends CardTestPlayerBase { setChoice(playerA, "Yes"); setChoice(playerA, "No"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, "Apex Hawks", 1); assertCounterCount("Apex Hawks", CounterType.P1P1, 2); assertPowerToughness(playerA, "Apex Hawks", 4, 4); - } @Test - public void testDontUseMultikicker() { + public void test_Multikicker_DontUse() { addCard(Zone.BATTLEFIELD, playerA, "Plains", 7); addCard(Zone.HAND, playerA, "Apex Hawks"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Apex Hawks"); setChoice(playerA, "No"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, "Apex Hawks", 1); assertCounterCount("Apex Hawks", CounterType.P1P1, 0); assertPowerToughness(playerA, "Apex Hawks", 2, 2); - } - /** - * When I cast Orim's Chant with Kicker cost, the player can play spells - * anyway during the turn. It seems like the kicker cost trigger an - * "instead" creatures can't attack. - */ @Test - public void testOrimsChantskicker() { + public void test_AndOr_UseOr() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + + // Kicker {1}{G} and/or {2}{U} + // When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying. + // When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards. + addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage"); + setChoice(playerA, "No"); // not use kicker {1}{G} + setChoice(playerA, "Yes"); // use kicker {2}{U} + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Sunscape Battlemage", 1); + assertHandCount(playerA, 2); + } + + @Test + public void test_AndOr_UseAnd() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + // Kicker {1}{G} and/or {2}{U} + // When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying. + // When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards. + addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W} + + addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage"); + setChoice(playerA, "Yes"); // use kicker {1}{G} + setChoice(playerA, "Yes"); // use kicker {2}{U} + setChoice(playerA, "When "); // two triggers from two kicker options + addTarget(playerA, "Birds of Paradise"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Birds of Paradise", 1); + assertPermanentCount(playerA, "Sunscape Battlemage", 1); + assertHandCount(playerA, 2); + } + + @Test + public void test_Conditional_MustWorkWithMultipleKickerOptions() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + // Kicker {1}{G} and/or {2}{U} + // When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying. + // When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards. + addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W} + + addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 1); + addCard(Zone.BATTLEFIELD, playerB, "Island", 1); + // Counter target spell if it was kicked. + addCard(Zone.HAND, playerB, "Ertai's Trickery", 1); + + // cast with kicker + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage"); + setChoice(playerA, "Yes"); // use kicker {1}{G} - destroy target creature with flying + setChoice(playerA, "Yes"); // use kicker {2}{U} - draw two cards + // spell must be countered, so no chooses + //setChoice(playerA, "When "); // two triggers rised: When {this} enters the battlefield, if it was kicked... + //addTarget(playerA, "Birds of Paradise"); // target for {1}{G} trigger + + // counter kicked spell + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Ertai's Trickery", "Sunscape Battlemage", "Sunscape Battlemage"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerB, "Birds of Paradise", 1); + assertGraveyardCount(playerB, "Ertai's Trickery", 1); + assertGraveyardCount(playerA, "Sunscape Battlemage", 1); + } + + @Test + public void test_ZCC_ReturnedPermanentMustNotBeKicked() { + // bug: + // If a creature is cast with kicker, dies, and is then returned to play + // from graveyard, it still behaves like it were kicked. I noticed this + // while testing some newly implemented cards, but it can be reproduced for + // example by Zombifying a Gatekeeper of Malakir. + + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + + // Kicker {B} (You may pay an additional {B} as you cast this spell.) + // When Gatekeeper of Malakir enters the battlefield, if it was kicked, target player sacrifices a creature. + addCard(Zone.HAND, playerA, "Gatekeeper of Malakir", 1); // 2/2 {B}{B} + + addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 2); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + // Return target permanent to its owner's hand. + addCard(Zone.HAND, playerB, "Boomerang", 1); + + // first cast with kicker + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gatekeeper of Malakir"); + setChoice(playerA, "Yes"); // use kicker + addTarget(playerA, playerB); // trigger's target + addTarget(playerB, "Birds of Paradise"); // sacrifice + + // return to hand + castSpell(1, PhaseStep.BEGIN_COMBAT, playerB, "Boomerang", "Gatekeeper of Malakir"); + + // second cast without kicker + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Gatekeeper of Malakir"); + setChoice(playerA, "No"); // no kicker + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Boomerang", 1); + assertGraveyardCount(playerB, "Birds of Paradise", 1); + assertPermanentCount(playerB, "Birds of Paradise", 1); + assertPermanentCount(playerA, "Gatekeeper of Malakir", 1); + } + + @Test + public void test_ZCC_CopiedSpellMustKeepKickerStatus() { + // https://github.com/magefree/mage/issues/7192 + // bug: Krark, the thumbless and a copy of him are on the field, and I cast Rite of replication kicked. + // The first coinflip fails and returns it to my hand, and the second coinflip wins and copies it, + // but does not copy the kicked part. I believe I did this before in another game and the first flip + // won then it would be a kicked copy. + + // Whenever you cast an instant or sorcery spell, you may copy that spell. You may choose new targets for the copy. + addCard(Zone.BATTLEFIELD, playerA, "Swarm Intelligence", 1); + // + // Kicker {1}{R} + // Destroy target nonblack creature. It can't be regenerated. + // If Agonizing Demise was kicked, it deals damage equal to that creature's power to the creature's controller. + addCard(Zone.HAND, playerA, "Agonizing Demise", 1); // {3}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears@bear1", 1); // 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears@bear2", 1); // 2/2 + + // cast spell with kicker and copy it (kicker status must be saved) + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 4); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Agonizing Demise", "@bear1"); + setChoice(playerA, "Yes"); // use kicker + checkStackSize("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // spell + trigger + checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 1); + checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 1); + // + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + setChoice(playerA, "Yes"); // copy spell + setChoice(playerA, "Yes"); // new target + addTarget(playerA, "@bear2"); + checkStackSize("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // spell + copy + checkStackObject("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 2); + checkStackObject("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 0); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 2 * 2); + } + + @Test + public void test_ZCC_CopiedSpellMustHaveIndependentZCC_InSpell() { + // reason: copied spell must have access to kicker status + + // Whenever you cast an instant or sorcery spell, you may copy that spell. You may choose new targets for the copy. + addCard(Zone.BATTLEFIELD, playerA, "Swarm Intelligence", 1); + // + // Kicker {1}{R} + // Destroy target nonblack creature. It can't be regenerated. + // If Agonizing Demise was kicked, it deals damage equal to that creature's power to the creature's controller. + addCard(Zone.HAND, playerA, "Agonizing Demise", 1); // {3}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears@bear1", 1); // 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears@bear2", 1); // 2/2 + // + // Counter target spell. You gain 3 life. + addCard(Zone.HAND, playerA, "Absorb", 1); // {W}{U}{U} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + // cast spell with kicker and copy it (kicker status must be saved) + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 4); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Agonizing Demise", "@bear1"); + setChoice(playerA, "Yes"); // use kicker + checkStackSize("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // spell + trigger + checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 1); + checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 1); + // + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + setChoice(playerA, "Yes"); // copy spell + setChoice(playerA, "Yes"); // new target + addTarget(playerA, "@bear2"); + checkStackSize("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // spell + copy + checkStackObject("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 2); + checkStackObject("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 0); + // + // counter first spell (non copy) to change original card's ZCC + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Absorb", "Agonizing Demise[no copy]", "Agonizing Demise", StackClause.WHILE_COPY_ON_STACK); + checkStackSize("before counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 4); // spell + copy + trigger + counter + checkStackObject("before counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 2); + checkStackObject("before counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 1); + checkStackObject("before counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Absorb", 1); + // + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); // trigger + setChoice(playerA, "No"); // do not copy counter spell + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); // counter + checkStackSize("after counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1); // copy + checkStackObject("after counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // cast spell - countered + // copied spell - resolved (2 damage) + // counter - resolved (3 life) + // possible bug: kicker status for copied spell was lost and no 2 damage + assertLife(playerA, 20 - 2 + 3); + } + + @Test + public void test_ZCC_CopiedSpellMustHaveIndependentZCC_InStaticAbility() { + // reason: static ability from copied spell's permanent must have access to kicker status + + // {4}, {T}: Copy target permanent spell you control. + addCard(Zone.BATTLEFIELD, playerA, "Lithoform Engine", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + // + // Kicker {4} + // If Academy Drake was kicked, it enters the battlefield with two +1/+1 counters on it. + addCard(Zone.HAND, playerA, "Academy Drake", 1); // {2}{U}, 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Island", 4 + 4 + 3); + // + // Counter target spell. You gain 3 life. + addCard(Zone.HAND, playerA, "Absorb", 1); // {W}{U}{U} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + // cast spell with kicker + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Academy Drake"); + setChoice(playerA, "Yes"); // use kicker + + // copy spell + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 4); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}, {T}", "Academy Drake", "Academy Drake"); + checkStackObject("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Academy Drake", 1); + checkStackObject("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}, {T}", 1); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + checkStackObject("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Academy Drake", 2); + + // counter first spell (non copy) to change original card's ZCC + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Absorb", "Academy Drake[no copy]", "Academy Drake", StackClause.WHILE_COPY_ON_STACK); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + + // cast spell - countered + // copied spell - +2 counters + // counter - resolved (3 life) + // possible bug: kicker status for copied spell was lost and no 2 counters added + assertPermanentCount(playerA, "Academy Drake", 1); + assertCounterCount(playerA, "Academy Drake", CounterType.P1P1, 2); + } + + @Test + public void test_Single_OrimsChants() { + // bug: + // When I cast Orim's Chant with Kicker cost, the player can play spells + // anyway during the turn. It seems like the kicker cost trigger an + // "instead" creatures can't attack. + addCard(Zone.BATTLEFIELD, playerA, "Raging Goblin", 1); // Haste 1/1 addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); // Kicker {W} (You may pay an additional {W} as you cast this spell.) @@ -208,23 +499,25 @@ public class KickerTest extends CardTestPlayerBase { castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", playerA); + // attack must be restricted, so no attack commands available + //setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); + //assertAllCommandsUsed(); assertGraveyardCount(playerA, "Orim's Chant", 1); assertGraveyardCount(playerB, "Lightning Bolt", 0); assertLife(playerA, 20); assertLife(playerB, 20); - } - /** - * Bloodhusk Ritualist's discard trigger does nothing if the Ritualist - * leaves the battlefield before the trigger resolves. - */ @Test - public void testBloodhuskRitualist() { + public void test_Single_BloodhuskRitualist() { + // bug: + // Bloodhusk Ritualist's discard trigger does nothing if the Ritualist + // leaves the battlefield before the trigger resolves. + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); addCard(Zone.HAND, playerB, "Lightning Bolt"); addCard(Zone.HAND, playerB, "Fireball", 2); @@ -235,13 +528,15 @@ public class KickerTest extends CardTestPlayerBase { // Multikicker (You may pay an additional {B} any number of times as you cast this spell.) // When Bloodhusk Ritualist enters the battlefield, target opponent discards a card for each time it was kicked. castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodhusk Ritualist"); - setChoice(playerA, "Yes"); // 2 x Multikicker - setChoice(playerA, "Yes"); - setChoice(playerA, "No"); + setChoice(playerA, "Yes", 2); // 2 x Multikicker + setChoice(playerA, "No"); // stop the kicking castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Bloodhusk Ritualist"); + addTarget(playerA, playerB); // target for kicker's trigger (discard cards) + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); Assert.assertEquals("All mana has to be used", "[]", playerA.getManaAvailable(currentGame).toString()); assertGraveyardCount(playerB, "Lightning Bolt", 1); @@ -251,138 +546,13 @@ public class KickerTest extends CardTestPlayerBase { assertHandCount(playerB, 0); } - /** - * Test and/or kicker costs - */ @Test - public void testSunscapeBattlemage1() { - addCard(Zone.BATTLEFIELD, playerA, "Island", 3); - addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + public void test_Single_MarshCasualties() { + // bug: + // Paying the Kicker on "Marsh Casualties" has no effect. Target player's + // creatures still only get -1/-1 instead of -2/-2. Was playing against AI. + // It was me who cast the spell. - // Kicker {1}{G} and/or {2}{U} - // When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying. - // When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards. - addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W} - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage"); - setChoice(playerA, "No"); // no {1}{G} - setChoice(playerA, "Yes"); // but {2}{U} - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertPermanentCount(playerA, "Sunscape Battlemage", 1); - assertHandCount(playerA, 2); - } - - /** - * Test and/or kicker costs - */ - @Test - public void testSunscapeBattlemage2() { - addCard(Zone.BATTLEFIELD, playerA, "Island", 3); - addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - - // Kicker {1}{G} and/or {2}{U} - // When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying. - // When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards. - addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W} - - addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage"); - addTarget(playerA, "Birds of Paradise"); - setChoice(playerA, "Yes"); // no {1}{G} - setChoice(playerA, "Yes"); // but {2}{U} - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertGraveyardCount(playerB, "Birds of Paradise", 1); - assertPermanentCount(playerA, "Sunscape Battlemage", 1); - assertHandCount(playerA, 2); - } - - /** - * If a creature is cast with kicker, dies, and is then returned to play - * from graveyard, it still behaves like it were kicked. I noticed this - * while testing some newly implemented cards, but it can be reproduced for - * example by Zombifying a Gatekeeper of Malakir. - */ - @Test - public void testKickerGoneForRecast() { - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); - - // Kicker {B} (You may pay an additional {B} as you cast this spell.) - // When Gatekeeper of Malakir enters the battlefield, if it was kicked, target player sacrifices a creature. - addCard(Zone.HAND, playerA, "Gatekeeper of Malakir", 1); // 2/2 {B}{B} - - addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 2); - addCard(Zone.BATTLEFIELD, playerB, "Island", 2); - addCard(Zone.HAND, playerB, "Boomerang", 1); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gatekeeper of Malakir"); - addTarget(playerA, playerB); - setChoice(playerA, "Yes"); // Kicker - - castSpell(1, PhaseStep.BEGIN_COMBAT, playerB, "Boomerang", "Gatekeeper of Malakir"); - - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Gatekeeper of Malakir"); - setChoice(playerA, "No"); // no Kicker - - setStopAt(1, PhaseStep.END_TURN); - execute(); - - assertGraveyardCount(playerB, "Boomerang", 1); - assertGraveyardCount(playerB, "Birds of Paradise", 1); - assertPermanentCount(playerB, "Birds of Paradise", 1); - assertPermanentCount(playerA, "Gatekeeper of Malakir", 1); - - } - - /** - * Check that kicker condition does also work for kicker cards with multiple - * kicker options - */ - @Test - public void testKickerCondition() { - addCard(Zone.BATTLEFIELD, playerA, "Island", 3); - addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - - // Kicker {1}{G} and/or {2}{U} - // When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying. - // When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards. - addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W} - - addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 1); - addCard(Zone.BATTLEFIELD, playerB, "Island", 1); - // Counter target spell if it was kicked. - addCard(Zone.HAND, playerB, "Ertai's Trickery", 1); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage"); - addTarget(playerA, "Birds of Paradise"); - setChoice(playerA, "Yes"); // {1}{G} destroy target creature with flying - setChoice(playerA, "Yes"); // {2}{U} draw two cards - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Ertai's Trickery", "Sunscape Battlemage"); - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertPermanentCount(playerB, "Birds of Paradise", 1); - assertGraveyardCount(playerB, "Ertai's Trickery", 1); - assertGraveyardCount(playerA, "Sunscape Battlemage", 1); - - } - - /** - * Paying the Kicker on "Marsh Casualties" has no effect. Target player's - * creatures still only get -1/-1 instead of -2/-2. Was playing against AI. - * It was me who cast the spell. - */ - @Test - public void testMarshCasualties() { addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); // Kicker {3} @@ -394,13 +564,13 @@ public class KickerTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Marsh Casualties", playerB); setChoice(playerA, "Yes"); // Pay Kicker + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertTappedCount("Swamp", true, 5); assertGraveyardCount(playerA, "Marsh Casualties", 1); assertPowerToughness(playerB, "Centaur Courser", 1, 1); } - - } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/KrarkTheThumblessTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/KrarkTheThumblessTest.java index e045a54021b..9b0d461c7c9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/KrarkTheThumblessTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/KrarkTheThumblessTest.java @@ -37,18 +37,20 @@ public class KrarkTheThumblessTest extends CardTestPlayerBase { waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); // FIRST BOLT CAST - // cast bolt and generate 3 triggers + // cast bolt and generate 2 triggers castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); setChoice(playerA, "Whenever"); // 2x triggers raise checkStackSize("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 3); // bolt + 2x triggers checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast L", 1); checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 2); + checkHandCardCount("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", 0); // resolve first trigger and lose (move spell to hand) setFlipCoinResult(playerA, false); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); - checkStackSize("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1); // second trigger + checkStackSize("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1); // one trigger checkStackObject("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast L", 0); + checkStackObject("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 1); checkHandCardCount("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", 1); // moved to hand // resolve second trigger and win (copy the spell) @@ -57,6 +59,7 @@ public class KrarkTheThumblessTest extends CardTestPlayerBase { setChoice(playerA, "No"); // do not change a target of the copy checkStackSize("after win trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1); // copied bolt checkStackObject("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast L", 1); + checkStackObject("after win trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 0); checkHandCardCount("after win trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", 1); // stil in hand // SECOND BOLT CAST diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/soi/PrizedAmalgamTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/soi/PrizedAmalgamTest.java index 6dc75feb43d..4e185158196 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/soi/PrizedAmalgamTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/soi/PrizedAmalgamTest.java @@ -43,9 +43,14 @@ public class PrizedAmalgamTest extends CardTestPlayerBase { */ @Test public void testGravecrawlerCastFromGrave() { - + // You may cast Gravecrawler from your graveyard as long as you control a Zombie. addCard(Zone.GRAVEYARD, playerA, "Gravecrawler", 1); + // + // Whenever a creature enters the battlefield, if it entered from your graveyard or you cast it from your + // graveyard, return Prized Amalgam from your graveyard to the battlefield tapped at the beginning of the + // next end step. addCard(Zone.GRAVEYARD, playerA, "Prized Amalgam", 1); + // addCard(Zone.BATTLEFIELD, playerA, "Gnawing Zombie", 1); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 10f196b1ded..8e8fe061f9a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1695,14 +1695,26 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement assertAliaseSupportInActivateCommand(cardName, true); assertAliaseSupportInActivateCommand(targetName, true); assertAliaseSupportInActivateCommand(spellOnStack, false); - if (StackClause.WHILE_ON_STACK == clause) { - addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName - + '$' + (targetName != null && targetName.startsWith("target") ? targetName : "target=" + targetName) - + "$spellOnStack=" + spellOnStack); - } else { - addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName - + '$' + (targetName != null && targetName.startsWith("target") ? targetName : "target=" + targetName) - + "$!spellOnStack=" + spellOnStack); + + String targetInfo = (targetName != null && targetName.startsWith("target") ? targetName : "target=" + targetName); + switch (clause) { + case WHILE_ON_STACK: + addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + + '$' + targetInfo + + "$spellOnStack=" + spellOnStack); + break; + case WHILE_COPY_ON_STACK: + addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + + '$' + targetInfo + + "$spellCopyOnStack=" + spellOnStack); + break; + case WHILE_NOT_ON_STACK: + addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + + '$' + targetInfo + + "$!spellOnStack=" + spellOnStack); + break; + default: + throw new IllegalArgumentException("Unknown stack clause " + clause); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/testapi/TestAliases.java b/Mage.Tests/src/test/java/org/mage/test/testapi/TestAliases.java index 577c816bbcb..eb9403b086d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/testapi/TestAliases.java +++ b/Mage.Tests/src/test/java/org/mage/test/testapi/TestAliases.java @@ -62,8 +62,8 @@ public class TestAliases extends CardTestPlayerBase { // name with face down spells: face down spells don't have names, see https://github.com/magefree/mage/issues/6569 Card bearCard = CardRepository.instance.findCard("Balduvian Bears").getCard(); - Spell normalSpell = new Spell(bearCard, bearCard.getSpellAbility(), playerA.getId(), Zone.HAND); - Spell faceDownSpell = new Spell(bearCard, bearCard.getSpellAbility(), playerA.getId(), Zone.HAND); + Spell normalSpell = new Spell(bearCard, bearCard.getSpellAbility(), playerA.getId(), Zone.HAND, currentGame); + Spell faceDownSpell = new Spell(bearCard, bearCard.getSpellAbility(), playerA.getId(), Zone.HAND, currentGame); faceDownSpell.setFaceDown(true, currentGame); // normal spell Assert.assertFalse(CardUtil.haveSameNames(normalSpell, "", currentGame)); diff --git a/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java index f5ce28a64d2..1c9d5681d37 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/KickedCondition.java @@ -1,5 +1,3 @@ - - package mage.abilities.condition.common; import mage.abilities.Ability; @@ -21,10 +19,15 @@ public enum KickedCondition implements Condition { @Override public boolean apply(Game game, Ability source) { Card card = game.getCard(source.getSourceId()); + if (card == null) { + // if permanent spell was copied then it enters with sourceId = PermanentToken instead Card (example: Lithoform Engine) + card = game.getPermanentEntering(source.getSourceId()); + } + if (card != null) { - for (Ability ability: card.getAbilities()) { + for (Ability ability : card.getAbilities()) { if (ability instanceof KickerAbility) { - if(((KickerAbility) ability).isKicked(game, source, "")) { + if (((KickerAbility) ability).isKicked(game, source, "")) { return true; } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java index 838d809d9a2..3145e222af6 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopySpellForEachItCouldTargetEffect.java @@ -11,7 +11,6 @@ import mage.filter.FilterInPlay; import mage.filter.predicate.mageobject.FromSetPredicate; import mage.game.Game; import mage.game.events.CopiedStackObjectEvent; -import mage.game.events.GameEvent; import mage.game.stack.Spell; import mage.players.Player; import mage.target.Target; @@ -82,7 +81,7 @@ public abstract class CopySpellForEachItCouldTargetEffect ex } // collect objects that can be targeted - Spell copy = spell.copySpell(source.getControllerId()); + Spell copy = spell.copySpell(source.getControllerId(), game); modifyCopy(copy, game, source); Target sampleTarget = targetsToBeChanged.iterator().next().getTarget(copy); sampleTarget.setNotTarget(true); @@ -94,7 +93,7 @@ public abstract class CopySpellForEachItCouldTargetEffect ex obj = game.getPlayer(objId); } if (obj != null) { - copy = spell.copySpell(source.getControllerId()); + copy = spell.copySpell(source.getControllerId(), game); try { modifyCopy(copy, (T) obj, game, source); if (!filter.match((T) obj, source.getSourceId(), actingPlayer.getId(), game)) { diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java index 4173e8eaf8f..b0477f5c862 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java @@ -34,15 +34,15 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { private final CardType additionalCardType; private boolean hasHaste; private final int number; - private List addedTokenPermanents; + private final List addedTokenPermanents; private SubType additionalSubType; private SubType onlySubType; - private boolean tapped; - private boolean attacking; - private UUID attackedPlayer; + private final boolean tapped; + private final boolean attacking; + private final UUID attackedPlayer; private final int tokenPower; private final int tokenToughness; - private boolean gainsFlying; + private final boolean gainsFlying; private boolean becomesArtifact; private ObjectColor color; private boolean useLKI = false; @@ -170,7 +170,7 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { } EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(copyFrom); // needed so that entersBattlefied triggered abilities see the attributes (e.g. Master Biomancer) + CardUtil.copyTo(token).from(copyFrom, game); // needed so that entersBattlefied triggered abilities see the attributes (e.g. Master Biomancer) applier.apply(game, token, source, targetId); if (becomesArtifact) { token.addCardType(CardType.ARTIFACT); diff --git a/Mage/src/main/java/mage/abilities/effects/common/EpicEffect.java b/Mage/src/main/java/mage/abilities/effects/common/EpicEffect.java index cd58b7ac8c6..3bf0e41e884 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/EpicEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/EpicEffect.java @@ -4,7 +4,6 @@ */ package mage.abilities.effects.common; -import java.util.Objects; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.DelayedTriggeredAbility; @@ -20,10 +19,11 @@ import mage.game.stack.Spell; import mage.game.stack.StackObject; import mage.players.Player; +import java.util.Objects; + /** - * * @author jeffwadsworth - * + *

* 702.49. Epic 702.49a Epic represents two spell abilities, one of which * creates a delayed triggered ability. “Epic” means “For the rest of the game, * you can't cast spells,” and “At the beginning of each of your upkeeps for the @@ -55,7 +55,7 @@ public class EpicEffect extends OneShotEffect { if (spell == null) { return false; } - spell = spell.copySpell(source.getControllerId()); + spell = spell.copySpell(source.getControllerId(), game); // Remove Epic effect from the spell Effect epicEffect = null; for (Effect effect : spell.getSpellAbility().getEffects()) { @@ -118,9 +118,7 @@ class EpicReplacementEffect extends ContinuousRuleModifyingEffectImpl { public boolean applies(GameEvent event, Ability source, Game game) { if (Objects.equals(source.getControllerId(), event.getPlayerId())) { MageObject object = game.getObject(event.getSourceId()); - if (object != null) { - return true; - } + return object != null; } return false; } diff --git a/Mage/src/main/java/mage/abilities/keyword/EmbalmAbility.java b/Mage/src/main/java/mage/abilities/keyword/EmbalmAbility.java index 446f94acd89..0beb2103502 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EmbalmAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EmbalmAbility.java @@ -22,7 +22,7 @@ import mage.util.CardUtil; */ public class EmbalmAbility extends ActivatedAbilityImpl { - private String rule; + private final String rule; public EmbalmAbility(Cost cost, Card card) { super(Zone.GRAVEYARD, new EmbalmEffect(), cost); @@ -85,7 +85,7 @@ class EmbalmEffect extends OneShotEffect { return false; } EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(card); // needed so that entersBattlefied triggered abilities see the attributes (e.g. Master Biomancer) + CardUtil.copyTo(token).from(card, game); // needed so that entersBattlefied triggered abilities see the attributes (e.g. Master Biomancer) token.getColor(game).setColor(ObjectColor.WHITE); token.addSubType(game, SubType.ZOMBIE); token.getManaCost().clear(); diff --git a/Mage/src/main/java/mage/abilities/keyword/EncoreAbility.java b/Mage/src/main/java/mage/abilities/keyword/EncoreAbility.java index 1f455b6519b..472a684bde5 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EncoreAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EncoreAbility.java @@ -85,7 +85,7 @@ class EncoreEffect extends OneShotEffect { return false; } EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(card); + CardUtil.copyTo(token).from(card, game); Set addedTokens = new HashSet<>(); int opponentCount = game.getOpponents(source.getControllerId()).size(); if (opponentCount < 1) { diff --git a/Mage/src/main/java/mage/abilities/keyword/EntwineAbility.java b/Mage/src/main/java/mage/abilities/keyword/EntwineAbility.java index fffd612c754..1ce40574a2b 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EntwineAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EntwineAbility.java @@ -8,7 +8,6 @@ import mage.abilities.costs.OptionalAdditionalCost; import mage.abilities.costs.OptionalAdditionalCostImpl; import mage.abilities.costs.OptionalAdditionalModeSourceCosts; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.constants.AbilityType; import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; @@ -144,17 +143,7 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM } private String getActivationKey(Ability source, Game game) { - // same logic as KickerAbility, uses for source ability only - int zcc = 0; - if (source.getAbilityType() == AbilityType.TRIGGERED) { - zcc = source.getSourceObjectZoneChangeCounter(); - } - if (zcc == 0) { - zcc = game.getState().getZoneChangeCounter(source.getSourceId()); - } - if (zcc > 0 && (source.getAbilityType() == AbilityType.TRIGGERED)) { - --zcc; - } - return String.valueOf(zcc); + // same logic as KickerAbility + return KickerAbility.getActivationKey(source, game); } } diff --git a/Mage/src/main/java/mage/abilities/keyword/EternalizeAbility.java b/Mage/src/main/java/mage/abilities/keyword/EternalizeAbility.java index 012f6fec20d..92691a7ec2c 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EternalizeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EternalizeAbility.java @@ -90,7 +90,7 @@ class EternalizeEffect extends OneShotEffect { return false; } EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(card); // needed so that entersBattlefied triggered abilities see the attributes (e.g. Master Biomancer) + CardUtil.copyTo(token).from(card, game); // needed so that entersBattlefied triggered abilities see the attributes (e.g. Master Biomancer) token.getColor(game).setColor(ObjectColor.BLACK); token.addSubType(game, SubType.ZOMBIE); token.getManaCost().clear(); diff --git a/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java b/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java index 475f6ef638d..3cc6fbcbf69 100644 --- a/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java @@ -159,18 +159,30 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo game.fireEvent(GameEvent.getEvent(GameEvent.EventType.KICKED, source.getSourceId(), source, source.getControllerId())); } - private String getActivationKey(Ability source, String costText, Game game) { - int zcc = 0; - if (source.getAbilityType() == AbilityType.TRIGGERED) { - zcc = source.getSourceObjectZoneChangeCounter(); - } + /** + * Return activation zcc key for searching spell's settings in source object + * + * @param source + * @param game + * @return + */ + public static String getActivationKey(Ability source, Game game) { + // must use ZCC from the moment of spell's ability activation + int zcc = source.getSourceObjectZoneChangeCounter(); if (zcc == 0) { + // if ability is not activated yet (example: triggered ability checking the kicker conditional) zcc = game.getState().getZoneChangeCounter(source.getSourceId()); } - if (zcc > 0 && (source.getAbilityType() == AbilityType.TRIGGERED)) { + + // triggers or activated abilities moves to stack and card's ZCC is changed -- so you must use workaround to find spell's zcc + if (source.getAbilityType() == AbilityType.TRIGGERED || source.getAbilityType() == AbilityType.ACTIVATED) { --zcc; } - return zcc + ((kickerCosts.size() > 1) ? costText : ""); + return zcc + ""; + } + + private String getActivationKey(Ability source, String costText, Game game) { + return getActivationKey(source, game) + ((kickerCosts.size() > 1) ? costText : ""); } @Override diff --git a/Mage/src/main/java/mage/abilities/mana/conditional/ConditionalSpellManaBuilder.java b/Mage/src/main/java/mage/abilities/mana/conditional/ConditionalSpellManaBuilder.java index eb76a1151e0..e913956624a 100644 --- a/Mage/src/main/java/mage/abilities/mana/conditional/ConditionalSpellManaBuilder.java +++ b/Mage/src/main/java/mage/abilities/mana/conditional/ConditionalSpellManaBuilder.java @@ -5,7 +5,6 @@ */ package mage.abilities.mana.conditional; -import java.util.UUID; import mage.ConditionalMana; import mage.MageObject; import mage.Mana; @@ -20,8 +19,9 @@ import mage.game.Game; import mage.game.stack.Spell; import mage.game.stack.StackObject; +import java.util.UUID; + /** - * * @author LevelX2 */ public class ConditionalSpellManaBuilder extends ConditionalManaBuilder { @@ -66,9 +66,9 @@ class SpellCastManaCondition extends ManaCondition implements Condition { if (source instanceof SpellAbility) { MageObject object = game.getObject(source.getSourceId()); if (game.inCheckPlayableState() && object instanceof Card) { - Spell spell = new Spell((Card) object, (SpellAbility) source, source.getControllerId(), game.getState().getZone(source.getSourceId())); - return spell != null && filter.match(spell, source.getSourceId(), source.getControllerId(), game); - } + Spell spell = new Spell((Card) object, (SpellAbility) source, source.getControllerId(), game.getState().getZone(source.getSourceId()), game); + return filter.match(spell, source.getSourceId(), source.getControllerId(), game); + } if ((object instanceof StackObject)) { return filter.match((StackObject) object, source.getSourceId(), source.getControllerId(), game); } diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index 4010706e33a..d7da027ca87 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -453,8 +453,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card { public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) { Card mainCard = getMainCard(); ZoneChangeEvent event = new ZoneChangeEvent(mainCard.getId(), ability, controllerId, fromZone, Zone.STACK); - ZoneChangeInfo.Stack info - = new ZoneChangeInfo.Stack(event, new Spell(this, ability.getSpellAbilityToResolve(game), controllerId, event.getFromZone())); + Spell spell = new Spell(this, ability.getSpellAbilityToResolve(game), controllerId, event.getFromZone(), game); + ZoneChangeInfo.Stack info = new ZoneChangeInfo.Stack(event, spell); return ZonesHandler.cast(info, game, ability); } diff --git a/Mage/src/main/java/mage/game/ZonesHandler.java b/Mage/src/main/java/mage/game/ZonesHandler.java index 4b41a9f7c4e..9338b334af2 100644 --- a/Mage/src/main/java/mage/game/ZonesHandler.java +++ b/Mage/src/main/java/mage/game/ZonesHandler.java @@ -234,8 +234,9 @@ public final class ZonesHandler { if (info instanceof ZoneChangeInfo.Stack && ((ZoneChangeInfo.Stack) info).spell != null) { spell = ((ZoneChangeInfo.Stack) info).spell; } else { - spell = new Spell(card, card.getSpellAbility().copy(), card.getOwnerId(), event.getFromZone()); + spell = new Spell(card, card.getSpellAbility().copy(), card.getOwnerId(), event.getFromZone(), game); } + spell.syncZoneChangeCounterOnStack(card, game); game.getStack().push(spell); game.getState().setZone(spell.getId(), Zone.STACK); game.getState().setZone(card.getId(), Zone.STACK); diff --git a/Mage/src/main/java/mage/game/command/emblems/MomirEmblem.java b/Mage/src/main/java/mage/game/command/emblems/MomirEmblem.java index 20fe58be4b9..132beb9dc16 100644 --- a/Mage/src/main/java/mage/game/command/emblems/MomirEmblem.java +++ b/Mage/src/main/java/mage/game/command/emblems/MomirEmblem.java @@ -81,7 +81,7 @@ class MomirEffect extends OneShotEffect { Card card = options.get(index).getCard(); if (card != null) { token = new EmptyToken(); - CardUtil.copyTo(token).from(card); + CardUtil.copyTo(token).from(card, game); break; } else { options.remove(index); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentToken.java b/Mage/src/main/java/mage/game/permanent/PermanentToken.java index b97a168e0f4..bba4ac18cce 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentToken.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentToken.java @@ -25,6 +25,10 @@ public class PermanentToken extends PermanentImpl { this.power.modifyBaseValue(token.getPower().getBaseValueModified()); this.toughness.modifyBaseValue(token.getToughness().getBaseValueModified()); this.copyFromToken(this.token, game, false); // needed to have at this time (e.g. for subtypes for entersTheBattlefield replacement effects) + + // token's ZCC must be synced with original token to keep abilities settings + // Example: kicker ability and kicked status + this.setZoneChangeCounter(this.token.getZoneChangeCounter(game), game); } public PermanentToken(final PermanentToken permanent) { diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 1767c04f9c9..7fe2b19a668 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -25,7 +25,6 @@ import mage.game.GameState; import mage.game.events.CopiedStackObjectEvent; import mage.game.events.CopyStackObjectEvent; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; import mage.game.permanent.PermanentCard; @@ -58,6 +57,7 @@ public class Spell extends StackObjImpl implements Card { private final SpellAbility ability; private final Zone fromZone; private final UUID id; + protected int zoneChangeCounter; // spell's ZCC must be synced with card's on stack or another copied spell private UUID controllerId; private boolean copy; @@ -69,12 +69,13 @@ public class Spell extends StackObjImpl implements Card { private ActivationManaAbilityStep currentActivatingManaAbilitiesStep = ActivationManaAbilityStep.BEFORE; - public Spell(Card card, SpellAbility ability, UUID controllerId, Zone fromZone) { + public Spell(Card card, SpellAbility ability, UUID controllerId, Zone fromZone, Game game) { this.card = card; this.color = card.getColor(null).copy(); this.frameColor = card.getFrameColor(null).copy(); this.frameStyle = card.getFrameStyle(); - id = ability.getId(); + this.id = ability.getId(); + this.zoneChangeCounter = card.getZoneChangeCounter(game); // sync card's ZCC with spell (copy spell settings) this.ability = ability; this.ability.setControllerId(controllerId); if (ability.getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) { @@ -93,6 +94,7 @@ public class Spell extends StackObjImpl implements Card { public Spell(final Spell spell) { this.id = spell.id; + this.zoneChangeCounter = spell.zoneChangeCounter; for (SpellAbility spellAbility : spell.spellAbilities) { this.spellAbilities.add(spellAbility.copy()); } @@ -265,7 +267,7 @@ public class Spell extends StackObjImpl implements Card { flag = controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null); } else { EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(card); + CardUtil.copyTo(token).from(card, game, this); // The token that a resolving copy of a spell becomes isn’t said to have been “created.” (2020-09-25) if (token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, false)) { permId = token.getLastAddedToken(); @@ -325,7 +327,7 @@ public class Spell extends StackObjImpl implements Card { } } else if (isCopy()) { EmptyToken token = new EmptyToken(); - CardUtil.copyTo(token).from(card); + CardUtil.copyTo(token).from(card, game, this); // The token that a resolving copy of a spell becomes isn’t said to have been “created.” (2020-09-25) token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, false); return true; @@ -744,8 +746,8 @@ public class Spell extends StackObjImpl implements Card { return new Spell(this); } - public Spell copySpell(UUID newController) { - Spell spellCopy = new Spell(this.card, this.ability.copySpell(), this.controllerId, this.fromZone); + public Spell copySpell(UUID newController, Game game) { + Spell spellCopy = new Spell(this.card, this.ability.copySpell(), this.controllerId, this.fromZone, game); boolean firstDone = false; for (SpellAbility spellAbility : this.getSpellAbilities()) { if (!firstDone) { @@ -758,6 +760,7 @@ public class Spell extends StackObjImpl implements Card { } spellCopy.setCopy(true, this); spellCopy.setControllerId(newController); + spellCopy.syncZoneChangeCounterOnStack(this, game); return spellCopy; } @@ -850,16 +853,6 @@ public class Spell extends StackObjImpl implements Card { throw new UnsupportedOperationException("Unsupported operation"); } - @Override - public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) { - throw new UnsupportedOperationException("Unsupported operation"); - } - - @Override - public void setZoneChangeCounter(int value, Game game) { - throw new UnsupportedOperationException("Unsupported operation"); - } - @Override public Ability getStackAbility() { return this.ability; @@ -877,7 +870,38 @@ public class Spell extends StackObjImpl implements Card { @Override public int getZoneChangeCounter(Game game) { - return card.getZoneChangeCounter(game); + // spell's zcc can't be changed after put to stack + return zoneChangeCounter; + } + + @Override + public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) { + throw new UnsupportedOperationException("Unsupported operation"); + } + + @Override + public void setZoneChangeCounter(int value, Game game) { + throw new UnsupportedOperationException("Unsupported operation"); + } + + /** + * Sync ZCC with card on stack + * + * @param card + * @param game + */ + public void syncZoneChangeCounterOnStack(Card card, Game game) { + this.zoneChangeCounter = card.getZoneChangeCounter(game); + } + + /** + * Sync ZCC with copy spell on stack + * + * @param spell + * @param game + */ + public void syncZoneChangeCounterOnStack(Spell spell, Game game) { + this.zoneChangeCounter = spell.getZoneChangeCounter(game); } @Override @@ -1048,7 +1072,7 @@ public class Spell extends StackObjImpl implements Card { return null; } for (int i = 0; i < gameEvent.getAmount(); i++) { - spellCopy = this.copySpell(newControllerId); + spellCopy = this.copySpell(newControllerId, game); game.getState().setZone(spellCopy.getId(), Zone.STACK); // required for targeting ex: Nivmagus Elemental game.getStack().push(spellCopy); if (chooseNewTargets) { diff --git a/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java b/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java index 86b4b26a6e5..2a27fce4194 100644 --- a/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java +++ b/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java @@ -1,4 +1,3 @@ - package mage.util.functions; import mage.MageObject; @@ -8,9 +7,11 @@ import mage.cards.Card; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; +import mage.game.Game; import mage.game.permanent.PermanentCard; import mage.game.permanent.PermanentToken; import mage.game.permanent.token.Token; +import mage.game.stack.Spell; /** * @author nantuko @@ -27,7 +28,7 @@ public class CopyTokenFunction implements Function { } @Override - public Token apply(Card source) { + public Token apply(Card source, Game game) { if (target == null) { throw new IllegalArgumentException("Target can't be null"); } @@ -94,7 +95,22 @@ public class CopyTokenFunction implements Function { return target; } - public Token from(Card source) { - return apply(source); + public Token from(Card source, Game game) { + return from(source, game, null); + } + + public Token from(Card source, Game game, Spell spell) { + apply(source, game); + + // token's ZCC must be synced with original card to keep abilities settings + // Example: kicker ability and kicked status + if (spell != null) { + // copied spell puts to battlefield as token, so that token's ZCC must be synced with spell instead card (card can be moved before resolve) + target.setZoneChangeCounter(spell.getZoneChangeCounter(game), game); + } else { + target.setZoneChangeCounter(source.getZoneChangeCounter(game), game); + } + + return target; } } diff --git a/Mage/src/main/java/mage/util/functions/Function.java b/Mage/src/main/java/mage/util/functions/Function.java index def77739a46..c8f4817fca6 100644 --- a/Mage/src/main/java/mage/util/functions/Function.java +++ b/Mage/src/main/java/mage/util/functions/Function.java @@ -1,10 +1,11 @@ - package mage.util.functions; +import mage.game.Game; + /** * @author nantuko */ @FunctionalInterface public interface Function { - X apply(Y in); + X apply(Y in, Game game); }