* Kicker - added support of X and mana cost interactions like Rosheen Meanderer + Verdeloth the Ancient combo (#3538);

* Rosheen Meanderer - fixed that mana can be payed for mana cost with X instead any cost with X (#3538);
This commit is contained in:
Oleg Agafonov 2019-06-18 11:28:41 +04:00
parent 49fc094546
commit cc54a92daa
7 changed files with 216 additions and 147 deletions

View file

@ -1,22 +1,17 @@
package mage.cards.k;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.common.KickedCondition;
import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.CountersSourceCount;
import mage.abilities.effects.Effect;
import mage.abilities.dynamicvalue.common.GetKickerXValue;
import mage.abilities.effects.common.continuous.BoostAllEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.KickerAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
@ -24,10 +19,10 @@ import mage.counters.CounterType;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.SubtypePredicate;
import mage.filter.predicate.permanent.AnotherPredicate;
import mage.game.Game;
import java.util.UUID;
/**
*
* @author emerald000
*/
public final class KangeeAerieKeeper extends CardImpl {
@ -55,7 +50,7 @@ public final class KangeeAerieKeeper extends CardImpl {
this.addAbility(FlyingAbility.getInstance());
// When Kangee, Aerie Keeper enters the battlefield, if it was kicked, put X feather counters on it.
TriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new AddCountersSourceEffect(CounterType.FEATHER.createInstance(), new KangeeAerieKeeperGetKickerXValue(), true));
TriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new AddCountersSourceEffect(CounterType.FEATHER.createInstance(), GetKickerXValue.instance, true));
this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, KickedCondition.instance, "When {this} enters the battlefield, if it was kicked, put X feather counters on it."));
// Other Bird creatures get +1/+1 for each feather counter on Kangee, Aerie Keeper.
@ -71,38 +66,3 @@ public final class KangeeAerieKeeper extends CardImpl {
return new KangeeAerieKeeper(this);
}
}
class KangeeAerieKeeperGetKickerXValue implements DynamicValue {
public KangeeAerieKeeperGetKickerXValue() {
}
@Override
public int calculate(Game game, Ability source, Effect effect) {
int count = 0;
Card card = game.getCard(source.getSourceId());
if (card != null) {
for (Ability ability : card.getAbilities()) {
if (ability instanceof KickerAbility) {
count += ((KickerAbility) ability).getXManaValue();
}
}
}
return count;
}
@Override
public KangeeAerieKeeperGetKickerXValue copy() {
return new KangeeAerieKeeperGetKickerXValue();
}
@Override
public String toString() {
return "X";
}
@Override
public String getMessage() {
return "X";
}
}

View file

@ -1,10 +1,7 @@
package mage.cards.r;
import java.util.UUID;
import mage.ConditionalMana;
import mage.MageInt;
import mage.MageObject;
import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
@ -12,20 +9,20 @@ import mage.abilities.effects.mana.BasicManaEffect;
import mage.abilities.mana.BasicManaAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.AbilityType;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.game.Game;
import java.util.UUID;
/**
*
* @author jeffwadsworth
*/
public final class RosheenMeanderer extends CardImpl {
public RosheenMeanderer(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{R/G}");
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R/G}");
addSuperType(SuperType.LEGENDARY);
this.subtype.add(SubType.GIANT);
this.subtype.add(SubType.SHAMAN);
@ -75,15 +72,16 @@ class RosheenMeandererConditionalMana extends ConditionalMana {
class RosheenMeandererManaCondition implements Condition {
/*
A cost that contains {X} may be a spells total cost, an activated abilitys cost, a suspend cost, or a cost youre
asked to pay as part of the resolution of a spell or ability (such as Condescend). A spells total cost includes either
its mana cost (printed in the upper right corner) or its alternative cost (such as flashback), as well as any additional
costs (such as kicker). If its something you can spend mana on, its a cost. If that cost includes the {X} symbol in it,
you can spend mana generated by Rosheen on that cost. (2017-11-17)
*/
@Override
public boolean apply(Game game, Ability source) {
if (AbilityType.SPELL == source.getAbilityType()) {
MageObject object = game.getObject(source.getSourceId());
return object != null
&& object.getManaCost().getText().contains("X");
} else {
return source.getManaCosts().getText().contains("X");
}
return source.getManaCostsToPay().containsX();
}
}

View file

@ -1,19 +1,14 @@
package mage.cards.v;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.common.KickedCondition;
import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.dynamicvalue.common.GetKickerXValue;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.continuous.BoostAllEffect;
import mage.abilities.keyword.KickerAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
@ -21,17 +16,17 @@ import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.SubtypePredicate;
import mage.filter.predicate.permanent.PermanentIdPredicate;
import mage.game.Game;
import mage.game.permanent.token.SaprolingToken;
import java.util.UUID;
/**
*
* @author LevelX2
*/
public final class VerdelothTheAncient extends CardImpl {
public VerdelothTheAncient(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{4}{G}{G}");
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}{G}");
addSuperType(SuperType.LEGENDARY);
this.subtype.add(SubType.TREEFOLK);
@ -40,22 +35,22 @@ public final class VerdelothTheAncient extends CardImpl {
// Kicker {X}
this.addAbility(new KickerAbility("{X}"));
// Saproling creatures and other Treefolk creatures get +1/+1.
FilterCreaturePermanent filter = new FilterCreaturePermanent("Saproling creatures and other Treefolk creatures");
filter.add(Predicates.or(
Predicates.and(new SubtypePredicate(SubType.TREEFOLK), Predicates.not(new PermanentIdPredicate(this.getId()))),
new SubtypePredicate(SubType.SAPROLING))
);
);
filter.add(Predicates.not(new PermanentIdPredicate(this.getId())));
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostAllEffect(1,1, Duration.WhileOnBattlefield, filter, false)));
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostAllEffect(1, 1, Duration.WhileOnBattlefield, filter, false)));
// When Verdeloth the Ancient enters the battlefield, if it was kicked, create X 1/1 green Saproling creature tokens.
EntersBattlefieldTriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new SaprolingToken(), new GetKickerXValue()), false);
EntersBattlefieldTriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new SaprolingToken(), GetKickerXValue.instance), false);
this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, KickedCondition.instance,
"When {this} enters the battlefield, if it was kicked, create X 1/1 green Saproling creature tokens."));
}
public VerdelothTheAncient(final VerdelothTheAncient card) {
@ -66,39 +61,4 @@ public final class VerdelothTheAncient extends CardImpl {
public VerdelothTheAncient copy() {
return new VerdelothTheAncient(this);
}
}
class GetKickerXValue implements DynamicValue {
public GetKickerXValue() {
}
@Override
public int calculate(Game game, Ability source, Effect effect) {
int count = 0;
Card card = game.getCard(source.getSourceId());
if (card != null) {
for (Ability ability: card.getAbilities()) {
if (ability instanceof KickerAbility) {
count += ((KickerAbility) ability).getXManaValue();
}
}
}
return count;
}
@Override
public GetKickerXValue copy() {
return new GetKickerXValue();
}
@Override
public String toString() {
return "X";
}
@Override
public String getMessage() {
return "X";
}
}

View file

@ -0,0 +1,96 @@
package org.mage.test.cards.mana;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author JayDi85
*/
public class RosheenMeandererManaXTest extends CardTestPlayerBase {
// https://github.com/magefree/mage/issues/3538
// Rosheen Meanderer {3}{R/G}
// {T}: Add {C}{C}{C}{C}. Spend this mana only on costs that contain {X}.
// Verdeloth the Ancient {4}{G}{G}
// Kicker {X} (You may pay an additional {X} as you cast this spell.)
// Saproling creatures and other Treefolk creatures get +1/+1.
// When Verdeloth the Ancient enters the battlefield, if it was kicked, create X 1/1 green Saproling creature tokens.
@Test
public void test_SimpleKicker() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6 + 2);
addCard(Zone.HAND, playerA, "Verdeloth the Ancient");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Verdeloth the Ancient");
setChoice(playerA, "Yes"); // use kicker
setChoice(playerA, "X=2");
checkPermanentCount("after", 1, PhaseStep.END_TURN, playerA, "Verdeloth the Ancient", 1);
checkPermanentCount("after", 1, PhaseStep.END_TURN, playerA, "Saproling", 2);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
}
@Test
public void test_KickerWithXMana() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6 + 2 - 4);
addCard(Zone.HAND, playerA, "Verdeloth the Ancient");
//
addCard(Zone.BATTLEFIELD, playerA, "Rosheen Meanderer");
// make 4 mana for X pay
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {C}");
checkManaPool("mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "C", 4);
// cast kicker X and use extra 4 mana
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Verdeloth the Ancient");
setChoice(playerA, "Yes"); // use kicker
setChoice(playerA, "X=2");
checkPermanentCount("after", 1, PhaseStep.END_TURN, playerA, "Verdeloth the Ancient", 1);
checkPermanentCount("after", 1, PhaseStep.END_TURN, playerA, "Saproling", 2);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
}
@Test
public void test_KickerWithXZero() {
// You can spend mana generated by Rosheen on a cost that includes {X} even if youve chosen an X of 0,
// or if the card specifies that you can spend only colored mana on X. (Youll have to spend Rosheens mana on
// a different part of that cost, of course.) (2017-11-17)
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6 - 4);
addCard(Zone.HAND, playerA, "Verdeloth the Ancient");
//
addCard(Zone.BATTLEFIELD, playerA, "Rosheen Meanderer");
// make 4 mana for X pay
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {C}");
checkManaPool("mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "C", 4);
// cast kicker X and use extra 4 mana
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Verdeloth the Ancient");
setChoice(playerA, "Yes"); // use kicker
setChoice(playerA, "X=0");
checkPermanentCount("after", 1, PhaseStep.END_TURN, playerA, "Verdeloth the Ancient", 1);
checkPermanentCount("after", 1, PhaseStep.END_TURN, playerA, "Saproling", 0);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
}
}

View file

@ -1129,7 +1129,9 @@ public class TestPlayer implements Player {
}
private void assertManaPoolInner(PlayerAction action, Player player, ManaType manaType, Integer amount) {
Integer current = player.getManaPool().get(manaType);
Integer normal = player.getManaPool().getMana().get(manaType);
Integer conditional = player.getManaPool().getConditionalMana().stream().mapToInt(a -> a.get(manaType)).sum(); // calcs FULL conditional mana, not real conditions
Integer current = normal + conditional;
Assert.assertEquals(action.getActionName() + " - mana pool must contain [" + amount.toString() + " " + manaType.toString() + "], but found [" + current.toString() + "]", amount, current);
}

View file

@ -0,0 +1,73 @@
package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.costs.OptionalAdditionalCost;
import mage.abilities.costs.OptionalAdditionalCostImpl;
import mage.abilities.costs.VariableCost;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.keyword.KickerAbility;
import mage.game.Game;
import mage.game.stack.Spell;
import java.util.List;
/**
* @author JayDi85
*/
public enum GetKickerXValue implements DynamicValue {
instance;
@Override
public int calculate(Game game, Ability source, Effect effect) {
// calcs only kicker with X values
// kicker adds additional costs to spell ability
// only one X value per card possible
// kicker can be calls multiple times (use getKickedCounter)
int finalValue = 0;
Spell spell = game.getSpellOrLKIStack(source.getSourceId());
if (spell != null && spell.getSpellAbility() != null) {
int xValue = spell.getSpellAbility().getManaCostsToPay().getX();
for (Ability ability : spell.getAbilities()) {
if (ability instanceof KickerAbility) {
// search that kicker used X value
KickerAbility kickerAbility = (KickerAbility) ability;
boolean haveVarCost = false;
for (OptionalAdditionalCost cost : kickerAbility.getKickerCosts()) {
List<VariableCost> varCosts = ((OptionalAdditionalCostImpl) cost).getVariableCosts();
if (!varCosts.isEmpty()) {
haveVarCost = true;
break;
}
}
if (haveVarCost) {
int kickedCount = ((KickerAbility) ability).getKickedCounter(game, source);
if (kickedCount > 0) {
finalValue += kickedCount * xValue;
}
}
}
}
}
return finalValue;
}
@Override
public GetKickerXValue copy() {
return GetKickerXValue.instance;
}
@Override
public String toString() {
return "X";
}
@Override
public String getMessage() {
return "";
}
}

View file

@ -1,15 +1,10 @@
package mage.abilities.keyword;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility;
import mage.abilities.costs.*;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.costs.mana.VariableManaCost;
import mage.constants.AbilityType;
import mage.constants.Outcome;
import mage.constants.Zone;
@ -17,6 +12,12 @@ import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 20121001 702.31. Kicker 702.31a Kicker is a static ability that functions
* while the spell with kicker is on the stack. "Kicker [cost]" means "You may
@ -57,7 +58,6 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
protected String keywordText;
protected String reminderText;
protected List<OptionalAdditionalCost> kickerCosts = new LinkedList<>();
private int xManaValue = 0;
public KickerAbility(String manaString) {
this(KICKER_KEYWORD, KICKER_REMINDER_MANA);
@ -79,10 +79,11 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
public KickerAbility(final KickerAbility ability) {
super(ability);
this.kickerCosts.addAll(ability.kickerCosts);
for (OptionalAdditionalCost cost : ability.kickerCosts) {
this.kickerCosts.add((OptionalAdditionalCost) cost.copy());
}
this.keywordText = ability.keywordText;
this.reminderText = ability.reminderText;
this.xManaValue = ability.xManaValue;
this.activations.putAll(ability.activations);
}
@ -108,7 +109,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
cost.reset();
}
String key = getActivationKey(source, "", game);
for (Iterator<String> iterator = activations.keySet().iterator(); iterator.hasNext();) {
for (Iterator<String> iterator = activations.keySet().iterator(); iterator.hasNext(); ) {
String activationKey = iterator.next();
if (activationKey.startsWith(key) && activations.get(activationKey) > 0) {
activations.put(key, 0);
@ -116,10 +117,6 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
}
}
public int getXManaValue() {
return xManaValue;
}
public int getKickedCounter(Game game, Ability source) {
String key = getActivationKey(source, "", game);
return activations.getOrDefault(key, 0);
@ -167,7 +164,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
if (zcc > 0 && (source.getAbilityType() == AbilityType.TRIGGERED)) {
--zcc;
}
return String.valueOf(zcc) + ((kickerCosts.size() > 1) ? costText : "");
return zcc + ((kickerCosts.size() > 1) ? costText : "");
}
@Override
@ -182,16 +179,16 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
String times = "";
if (kickerCost.isRepeatable()) {
int activatedCount = getKickedCounter(game, ability);
times = Integer.toString(activatedCount + 1) + (activatedCount == 0 ? " time " : " times ");
times = (activatedCount + 1) + (activatedCount == 0 ? " time " : " times ");
}
if (kickerCost.canPay(ability, sourceId, controllerId, game)
&& player.chooseUse(Outcome.Benefit, "Pay " + times + kickerCost.getText(false) + " ?", ability, game)) {
this.activateKicker(kickerCost, ability, game);
if (kickerCost instanceof Costs) {
for (Iterator itKickerCost = ((Costs) kickerCost).iterator(); itKickerCost.hasNext();) {
for (Iterator itKickerCost = ((Costs) kickerCost).iterator(); itKickerCost.hasNext(); ) {
Object kickerCostObject = itKickerCost.next();
if ((kickerCostObject instanceof Costs) || (kickerCostObject instanceof CostsImpl)) {
for (@SuppressWarnings("unchecked") Iterator<Cost> itDetails = ((Costs) kickerCostObject).iterator(); itDetails.hasNext();) {
for (@SuppressWarnings("unchecked") Iterator<Cost> itDetails = ((Costs) kickerCostObject).iterator(); itDetails.hasNext(); ) {
addKickerCostsToAbility(itDetails.next(), ability, game);
}
} else {
@ -199,7 +196,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
}
}
} else {
addKickerCostsToAbility((Cost) kickerCost, ability, game);
addKickerCostsToAbility(kickerCost, ability, game);
}
again = kickerCost.isRepeatable();
} else {
@ -212,26 +209,9 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
}
private void addKickerCostsToAbility(Cost cost, Ability ability, Game game) {
// can contains multiple costs from multikicker ability
if (cost instanceof ManaCostsImpl) {
@SuppressWarnings("unchecked")
List<VariableManaCost> varCosts = ((ManaCostsImpl) cost).getVariableCosts();
if (!varCosts.isEmpty()) {
// use only first variable cost
xManaValue = game.getPlayer(this.controllerId).announceXMana(varCosts.get(0).getMinX(), Integer.MAX_VALUE, "Announce kicker value for " + varCosts.get(0).getText(), game, this);
// kicker variable X costs handled internally as multikicker with {1} cost (no multikicker on card)
if (!game.isSimulation()) {
game.informPlayers(game.getPlayer(this.controllerId).getLogName() + " announced a value of " + xManaValue + " for " + " kicker X ");
}
ability.getManaCostsToPay().add(new GenericManaCost(xManaValue));
ManaCostsImpl<ManaCost> kickerManaCosts = (ManaCostsImpl) cost;
for (ManaCost manaCost : kickerManaCosts) {
if (!(manaCost instanceof VariableManaCost)) {
ability.getManaCostsToPay().add(manaCost.copy());
}
}
} else {
ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy());
}
ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy());
} else {
ability.getCosts().add(cost.copy());
}