diff --git a/Mage.Sets/src/mage/sets/journeyintonyx/BrainMaggot.java b/Mage.Sets/src/mage/sets/journeyintonyx/BrainMaggot.java index 5406412f5f0..e2ce4ed1bcb 100644 --- a/Mage.Sets/src/mage/sets/journeyintonyx/BrainMaggot.java +++ b/Mage.Sets/src/mage/sets/journeyintonyx/BrainMaggot.java @@ -73,7 +73,7 @@ public class BrainMaggot extends CardImpl { // When Brain Maggot enters the battlefield, target opponent reveals his or her hand and you choose a nonland card from it. Exile that card until Brain Maggot leaves the battlefield. Ability ability = new EntersBattlefieldTriggeredAbility(new BrainMaggotExileEffect()); ability.addTarget(new TargetOpponent()); - ability.addEffect(new CreateDelayedTriggeredAbilityEffect(new BrainMaggotReturnExiledCreatureAbility())); + ability.addEffect(new CreateDelayedTriggeredAbilityEffect(new BrainMaggotReturnExiledCardAbility())); this.addAbility(ability); } @@ -135,21 +135,21 @@ class BrainMaggotExileEffect extends OneShotEffect { * @author LevelX2 */ -class BrainMaggotReturnExiledCreatureAbility extends DelayedTriggeredAbility { +class BrainMaggotReturnExiledCardAbility extends DelayedTriggeredAbility { - public BrainMaggotReturnExiledCreatureAbility() { - super(new BrainMaggotReturnExiledCreatureEffect(), Duration.OneUse); + public BrainMaggotReturnExiledCardAbility() { + super(new BrainMaggotReturnExiledCardEffect(), Duration.OneUse); this.usesStack = false; this.setRuleVisible(false); } - public BrainMaggotReturnExiledCreatureAbility(final BrainMaggotReturnExiledCreatureAbility ability) { + public BrainMaggotReturnExiledCardAbility(final BrainMaggotReturnExiledCardAbility ability) { super(ability); } @Override - public BrainMaggotReturnExiledCreatureAbility copy() { - return new BrainMaggotReturnExiledCreatureAbility(this); + public BrainMaggotReturnExiledCardAbility copy() { + return new BrainMaggotReturnExiledCardAbility(this); } @Override @@ -169,20 +169,20 @@ class BrainMaggotReturnExiledCreatureAbility extends DelayedTriggeredAbility { } } -class BrainMaggotReturnExiledCreatureEffect extends OneShotEffect { +class BrainMaggotReturnExiledCardEffect extends OneShotEffect { - public BrainMaggotReturnExiledCreatureEffect() { + public BrainMaggotReturnExiledCardEffect() { super(Outcome.Benefit); this.staticText = "Return exiled nonland card to its owner's hand"; } - public BrainMaggotReturnExiledCreatureEffect(final BrainMaggotReturnExiledCreatureEffect effect) { + public BrainMaggotReturnExiledCardEffect(final BrainMaggotReturnExiledCardEffect effect) { super(effect); } @Override - public BrainMaggotReturnExiledCreatureEffect copy() { - return new BrainMaggotReturnExiledCreatureEffect(this); + public BrainMaggotReturnExiledCardEffect copy() { + return new BrainMaggotReturnExiledCardEffect(this); } @Override 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 162211c664d..2e7f90fdfa0 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 @@ -25,7 +25,6 @@ * authors and should not be interpreted as representing official policies, either expressed * or implied, of BetaSteward_at_googlemail.com. */ - package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; @@ -42,39 +41,41 @@ import org.mage.test.serverside.base.CardTestPlayerBase; public class KickerTest extends CardTestPlayerBase { /** - * 702.32. Kicker - * 702.32a Kicker is a static ability that functions while the spell with kicker is on the stack. “Kicker - * [cost]” means “You may pay an additional [cost] as you cast this spell.” Paying a spell’s kicker - * cost(s) follows the rules for paying additional costs in rules 601.2b and 601.2e–g. - * 702.32b The phrase “Kicker [cost 1] and/or [cost 2]” means the same thing as “Kicker [cost 1], - * kicker [cost 2].” - * 702.32c Multikicker is a variant of the kicker ability. “Multikicker [cost]” means “You may pay an - * additional [cost] any number of times as you cast this spell.” A multikicker cost is a kicker cost. - * 702.32d If a spell’s controller declares the intention to pay any of that spell’s kicker costs, that spell - * has been “kicked.” If a spell has two kicker costs or has multikicker, it may be kicked multiple - * times. See rule 601.2b. - * 702.32e Objects with kicker or multikicker have additional abilities that specify what happens if - * they are kicked. These abilities are linked to the kicker or multikicker abilities printed on that - * object: they can refer only to those specific kicker or multikicker abilities. See rule 607, “Linked - * Abilities.” - * 702.32f Objects with more than one kicker cost have abilities that each correspond to a specific - * kicker cost. They contain the phrases “if it was kicked with its [A] kicker” and “if it was kicked - * with its [B] kicker,” where A and B are the first and second kicker costs listed on the card, - * respectively. Each of those abilities is linked to the appropriate kicker ability. - * 702.32g If part of a spell’s ability has its effect only if that spell was kicked, and that part of the - * ability includes any targets, the spell’s controller chooses those targets only if that spell was - * kicked. Otherwise, the spell is cast as if it did not have those targets. See rule 601.2c. - * + * 702.32. Kicker 702.32a Kicker is a static ability that functions while + * the spell with kicker is on the stack. “Kicker [cost]” means “You may pay + * an additional [cost] as you cast this spell.” Paying a spell’s kicker + * cost(s) follows the rules for paying additional costs in rules 601.2b and + * 601.2e–g. 702.32b The phrase “Kicker [cost 1] and/or [cost 2]” means the + * same thing as “Kicker [cost 1], kicker [cost 2].” 702.32c Multikicker is + * a variant of the kicker ability. “Multikicker [cost]” means “You may pay + * an additional [cost] any number of times as you cast this spell.” A + * multikicker cost is a kicker cost. 702.32d If a spell’s controller + * declares the intention to pay any of that spell’s kicker costs, that + * spell has been “kicked.” If a spell has two kicker costs or has + * multikicker, it may be kicked multiple times. See rule 601.2b. 702.32e + * Objects with kicker or multikicker have additional abilities that specify + * what happens if they are kicked. These abilities are linked to the kicker + * or multikicker abilities printed on that object: they can refer only to + * those specific kicker or multikicker abilities. See rule 607, “Linked + * Abilities.” 702.32f Objects with more than one kicker cost have abilities + * that each correspond to a specific kicker cost. They contain the phrases + * “if it was kicked with its [A] kicker” and “if it was kicked with its [B] + * kicker,” where A and B are the first and second kicker costs listed on + * the card, respectively. Each of those abilities is linked to the + * appropriate kicker ability. 702.32g If part of a spell’s ability has its + * effect only if that spell was kicked, and that part of the ability + * includes any targets, the spell’s controller chooses those targets only + * if that spell was kicked. Otherwise, the spell is cast as if it did not + * 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. - * If AEther Figment was kicked, it enters the battlefield with two +1/+1 counters on it. - * - */ + * 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. + * If AEther Figment was kicked, it enters the battlefield with two +1/+1 + * counters on it. + * + */ @Test public void testUseKicker() { addCard(Zone.BATTLEFIELD, playerA, "Island", 5); @@ -88,10 +89,10 @@ public class KickerTest extends CardTestPlayerBase { assertPermanentCount(playerA, "AEther Figment", 1); assertCounterCount("AEther Figment", CounterType.P1P1, 2); - assertPowerToughness(playerA, "AEther Figment", 3, 3); - + assertPowerToughness(playerA, "AEther Figment", 3, 3); + } - + @Test public void testDontUseKicker() { addCard(Zone.BATTLEFIELD, playerA, "Island", 5); @@ -105,17 +106,16 @@ public class KickerTest extends CardTestPlayerBase { assertPermanentCount(playerA, "AEther Figment", 1); assertCounterCount("AEther Figment", CounterType.P1P1, 0); - assertPowerToughness(playerA, "AEther Figment", 1, 1); - + assertPowerToughness(playerA, "AEther Figment", 1, 1); + } - + /** - * Apex Hawks - * Creature — Bird 2/2, 2W (3) - * Multikicker {1}{W} (You may pay an additional {1}{W} any number of times as you cast this spell.) - * Flying - * Apex Hawks enters the battlefield with a +1/+1 counter on it for each time it was kicked. - * + * Apex Hawks Creature — Bird 2/2, 2W (3) Multikicker {1}{W} (You may pay an + * additional {1}{W} any number of times as you cast this spell.) Flying + * Apex Hawks enters the battlefield with a +1/+1 counter on it for each + * time it was kicked. + * */ @Test public void testUseMultikickerOnce() { @@ -131,8 +131,8 @@ public class KickerTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Apex Hawks", 1); assertCounterCount("Apex Hawks", CounterType.P1P1, 1); - assertPowerToughness(playerA, "Apex Hawks", 3, 3); - + assertPowerToughness(playerA, "Apex Hawks", 3, 3); + } @Test @@ -150,10 +150,10 @@ public class KickerTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Apex Hawks", 1); assertCounterCount("Apex Hawks", CounterType.P1P1, 2); - assertPowerToughness(playerA, "Apex Hawks", 4, 4); - + assertPowerToughness(playerA, "Apex Hawks", 4, 4); + } - + @Test public void testDontUseMultikicker() { addCard(Zone.BATTLEFIELD, playerA, "Plains", 7); @@ -167,16 +167,17 @@ public class KickerTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Apex Hawks", 1); assertCounterCount("Apex Hawks", CounterType.P1P1, 0); - assertPowerToughness(playerA, "Apex Hawks", 2, 2); - + 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. + * 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 testOrimsChantskicker() { 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.) @@ -189,24 +190,25 @@ public class KickerTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Orim's Chant", playerB); setChoice(playerA, "Yes"); - + attack(1, playerA, "Raging Goblin"); - + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", playerA); - + setStopAt(1, PhaseStep.END_TURN); execute(); 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. + + } + + /** + * Bloodhusk Ritualist's discard trigger does nothing if the Ritualist + * leaves the battlefield before the trigger resolves. */ @Test public void testBloodhuskRitualist() { @@ -216,7 +218,7 @@ public class KickerTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); addCard(Zone.HAND, playerA, "Bloodhusk Ritualist", 1); // 2/2 {2}{B} - + // 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"); @@ -228,14 +230,14 @@ public class KickerTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); - Assert.assertEquals("All mana has to be used","[]", playerA.getManaAvailable(currentGame).toString()); + Assert.assertEquals("All mana has to be used", "[]", playerA.getManaAvailable(currentGame).toString()); assertGraveyardCount(playerB, "Lightning Bolt", 1); assertGraveyardCount(playerA, "Bloodhusk Ritualist", 1); - assertGraveyardCount(playerB, "Fireball", 2); - + assertGraveyardCount(playerB, "Fireball", 2); + assertHandCount(playerB, 0); } - + /** * Test and/or kicker costs */ @@ -246,9 +248,9 @@ public class KickerTest extends CardTestPlayerBase { // 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. + // 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} @@ -259,8 +261,7 @@ public class KickerTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Sunscape Battlemage", 1); assertHandCount(playerA, 2); } - - + /** * Test and/or kicker costs */ @@ -270,14 +271,13 @@ public class KickerTest extends CardTestPlayerBase { 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. + // 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} @@ -289,7 +289,43 @@ public class KickerTest extends CardTestPlayerBase { 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); + + } + } diff --git a/Mage/src/mage/abilities/TriggeredAbilities.java b/Mage/src/mage/abilities/TriggeredAbilities.java index fe0dd47fe64..430ae354192 100644 --- a/Mage/src/mage/abilities/TriggeredAbilities.java +++ b/Mage/src/mage/abilities/TriggeredAbilities.java @@ -47,9 +47,10 @@ import mage.game.permanent.Permanent; /** * * @author BetaSteward_at_googlemail.com - * + * * This class uses ConcurrentHashMap to avoid ConcurrentModificationExceptions. - * See ticket https://github.com/magefree/mage/issues/966 and https://github.com/magefree/mage/issues/473 + * See ticket https://github.com/magefree/mage/issues/966 and + * https://github.com/magefree/mage/issues/473 */ public class TriggeredAbilities extends ConcurrentHashMap { @@ -100,7 +101,6 @@ public class TriggeredAbilities extends ConcurrentHashMap 0 && (source.getAbilityType().equals(AbilityType.TRIGGERED) || source.getAbilityType().equals(AbilityType.STATIC))) {