mirror of
https://github.com/magefree/mage.git
synced 2025-12-22 11:32:00 -08:00
* Kicker - Fixed that kicker did not work correctly if the kicker card did change zone again before kicker dependant ability resolved.
This commit is contained in:
parent
14a8632f0f
commit
db5526a1c6
9 changed files with 145 additions and 55 deletions
|
|
@ -34,8 +34,6 @@ import mage.constants.Rarity;
|
||||||
import mage.MageInt;
|
import mage.MageInt;
|
||||||
import mage.abilities.Ability;
|
import mage.abilities.Ability;
|
||||||
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
|
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.dynamicvalue.common.MultikickerCount;
|
||||||
import mage.abilities.effects.common.discard.DiscardTargetEffect;
|
import mage.abilities.effects.common.discard.DiscardTargetEffect;
|
||||||
import mage.abilities.keyword.MultikickerAbility;
|
import mage.abilities.keyword.MultikickerAbility;
|
||||||
|
|
@ -61,10 +59,7 @@ public class BloodhuskRitualist extends CardImpl {
|
||||||
this.addAbility(new MultikickerAbility("{B}"));
|
this.addAbility(new MultikickerAbility("{B}"));
|
||||||
|
|
||||||
// When Bloodhusk Ritualist enters the battlefield, target opponent discards a card for each time it was kicked.
|
// When Bloodhusk Ritualist enters the battlefield, target opponent discards a card for each time it was kicked.
|
||||||
Ability ability = new ConditionalTriggeredAbility(
|
Ability ability = new EntersBattlefieldTriggeredAbility(new DiscardTargetEffect(new MultikickerCount()));
|
||||||
new EntersBattlefieldTriggeredAbility(new DiscardTargetEffect(new MultikickerCount())),
|
|
||||||
KickedCondition.getInstance(),
|
|
||||||
"");
|
|
||||||
ability.addTarget(new TargetOpponent());
|
ability.addTarget(new TargetOpponent());
|
||||||
this.addAbility(ability);
|
this.addAbility(ability);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ class RumblingAftershocksTriggeredAbility extends TriggeredAbilityImpl {
|
||||||
int damageAmount = 0;
|
int damageAmount = 0;
|
||||||
for (Ability ability: spell.getAbilities()) {
|
for (Ability ability: spell.getAbilities()) {
|
||||||
if (ability instanceof KickerAbility) {
|
if (ability instanceof KickerAbility) {
|
||||||
damageAmount += ((KickerAbility) ability).getKickedCounter(game);
|
damageAmount += ((KickerAbility) ability).getKickedCounter(game, spell.getSpellAbility());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (damageAmount > 0) {
|
if (damageAmount > 0) {
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
super(ownerId, 113, "Strength of the Tajuru", Rarity.RARE, new CardType[]{CardType.INSTANT}, "{X}{G}{G}");
|
||||||
this.expansionSetCode = "WWK";
|
this.expansionSetCode = "WWK";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Multikicker (You may pay an additional {1} any number of times as you cast this spell.)
|
// Multikicker (You may pay an additional {1} any number of times as you cast this spell.)
|
||||||
this.addAbility(new MultikickerAbility("{1}"));
|
this.addAbility(new MultikickerAbility("{1}"));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ package org.mage.test.cards.abilities.keywords;
|
||||||
import mage.constants.PhaseStep;
|
import mage.constants.PhaseStep;
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
import mage.counters.CounterType;
|
import mage.counters.CounterType;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||||
|
|
||||||
|
|
@ -203,4 +204,92 @@ public class KickerTest extends CardTestPlayerBase {
|
||||||
assertLife(playerB, 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() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ public class KickedCondition implements Condition {
|
||||||
if (card != null) {
|
if (card != null) {
|
||||||
for (Ability ability: card.getAbilities()) {
|
for (Ability ability: card.getAbilities()) {
|
||||||
if (ability instanceof KickerAbility) {
|
if (ability instanceof KickerAbility) {
|
||||||
if(((KickerAbility) ability).isKicked(game)) {
|
if(((KickerAbility) ability).isKicked(game, source, "")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,17 +24,9 @@ public class KickedCostCondition implements Condition {
|
||||||
public boolean apply(Game game, Ability source) {
|
public boolean apply(Game game, Ability source) {
|
||||||
Card card = game.getCard(source.getSourceId());
|
Card card = game.getCard(source.getSourceId());
|
||||||
if (card != null) {
|
if (card != null) {
|
||||||
KickerAbility kickerAbility = null;
|
|
||||||
for (Ability ability: card.getAbilities()) {
|
for (Ability ability: card.getAbilities()) {
|
||||||
if (ability instanceof KickerAbility) {
|
if (ability instanceof KickerAbility) {
|
||||||
kickerAbility = (KickerAbility) ability;
|
return ((KickerAbility) ability).isKicked(game, source, kickerCostText);
|
||||||
}
|
|
||||||
}
|
|
||||||
if (kickerAbility != null) {
|
|
||||||
for (OptionalAdditionalCost cost: kickerAbility.getKickerCosts()) {
|
|
||||||
if (cost.getText(true).equals(kickerCostText)) {
|
|
||||||
return cost.isActivated();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package mage.abilities.decorator;
|
package mage.abilities.decorator;
|
||||||
|
|
||||||
|
import mage.MageObject;
|
||||||
import mage.abilities.TriggeredAbility;
|
import mage.abilities.TriggeredAbility;
|
||||||
import mage.abilities.TriggeredAbilityImpl;
|
import mage.abilities.TriggeredAbilityImpl;
|
||||||
import mage.abilities.condition.Condition;
|
import mage.abilities.condition.Condition;
|
||||||
|
|
@ -74,4 +75,21 @@ public class ConditionalTriggeredAbility extends TriggeredAbilityImpl {
|
||||||
return ability.getEffects();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ public class MultikickerCount implements DynamicValue {
|
||||||
if (card != null) {
|
if (card != null) {
|
||||||
for (Ability ability: card.getAbilities()) {
|
for (Ability ability: card.getAbilities()) {
|
||||||
if (ability instanceof KickerAbility) {
|
if (ability instanceof KickerAbility) {
|
||||||
count += ((KickerAbility) ability).getKickedCounter(game);
|
count += ((KickerAbility) ability).getKickedCounter(game, source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,11 @@
|
||||||
|
|
||||||
package mage.abilities.keyword;
|
package mage.abilities.keyword;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import mage.abilities.Ability;
|
import mage.abilities.Ability;
|
||||||
import mage.abilities.SpellAbility;
|
import mage.abilities.SpellAbility;
|
||||||
import mage.abilities.StaticAbility;
|
import mage.abilities.StaticAbility;
|
||||||
|
|
@ -42,7 +44,7 @@ import mage.abilities.costs.OptionalAdditionalSourceCosts;
|
||||||
import mage.abilities.costs.mana.GenericManaCost;
|
import mage.abilities.costs.mana.GenericManaCost;
|
||||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||||
import mage.abilities.costs.mana.VariableManaCost;
|
import mage.abilities.costs.mana.VariableManaCost;
|
||||||
import mage.cards.Card;
|
import mage.constants.AbilityType;
|
||||||
import mage.constants.Outcome;
|
import mage.constants.Outcome;
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
import mage.game.Game;
|
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_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 static final String KICKER_REMINDER_COST = "(You may {cost} in addition to any other costs as you cast this spell.)";
|
||||||
|
|
||||||
|
protected Map<String, Integer> activations = new HashMap<>(); // zoneChangeCounter, activations
|
||||||
|
|
||||||
protected String keywordText;
|
protected String keywordText;
|
||||||
protected String reminderText;
|
protected String reminderText;
|
||||||
protected List<OptionalAdditionalCost> kickerCosts = new LinkedList<>();
|
protected List<OptionalAdditionalCost> kickerCosts = new LinkedList<>();
|
||||||
private int xManaValue = 0;
|
private int xManaValue = 0;
|
||||||
// needed to reset kicked status, if card changes zone after casting it
|
|
||||||
private int zoneChangeCounter = 0;
|
|
||||||
|
|
||||||
public KickerAbility(String manaString) {
|
public KickerAbility(String manaString) {
|
||||||
this(KICKER_KEYWORD, KICKER_REMINDER_MANA);
|
this(KICKER_KEYWORD, KICKER_REMINDER_MANA);
|
||||||
|
|
@ -118,7 +120,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
|
||||||
this.keywordText = ability.keywordText;
|
this.keywordText = ability.keywordText;
|
||||||
this.reminderText = ability.reminderText;
|
this.reminderText = ability.reminderText;
|
||||||
this.xManaValue = ability.xManaValue;
|
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) {
|
for (OptionalAdditionalCost cost: kickerCosts) {
|
||||||
cost.reset();
|
cost.reset();
|
||||||
}
|
}
|
||||||
zoneChangeCounter = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getXManaValue() {
|
public int getXManaValue() {
|
||||||
return xManaValue;
|
return xManaValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getKickedCounter(Game game) {
|
public int getKickedCounter(Game game, Ability source) {
|
||||||
if (isKicked(game)) {
|
String key = getActivationKey(source, "", game);
|
||||||
int counter = 0;
|
if (activations.containsKey(key)) {
|
||||||
for (OptionalAdditionalCost cost: kickerCosts) {
|
return activations.get(key);
|
||||||
counter += cost.getActivateCount();
|
|
||||||
}
|
|
||||||
return counter;
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isKicked(Game game) {
|
public boolean isKicked(Game game, Ability source, String costText) {
|
||||||
Card card = game.getCard(sourceId);
|
String key = getActivationKey(source, costText, game);
|
||||||
// kicked status counts only if card not changed zone since it was kicked
|
if (activations.containsKey(key)) {
|
||||||
if (card != null && card.getZoneChangeCounter(game) <= zoneChangeCounter +1) {
|
return activations.get(key) > 0;
|
||||||
for (OptionalAdditionalCost cost: kickerCosts) {
|
|
||||||
if(cost.isActivated()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.resetKicker();
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -180,17 +171,24 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
|
||||||
return kickerCosts;
|
return kickerCosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void activateKicker(OptionalAdditionalCost kickerCost, Game game) {
|
private void activateKicker(OptionalAdditionalCost kickerCost, Ability source, Game game) {
|
||||||
kickerCost.activate();
|
int amount = 1;
|
||||||
// remember zone change counter
|
String key = getActivationKey(source, kickerCost.getText(true), game);
|
||||||
if (zoneChangeCounter == 0) {
|
if (activations.containsKey(key)) {
|
||||||
Card card = game.getCard(getSourceId());
|
amount += activations.get(key);
|
||||||
if (card != null) {
|
|
||||||
zoneChangeCounter = card.getZoneChangeCounter(game);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Kicker source card not found");
|
|
||||||
}
|
}
|
||||||
|
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
|
@Override
|
||||||
|
|
@ -208,8 +206,8 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
|
||||||
times = Integer.toString(activatedCount + 1) + (activatedCount == 0 ? " time ":" times ");
|
times = Integer.toString(activatedCount + 1) + (activatedCount == 0 ? " time ":" times ");
|
||||||
}
|
}
|
||||||
if (kickerCost.canPay(ability, sourceId, controllerId, game) &&
|
if (kickerCost.canPay(ability, sourceId, controllerId, game) &&
|
||||||
player.chooseUse(Outcome.Benefit, new StringBuilder("Pay ").append(times).append(kickerCost.getText(false)).append(" ?").toString(), game)) {
|
player.chooseUse(Outcome.Benefit, "Pay " + times + kickerCost.getText(false) + " ?", game)) {
|
||||||
this.activateKicker(kickerCost, game);
|
this.activateKicker(kickerCost, ability, game);
|
||||||
for (Iterator it = ((Costs) kickerCost).iterator(); it.hasNext();) {
|
for (Iterator it = ((Costs) kickerCost).iterator(); it.hasNext();) {
|
||||||
Cost cost = (Cost) it.next();
|
Cost cost = (Cost) it.next();
|
||||||
if (cost instanceof ManaCostsImpl) {
|
if (cost instanceof ManaCostsImpl) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue