From db5526a1c6f20fd636c992b2420c20251c8f6201 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Fri, 19 Jun 2015 23:56:45 +0200 Subject: [PATCH] * Kicker - Fixed that kicker did not work correctly if the kicker card did change zone again before kicker dependant ability resolved. --- .../sets/worldwake/BloodhuskRitualist.java | 7 +- .../sets/worldwake/RumblingAftershocks.java | 2 +- .../sets/worldwake/StrengthOfTheTajuru.java | 2 - .../cards/abilities/keywords/KickerTest.java | 89 +++++++++++++++++++ .../condition/common/KickedCondition.java | 2 +- .../condition/common/KickedCostCondition.java | 10 +-- .../ConditionalTriggeredAbility.java | 18 ++++ .../dynamicvalue/common/MultikickerCount.java | 2 +- .../mage/abilities/keyword/KickerAbility.java | 68 +++++++------- 9 files changed, 145 insertions(+), 55 deletions(-) diff --git a/Mage.Sets/src/mage/sets/worldwake/BloodhuskRitualist.java b/Mage.Sets/src/mage/sets/worldwake/BloodhuskRitualist.java index d9ae21d721e..0a776c393d7 100644 --- a/Mage.Sets/src/mage/sets/worldwake/BloodhuskRitualist.java +++ b/Mage.Sets/src/mage/sets/worldwake/BloodhuskRitualist.java @@ -34,8 +34,6 @@ import mage.constants.Rarity; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.condition.common.KickedCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.dynamicvalue.common.MultikickerCount; import mage.abilities.effects.common.discard.DiscardTargetEffect; import mage.abilities.keyword.MultikickerAbility; @@ -61,10 +59,7 @@ public class BloodhuskRitualist extends CardImpl { this.addAbility(new MultikickerAbility("{B}")); // When Bloodhusk Ritualist enters the battlefield, target opponent discards a card for each time it was kicked. - Ability ability = new ConditionalTriggeredAbility( - new EntersBattlefieldTriggeredAbility(new DiscardTargetEffect(new MultikickerCount())), - KickedCondition.getInstance(), - ""); + Ability ability = new EntersBattlefieldTriggeredAbility(new DiscardTargetEffect(new MultikickerCount())); ability.addTarget(new TargetOpponent()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/sets/worldwake/RumblingAftershocks.java b/Mage.Sets/src/mage/sets/worldwake/RumblingAftershocks.java index 3a4894f321b..a8fcaefca2c 100644 --- a/Mage.Sets/src/mage/sets/worldwake/RumblingAftershocks.java +++ b/Mage.Sets/src/mage/sets/worldwake/RumblingAftershocks.java @@ -109,7 +109,7 @@ class RumblingAftershocksTriggeredAbility extends TriggeredAbilityImpl { int damageAmount = 0; for (Ability ability: spell.getAbilities()) { if (ability instanceof KickerAbility) { - damageAmount += ((KickerAbility) ability).getKickedCounter(game); + damageAmount += ((KickerAbility) ability).getKickedCounter(game, spell.getSpellAbility()); } } if (damageAmount > 0) { diff --git a/Mage.Sets/src/mage/sets/worldwake/StrengthOfTheTajuru.java b/Mage.Sets/src/mage/sets/worldwake/StrengthOfTheTajuru.java index 6645440db61..6767e5affff 100644 --- a/Mage.Sets/src/mage/sets/worldwake/StrengthOfTheTajuru.java +++ b/Mage.Sets/src/mage/sets/worldwake/StrengthOfTheTajuru.java @@ -54,8 +54,6 @@ public class StrengthOfTheTajuru extends CardImpl { super(ownerId, 113, "Strength of the Tajuru", Rarity.RARE, new CardType[]{CardType.INSTANT}, "{X}{G}{G}"); this.expansionSetCode = "WWK"; - - // Multikicker (You may pay an additional {1} any number of times as you cast this spell.) this.addAbility(new MultikickerAbility("{1}")); 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 8744f4f5a28..162211c664d 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 @@ -31,6 +31,7 @@ package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; import mage.constants.Zone; import mage.counters.CounterType; +import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -203,4 +204,92 @@ public class KickerTest extends CardTestPlayerBase { assertLife(playerB, 20); } + + /** + * Bloodhusk Ritualist's discard trigger does nothing if the Ritualist leaves the battlefield before the trigger resolves. + */ + @Test + public void testBloodhuskRitualist() { + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + addCard(Zone.HAND, playerB, "Fireball", 2); + + 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"); + setChoice(playerA, "Yes"); // 2 x Multikicker + setChoice(playerA, "Yes"); + setChoice(playerA, "No"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Bloodhusk Ritualist"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + 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); + + assertHandCount(playerB, 0); + } + + /** + * Test and/or kicker costs + */ + @Test + public void testSunscapeBattlemage1() { + 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"); // 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); + } + + } diff --git a/Mage/src/mage/abilities/condition/common/KickedCondition.java b/Mage/src/mage/abilities/condition/common/KickedCondition.java index 88491bdb5dc..02c9cfec31b 100644 --- a/Mage/src/mage/abilities/condition/common/KickedCondition.java +++ b/Mage/src/mage/abilities/condition/common/KickedCondition.java @@ -59,7 +59,7 @@ public class KickedCondition implements Condition { if (card != null) { for (Ability ability: card.getAbilities()) { if (ability instanceof KickerAbility) { - if(((KickerAbility) ability).isKicked(game)) { + if(((KickerAbility) ability).isKicked(game, source, "")) { return true; } } diff --git a/Mage/src/mage/abilities/condition/common/KickedCostCondition.java b/Mage/src/mage/abilities/condition/common/KickedCostCondition.java index 0add305c721..2a11646d850 100644 --- a/Mage/src/mage/abilities/condition/common/KickedCostCondition.java +++ b/Mage/src/mage/abilities/condition/common/KickedCostCondition.java @@ -24,17 +24,9 @@ public class KickedCostCondition implements Condition { public boolean apply(Game game, Ability source) { Card card = game.getCard(source.getSourceId()); if (card != null) { - KickerAbility kickerAbility = null; for (Ability ability: card.getAbilities()) { if (ability instanceof KickerAbility) { - kickerAbility = (KickerAbility) ability; - } - } - if (kickerAbility != null) { - for (OptionalAdditionalCost cost: kickerAbility.getKickerCosts()) { - if (cost.getText(true).equals(kickerCostText)) { - return cost.isActivated(); - } + return ((KickerAbility) ability).isKicked(game, source, kickerCostText); } } } diff --git a/Mage/src/mage/abilities/decorator/ConditionalTriggeredAbility.java b/Mage/src/mage/abilities/decorator/ConditionalTriggeredAbility.java index 28ba0af2cd9..5a4cae21da6 100644 --- a/Mage/src/mage/abilities/decorator/ConditionalTriggeredAbility.java +++ b/Mage/src/mage/abilities/decorator/ConditionalTriggeredAbility.java @@ -1,5 +1,6 @@ package mage.abilities.decorator; +import mage.MageObject; import mage.abilities.TriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.condition.Condition; @@ -74,4 +75,21 @@ public class ConditionalTriggeredAbility extends TriggeredAbilityImpl { return ability.getEffects(); } + @Override + public MageObject getSourceObjectIfItStillExists(Game game) { + return ability.getSourceObjectIfItStillExists(game); + } + + @Override + public MageObject getSourceObject(Game game) { + return ability.getSourceObject(game); + } + + + + @Override + public int getSourceObjectZoneChangeCounter() { + return ability.getSourceObjectZoneChangeCounter(); + } + } diff --git a/Mage/src/mage/abilities/dynamicvalue/common/MultikickerCount.java b/Mage/src/mage/abilities/dynamicvalue/common/MultikickerCount.java index 7ad2b93eebb..d52f265e435 100644 --- a/Mage/src/mage/abilities/dynamicvalue/common/MultikickerCount.java +++ b/Mage/src/mage/abilities/dynamicvalue/common/MultikickerCount.java @@ -50,7 +50,7 @@ public class MultikickerCount implements DynamicValue { if (card != null) { for (Ability ability: card.getAbilities()) { if (ability instanceof KickerAbility) { - count += ((KickerAbility) ability).getKickedCounter(game); + count += ((KickerAbility) ability).getKickedCounter(game, source); } } } diff --git a/Mage/src/mage/abilities/keyword/KickerAbility.java b/Mage/src/mage/abilities/keyword/KickerAbility.java index fef48ffa24d..6828e26a060 100644 --- a/Mage/src/mage/abilities/keyword/KickerAbility.java +++ b/Mage/src/mage/abilities/keyword/KickerAbility.java @@ -28,9 +28,11 @@ package mage.abilities.keyword; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import mage.abilities.Ability; import mage.abilities.SpellAbility; import mage.abilities.StaticAbility; @@ -42,7 +44,7 @@ import mage.abilities.costs.OptionalAdditionalSourceCosts; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.VariableManaCost; -import mage.cards.Card; +import mage.constants.AbilityType; import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; @@ -87,12 +89,12 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo protected static final String KICKER_REMINDER_MANA = "(You may pay an additional {cost} as you cast this spell.)"; protected static final String KICKER_REMINDER_COST = "(You may {cost} in addition to any other costs as you cast this spell.)"; + protected Map activations = new HashMap<>(); // zoneChangeCounter, activations + protected String keywordText; protected String reminderText; protected List kickerCosts = new LinkedList<>(); private int xManaValue = 0; - // needed to reset kicked status, if card changes zone after casting it - private int zoneChangeCounter = 0; public KickerAbility(String manaString) { this(KICKER_KEYWORD, KICKER_REMINDER_MANA); @@ -118,7 +120,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo this.keywordText = ability.keywordText; this.reminderText = ability.reminderText; this.xManaValue = ability.xManaValue; - this.zoneChangeCounter = ability.zoneChangeCounter; + this.activations.putAll(ability.activations); } @@ -143,35 +145,24 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo for (OptionalAdditionalCost cost: kickerCosts) { cost.reset(); } - zoneChangeCounter = 0; } public int getXManaValue() { return xManaValue; } - public int getKickedCounter(Game game) { - if (isKicked(game)) { - int counter = 0; - for (OptionalAdditionalCost cost: kickerCosts) { - counter += cost.getActivateCount(); - } - return counter; + public int getKickedCounter(Game game, Ability source) { + String key = getActivationKey(source, "", game); + if (activations.containsKey(key)) { + return activations.get(key); } return 0; } - public boolean isKicked(Game game) { - Card card = game.getCard(sourceId); - // kicked status counts only if card not changed zone since it was kicked - if (card != null && card.getZoneChangeCounter(game) <= zoneChangeCounter +1) { - for (OptionalAdditionalCost cost: kickerCosts) { - if(cost.isActivated()) { - return true; - } - } - } else { - this.resetKicker(); + public boolean isKicked(Game game, Ability source, String costText) { + String key = getActivationKey(source, costText, game); + if (activations.containsKey(key)) { + return activations.get(key) > 0; } return false; } @@ -180,19 +171,26 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo return kickerCosts; } - private void activateKicker(OptionalAdditionalCost kickerCost, Game game) { - kickerCost.activate(); - // remember zone change counter - if (zoneChangeCounter == 0) { - Card card = game.getCard(getSourceId()); - if (card != null) { - zoneChangeCounter = card.getZoneChangeCounter(game); - } else { - throw new IllegalArgumentException("Kicker source card not found"); - } + private void activateKicker(OptionalAdditionalCost kickerCost, Ability source, Game game) { + int amount = 1; + String key = getActivationKey(source, kickerCost.getText(true), game); + if (activations.containsKey(key)) { + amount += activations.get(key); } + activations.put(key, amount); } + private String getActivationKey(Ability source, String costText, Game game) { + int zcc = source.getSourceObjectZoneChangeCounter(); + if (source.getSourceObjectZoneChangeCounter() == 0) { + zcc = game.getState().getZoneChangeCounter(source.getSourceId()); + } + if (zcc > 0 && (source.getAbilityType().equals(AbilityType.TRIGGERED) || source.getAbilityType().equals(AbilityType.STATIC))) { + --zcc; + } + return String.valueOf(zcc) + ((kickerCosts.size() > 1) ? costText :""); + } + @Override public void addOptionalAdditionalCosts(Ability ability, Game game) { if (ability instanceof SpellAbility) { @@ -208,8 +206,8 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo times = Integer.toString(activatedCount + 1) + (activatedCount == 0 ? " time ":" times "); } if (kickerCost.canPay(ability, sourceId, controllerId, game) && - player.chooseUse(Outcome.Benefit, new StringBuilder("Pay ").append(times).append(kickerCost.getText(false)).append(" ?").toString(), game)) { - this.activateKicker(kickerCost, game); + player.chooseUse(Outcome.Benefit, "Pay " + times + kickerCost.getText(false) + " ?", game)) { + this.activateKicker(kickerCost, ability, game); for (Iterator it = ((Costs) kickerCost).iterator(); it.hasNext();) { Cost cost = (Cost) it.next(); if (cost instanceof ManaCostsImpl) {