mirror of
https://github.com/correl/mage.git
synced 2024-11-14 19:19:32 +00:00
* 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;
This commit is contained in:
parent
936be75a66
commit
4d362d7edc
32 changed files with 522 additions and 302 deletions
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<String, Integer> countNames = new HashMap<>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T extends MageItem> 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<T extends MageItem> 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)) {
|
||||
|
|
|
@ -34,15 +34,15 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect {
|
|||
private final CardType additionalCardType;
|
||||
private boolean hasHaste;
|
||||
private final int number;
|
||||
private List<Permanent> addedTokenPermanents;
|
||||
private final List<Permanent> 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);
|
||||
|
|
|
@ -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
|
||||
*
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<MageObjectReference> addedTokens = new HashSet<>();
|
||||
int opponentCount = game.getOpponents(source.getControllerId()).size();
|
||||
if (opponentCount < 1) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<Token, Card> {
|
|||
}
|
||||
|
||||
@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<Token, Card> {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
|
||||
package mage.util.functions;
|
||||
|
||||
import mage.game.Game;
|
||||
|
||||
/**
|
||||
* @author nantuko
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Function<X, Y> {
|
||||
X apply(Y in);
|
||||
X apply(Y in, Game game);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue