* Additional costs - added support of X costs on free cast (example: Kicker X, see Thieving Skydiver and Etali, Primal Storm combo);

* As an additional cost discard X cards - fixed wrong text (example: Channeled Force, Firestorm);
This commit is contained in:
Oleg Agafonov 2021-08-05 16:18:04 +04:00
parent d62cf17422
commit 53aababd44
65 changed files with 483 additions and 417 deletions

View file

@ -259,7 +259,7 @@ public abstract class AbilityImpl implements Ability {
if (!this.getManaCostsToPay().getVariableCosts().isEmpty()) {
int xValue = this.getManaCostsToPay().getX();
this.getManaCostsToPay().clear();
VariableManaCost xCosts = new VariableManaCost();
VariableManaCost xCosts = new VariableManaCost(VariableCostType.ADDITIONAL);
// no x events - rules from Unbound Flourishing:
// - Spells with additional costs that include X won't be affected by Unbound Flourishing. X must be in the spell's mana cost.
xCosts.setAmount(xValue, xValue, false);
@ -555,9 +555,17 @@ public abstract class AbilityImpl implements Ability {
* @return variableManaCost for posting to log later
*/
protected VariableManaCost handleManaXCosts(Game game, boolean noMana, Player controller) {
// 20121001 - 601.2b
// If the spell has a variable cost that will be paid as it's being cast (such as an {X} in
// its mana cost; see rule 107.3), the player announces the value of that variable.
// 20210723 - 601.2b
// If the spell has alternative or additional costs that will
// be paid as its being cast such as buyback or kicker costs (see rules 118.8 and 118.9),
// the player announces their intentions to pay any or all of those costs (see rule 601.2f).
// A player cant apply two alternative methods of casting or two alternative costs to a
// single spell. If the spell has a variable cost that will be paid as its being cast
// (such as an {X} in its mana cost; see rule 107.3), the player announces the value of that
// variable. If the value of that variable is defined in the text of the spell by a choice
// that player would make later in the announcement or resolution of the spell, that player
// makes that choice at this time instead of that later time.
// TODO: Handle announcing other variable costs here like: RemoveVariableCountersSourceCost
VariableManaCost variableManaCost = null;
for (ManaCost cost : manaCostsToPay) {
@ -574,7 +582,7 @@ public abstract class AbilityImpl implements Ability {
if (!variableManaCost.isPaid()) { // should only happen for human players
int xValue;
int xValueMultiplier = handleManaXMultiplier(game, 1);
if (!noMana) {
if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) {
xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(), xValueMultiplier,
"Announce the value for " + variableManaCost.getText(), game, this);
int amountMana = xValue * variableManaCost.getXInstancesCount();

View file

@ -1,8 +1,11 @@
package mage.abilities.costs;
import mage.util.Copyable;
/**
* Virtual optional/additional cost, it must be tranformed to simple cost on resolve in your custom ability.
* Don't forget to set up cost type for variable costs
* <p>
* Example: KickerAbility.
*
* @author LevelX2
*/
public interface OptionalAdditionalCost extends Cost {
@ -77,6 +80,13 @@ public interface OptionalAdditionalCost extends Cost {
*/
int getActivateCount();
/**
* Set cost type to variable costs like additional or normal (example: Kicker)
*
* @param costType
*/
void setCostType(VariableCostType costType);
@Override
OptionalAdditionalCost copy();
}

View file

@ -166,6 +166,12 @@ public class OptionalAdditionalCostImpl extends CostsImpl<Cost> implements Optio
return activatedCounter;
}
@Override
public void setCostType(VariableCostType costType) {
this.getVariableCosts().forEach(cost -> {
cost.setCostType(costType);
});
}
@Override
public OptionalAdditionalCostImpl copy() {

View file

@ -13,6 +13,12 @@ import mage.game.Game;
*/
public interface OptionalAdditionalSourceCosts {
/**
* Warning, don't forget to set up cost type for costs, it can help with X announce
*
* @param ability
* @param game
*/
// TODO: add AI support to use buyback, replicate and other additional costs (current version can't calc available mana before buyback use)
void addOptionalAdditionalCosts(Ability ability, Game game);

View file

@ -64,4 +64,8 @@ public interface VariableCost {
* @return
*/
Cost getFixedCostsFromAnnouncedValue(int xValue);
VariableCostType getCostType();
void setCostType(VariableCostType costType);
}

View file

@ -16,6 +16,7 @@ import java.util.UUID;
public abstract class VariableCostImpl implements Cost, VariableCost {
protected UUID id;
protected VariableCostType costType;
protected String text;
protected boolean paid;
protected Targets targets;
@ -23,8 +24,8 @@ public abstract class VariableCostImpl implements Cost, VariableCost {
protected String xText;
protected String actionText;
public VariableCostImpl(String actionText) {
this("X", actionText);
public VariableCostImpl(VariableCostType costType, String actionText) {
this(costType, "X", actionText);
}
/**
@ -32,17 +33,19 @@ public abstract class VariableCostImpl implements Cost, VariableCost {
* @param actionText what happens with the value (e.g. "to tap", "to exile
* from your graveyard")
*/
public VariableCostImpl(String xText, String actionText) {
id = UUID.randomUUID();
paid = false;
targets = new Targets();
amountPaid = 0;
public VariableCostImpl(VariableCostType costType, String xText, String actionText) {
this.id = UUID.randomUUID();
this.costType = costType;
this.paid = false;
this.targets = new Targets();
this.amountPaid = 0;
this.xText = xText;
this.actionText = actionText;
}
public VariableCostImpl(final VariableCostImpl cost) {
this.id = cost.id;
this.costType = cost.costType;
this.text = cost.text;
this.paid = cost.paid;
this.targets = cost.targets.copy();
@ -107,14 +110,12 @@ public abstract class VariableCostImpl implements Cost, VariableCost {
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
return true;
/* not used */
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
return true;
/* not used */
}
@Override
@ -150,4 +151,14 @@ public abstract class VariableCostImpl implements Cost, VariableCost {
}
return xValue;
}
@Override
public VariableCostType getCostType() {
return this.costType;
}
@Override
public void setCostType(VariableCostType costType) {
this.costType = costType;
}
}

View file

@ -0,0 +1,24 @@
package mage.abilities.costs;
/**
* See rules 601.2b
*
* @author JayDi85
*/
public enum VariableCostType {
NORMAL(false),
ALTERNATIVE(false),
ADDITIONAL(true);
// allows announcing X value on free cast (noMana) for additional costs, example: Kicker X
private final boolean canUseAnnounceOnFreeCast;
VariableCostType(boolean canUseAnnounceOnFreeCast) {
this.canUseAnnounceOnFreeCast = canUseAnnounceOnFreeCast;
}
public boolean canUseAnnounceOnFreeCast() {
return this.canUseAnnounceOnFreeCast;
}
}

View file

@ -3,6 +3,7 @@ package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
@ -19,9 +20,10 @@ public class DiscardXTargetCost extends VariableCostImpl {
this(filter, false);
}
public DiscardXTargetCost(FilterCard filter, boolean additionalCostText) {
super(filter.getMessage() + " to discard");
this.text = (additionalCostText ? "as an additional cost to cast this spell, discard " : "Discard ") + xText + ' ' + filter.getMessage();
public DiscardXTargetCost(FilterCard filter, boolean useAsAdditionalCost) {
super(useAsAdditionalCost ? VariableCostType.ADDITIONAL : VariableCostType.NORMAL,
filter.getMessage() + " to discard");
this.text = (useAsAdditionalCost ? "as an additional cost to cast this spell, discard " : "Discard ") + xText + ' ' + filter.getMessage();
this.filter = filter;
}

View file

@ -3,6 +3,7 @@ package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.mana.VariableManaCost;
import mage.cards.Card;
import mage.cards.Cards;
@ -23,7 +24,7 @@ import java.util.UUID;
public class ExileFromHandCost extends CostImpl {
List<Card> cards = new ArrayList<>();
private boolean setXFromCMC;
private final boolean setXFromCMC;
public ExileFromHandCost(TargetCardInHand target) {
this(target, false);
@ -32,7 +33,7 @@ public class ExileFromHandCost extends CostImpl {
/**
* @param target
* @param setXFromCMC the spells X value on the stack is set to the
* converted mana costs of the exiled card
* converted mana costs of the exiled card (alternative cost)
*/
public ExileFromHandCost(TargetCardInHand target, boolean setXFromCMC) {
this.addTarget(target);
@ -66,9 +67,10 @@ public class ExileFromHandCost extends CostImpl {
player.moveCards(cardsToExile, Zone.EXILED, ability, game);
paid = true;
if (setXFromCMC) {
VariableManaCost vmc = new VariableManaCost();
VariableManaCost vmc = new VariableManaCost(VariableCostType.ALTERNATIVE);
// no x events - rules from Unbound Flourishing:
// - Spells with additional costs that include X won't be affected by Unbound Flourishing. X must be in the spell's mana cost.
// TODO: wtf, look at setXFromCMC usage -- it used in cards with alternative costs, not additional... need to fix?
vmc.setAmount(cmc, cmc, false);
vmc.setPaid();
ability.getManaCostsToPay().add(vmc);

View file

@ -1,16 +1,15 @@
package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetCardInYourGraveyard;
/**
*
* @author LevelX2
*/
public class ExileXFromYourGraveCost extends VariableCostImpl {
@ -21,10 +20,11 @@ public class ExileXFromYourGraveCost extends VariableCostImpl {
this(filter, false);
}
public ExileXFromYourGraveCost(FilterCard filter, boolean additionalCostText) {
super(filter.getMessage() + " to exile");
public ExileXFromYourGraveCost(FilterCard filter, boolean useAsAdditionalCost) {
super(useAsAdditionalCost ? VariableCostType.ADDITIONAL : VariableCostType.NORMAL,
filter.getMessage() + " to exile");
this.filter = filter;
this.text = (additionalCostText ? "as an additional cost to cast this spell, exile " : "Exile ") + xText + ' ' + filter.getMessage();
this.text = (useAsAdditionalCost ? "as an additional cost to cast this spell, exile " : "Exile ") + xText + ' ' + filter.getMessage();
}
public ExileXFromYourGraveCost(final ExileXFromYourGraveCost cost) {

View file

@ -3,11 +3,11 @@ package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.game.Game;
import mage.players.Player;
/**
*
* @author LevelX2
*/
public class PayVariableLifeCost extends VariableCostImpl {
@ -16,9 +16,10 @@ public class PayVariableLifeCost extends VariableCostImpl {
this(false);
}
public PayVariableLifeCost(boolean additionalCostText) {
super("life to pay");
this.text = new StringBuilder(additionalCostText ? "as an additional cost to cast this spell, pay " : "Pay ")
public PayVariableLifeCost(boolean useAsAdditionalCost) {
super(useAsAdditionalCost ? VariableCostType.ADDITIONAL : VariableCostType.NORMAL,
"life to pay");
this.text = new StringBuilder(useAsAdditionalCost ? "as an additional cost to cast this spell, pay " : "Pay ")
.append(xText).append(' ').append("life").toString();
}

View file

@ -4,6 +4,7 @@ import mage.abilities.Ability;
import mage.abilities.LoyaltyAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.permanent.Permanent;
@ -24,7 +25,7 @@ public class PayVariableLoyaltyCost extends VariableCostImpl {
private int costModification = 0;
public PayVariableLoyaltyCost() {
super("loyality counters to remove");
super(VariableCostType.NORMAL, "loyality counters to remove");
this.text = "-X";
}

View file

@ -1,21 +1,20 @@
package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.counters.Counter;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
*
* @author LevelX2
*/
public class RemoveVariableCountersSourceCost extends VariableCostImpl {
protected int minimalCountersToPay = 0;
private String counterName;
private final String counterName;
public RemoveVariableCountersSourceCost(Counter counter) {
this(counter, 0);
@ -30,7 +29,7 @@ public class RemoveVariableCountersSourceCost extends VariableCostImpl {
}
public RemoveVariableCountersSourceCost(Counter counter, int minimalCountersToPay, String text) {
super(counter.getName() + " counters to remove");
super(VariableCostType.NORMAL, counter.getName() + " counters to remove");
this.minimalCountersToPay = minimalCountersToPay;
this.counterName = counter.getName();
if (text == null || text.isEmpty()) {

View file

@ -1,11 +1,9 @@
package mage.abilities.costs.common;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.counters.Counter;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
@ -13,11 +11,12 @@ import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.TargetPermanent;
import java.util.UUID;
/**
*
* @author LevelX
*/
public class RemoveVariableCountersTargetCost extends VariableCostImpl {
public class RemoveVariableCountersTargetCost extends VariableCostImpl {
protected FilterPermanent filter;
protected CounterType counterTypeToRemove;
@ -32,7 +31,7 @@ public class RemoveVariableCountersTargetCost extends VariableCostImpl {
}
public RemoveVariableCountersTargetCost(FilterPermanent filter, CounterType counterTypeToRemove, String xText, int minValue) {
super(xText, new StringBuilder(counterTypeToRemove != null ? counterTypeToRemove.getName() + ' ' :"").append("counters to remove").toString());
super(VariableCostType.NORMAL, xText, new StringBuilder(counterTypeToRemove != null ? counterTypeToRemove.getName() + ' ' : "").append("counters to remove").toString());
this.filter = filter;
this.counterTypeToRemove = counterTypeToRemove;
this.text = setText();
@ -67,11 +66,11 @@ public class RemoveVariableCountersTargetCost extends VariableCostImpl {
@Override
public int getMaxValue(Ability source, Game game) {
int maxValue = 0;
for (Permanent permanent :game.getBattlefield().getAllActivePermanents(filter, source.getControllerId(), game)) {
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, source.getControllerId(), game)) {
if (counterTypeToRemove != null) {
maxValue += permanent.getCounters(game).getCount(counterTypeToRemove);
} else {
for(Counter counter :permanent.getCounters(game).values()){
for (Counter counter : permanent.getCounters(game).values()) {
maxValue += counter.getCount();
}
}
@ -81,7 +80,7 @@ public class RemoveVariableCountersTargetCost extends VariableCostImpl {
@Override
public Cost getFixedCostsFromAnnouncedValue(int xValue) {
return new RemoveCounterCost(new TargetPermanent(minValue,Integer.MAX_VALUE, filter, true), counterTypeToRemove, xValue);
return new RemoveCounterCost(new TargetPermanent(minValue, Integer.MAX_VALUE, filter, true), counterTypeToRemove, xValue);
}
}

View file

@ -1,15 +1,14 @@
package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.filter.common.FilterControlledPermanent;
import mage.game.Game;
import mage.target.common.TargetControlledPermanent;
/**
*
* @author LevelX2
*/
public class SacrificeXTargetCost extends VariableCostImpl {
@ -20,9 +19,10 @@ public class SacrificeXTargetCost extends VariableCostImpl {
this(filter, false);
}
public SacrificeXTargetCost(FilterControlledPermanent filter, boolean additionalCostText) {
super(filter.getMessage() + " to sacrifice");
this.text = (additionalCostText ? "as an additional cost to cast this spell, sacrifice " : "Sacrifice ") + xText + ' ' + filter.getMessage();
public SacrificeXTargetCost(FilterControlledPermanent filter, boolean useAsAdditionalCost) {
super(useAsAdditionalCost ? VariableCostType.ADDITIONAL : VariableCostType.NORMAL,
filter.getMessage() + " to sacrifice");
this.text = (useAsAdditionalCost ? "as an additional cost to cast this spell, sacrifice " : "Sacrifice ") + xText + ' ' + filter.getMessage();
this.filter = filter;
}

View file

@ -1,19 +1,17 @@
package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.filter.common.FilterControlledPermanent;
import mage.game.Game;
import mage.target.common.TargetControlledPermanent;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class TapVariableTargetCost extends VariableCostImpl {
public class TapVariableTargetCost extends VariableCostImpl {
protected FilterControlledPermanent filter;
@ -21,10 +19,11 @@ public class TapVariableTargetCost extends VariableCostImpl {
this(filter, false, "X");
}
public TapVariableTargetCost(FilterControlledPermanent filter, boolean additionalCostText, String xText) {
super(xText, new StringBuilder(filter.getMessage()).append(" to tap").toString());
public TapVariableTargetCost(FilterControlledPermanent filter, boolean useAsAdditionalCost, String xText) {
super(useAsAdditionalCost ? VariableCostType.ADDITIONAL : VariableCostType.NORMAL,
xText, new StringBuilder(filter.getMessage()).append(" to tap").toString());
this.filter = filter;
this.text = new StringBuilder(additionalCostText ? "as an additional cost to cast this spell, tap ":"Tap ")
this.text = new StringBuilder(useAsAdditionalCost ? "as an additional cost to cast this spell, tap " : "Tap ")
.append(this.xText).append(' ').append(filter.getMessage()).toString();
}

View file

@ -2,10 +2,7 @@ package mage.abilities.costs.mana;
import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.CostsImpl;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.*;
import mage.abilities.costs.common.PayLifeCost;
import mage.abilities.mana.ManaOptions;
import mage.constants.ColoredManaSymbol;
@ -32,7 +29,7 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
protected final UUID id;
protected String text = null;
private static Map<String, ManaCosts> costsCache = new ConcurrentHashMap<>(); // must be thread safe, can't use nulls
private static final Map<String, ManaCosts> costsCache = new ConcurrentHashMap<>(); // must be thread safe, can't use nulls
public ManaCostsImpl() {
this.id = UUID.randomUUID();
@ -471,7 +468,7 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
modifierForX++;
}
}
this.add(new VariableManaCost(modifierForX));
this.add(new VariableManaCost(VariableCostType.NORMAL, modifierForX));
} //TODO: handle multiple {X} and/or {Y} symbols
} else if (Character.isDigit(symbol.charAt(0))) {
MonoHybridManaCost cost;

View file

@ -4,6 +4,7 @@ import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.VariableCostType;
import mage.constants.ColoredManaSymbol;
import mage.filter.FilterMana;
import mage.game.Game;
@ -18,6 +19,7 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
// 1. as X value in spell/ability cast (announce X, set VariableManaCost as paid and add generic mana to pay instead)
// 2. as X value in direct pay (X already announced, cost is unpaid, need direct pay)
protected VariableCostType costType;
protected int xInstancesCount; // number of {X} instances in cost like {X} or {X}{X}
protected int xValue = 0; // final X value after announce and replace events
protected int xPay = 0; // final/total need pay after announce and replace events (example: {X}{X}, X=3, xPay = 6)
@ -27,11 +29,12 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
protected int minX = 0;
protected int maxX = Integer.MAX_VALUE;
public VariableManaCost() {
this(1);
public VariableManaCost(VariableCostType costType) {
this(costType, 1);
}
public VariableManaCost(int xInstancesCount) {
public VariableManaCost(VariableCostType costType, int xInstancesCount) {
this.costType = costType;
this.xInstancesCount = xInstancesCount;
this.cost = new Mana();
options.add(new Mana());
@ -39,6 +42,7 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
public VariableManaCost(final VariableManaCost manaCost) {
super(manaCost);
this.costType = manaCost.costType;
this.xInstancesCount = manaCost.xInstancesCount;
this.xValue = manaCost.xValue;
this.xPay = manaCost.xPay;
@ -171,4 +175,14 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
public void setFilter(FilterMana filter) {
this.filter = filter;
}
@Override
public VariableCostType getCostType() {
return this.costType;
}
@Override
public void setCostType(VariableCostType costType) {
this.costType = costType;
}
}

View file

@ -36,25 +36,31 @@ public class BuybackAbility extends StaticAbility implements OptionalAdditionalS
private static final String keywordText = "Buyback";
private static final String reminderTextCost = "You may {cost} in addition to any other costs as you cast this spell. If you do, put this card into your hand as it resolves.";
private static final String reminderTextMana = "You may pay an additional {cost} as you cast this spell. If you do, put this card into your hand as it resolves.";
protected OptionalAdditionalCost buybackCost;
private int amountToReduceBy = 0;
public BuybackAbility(String manaString) {
super(Zone.STACK, new BuybackEffect());
this.buybackCost = new OptionalAdditionalCostImpl(keywordText, reminderTextMana, new ManaCostsImpl(manaString));
addBuybackCostAndSetup(new OptionalAdditionalCostImpl(keywordText, reminderTextMana, new ManaCostsImpl(manaString)));
setRuleAtTheTop(true);
}
public BuybackAbility(Cost cost) {
super(Zone.STACK, new BuybackEffect());
this.buybackCost = new OptionalAdditionalCostImpl(keywordText, "&mdash;", reminderTextCost, cost);
addBuybackCostAndSetup(new OptionalAdditionalCostImpl(keywordText, "&mdash;", reminderTextCost, cost));
setRuleAtTheTop(true);
}
private void addBuybackCostAndSetup(OptionalAdditionalCost newCost) {
this.buybackCost = newCost;
this.buybackCost.setCostType(VariableCostType.ADDITIONAL);
}
public BuybackAbility(final BuybackAbility ability) {
super(ability);
buybackCost = new OptionalAdditionalCostImpl((OptionalAdditionalCostImpl) ability.buybackCost);
amountToReduceBy = ability.amountToReduceBy;
this.buybackCost = ability.buybackCost.copy();
this.amountToReduceBy = ability.amountToReduceBy;
}
@Override

View file

@ -4,11 +4,9 @@ import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.OptionalAdditionalCostImpl;
import mage.abilities.costs.OptionalAdditionalSourceCosts;
import mage.abilities.costs.*;
import mage.abilities.costs.common.TapTargetCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
@ -62,9 +60,9 @@ public class ConspireAbility extends StaticAbility implements OptionalAdditional
MORE
}
private UUID conspireId;
private final UUID conspireId;
private String reminderText;
private OptionalAdditionalCostImpl conspireCost;
private OptionalAdditionalCost conspireCost;
/**
* Unique Id for a ConspireAbility but may not change while a continuous
@ -87,12 +85,19 @@ public class ConspireAbility extends StaticAbility implements OptionalAdditional
reminderText = "As you cast this spell, you may tap two untapped creatures you control that share a color with it. When you do, copy it and you may choose new targets for the copy.";
break;
}
Cost cost = new TapTargetCost(new TargetControlledPermanent(2, 2, filter, true));
cost.setText("");
conspireCost = new OptionalAdditionalCostImpl(keywordText, " ", reminderText, cost);
addConspireCostAndSetup(new OptionalAdditionalCostImpl(keywordText, " ", reminderText, cost));
addSubAbility(new ConspireTriggeredAbility(conspireId));
}
private void addConspireCostAndSetup(OptionalAdditionalCost newCost) {
this.conspireCost = newCost;
this.conspireCost.setCostType(VariableCostType.ADDITIONAL);
}
public ConspireAbility(final ConspireAbility ability) {
super(ability);
this.conspireId = ability.conspireId;
@ -139,9 +144,13 @@ public class ConspireAbility extends StaticAbility implements OptionalAdditional
if (conspireCost.canPay(ability, this, getControllerId(), game)
&& player.chooseUse(Outcome.Benefit, "Pay " + conspireCost.getText(false) + " ?", ability, game)) {
activateConspire(ability, game);
for (Iterator it = conspireCost.iterator(); it.hasNext(); ) {
for (Iterator it = ((Costs) conspireCost).iterator(); it.hasNext(); ) {
Cost cost = (Cost) it.next();
ability.getCosts().add(cost.copy());
if (cost instanceof ManaCostsImpl) {
ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy());
} else {
ability.getCosts().add(cost.copy());
}
}
}
}
@ -194,7 +203,7 @@ public class ConspireAbility extends StaticAbility implements OptionalAdditional
class ConspireTriggeredAbility extends TriggeredAbilityImpl {
private UUID conspireId;
private final UUID conspireId;
public ConspireTriggeredAbility(UUID conspireId) {
super(Zone.STACK, new ConspireEffect());

View file

@ -3,10 +3,7 @@ package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.OptionalAdditionalCost;
import mage.abilities.costs.OptionalAdditionalCostImpl;
import mage.abilities.costs.OptionalAdditionalModeSourceCosts;
import mage.abilities.costs.*;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.constants.Outcome;
import mage.constants.Zone;
@ -14,6 +11,7 @@ import mage.game.Game;
import mage.players.Player;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
@ -35,12 +33,12 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
private static final String keywordText = "Entwine";
protected static final String reminderText = "You may {cost} in addition to any other costs to use all modes.";
protected OptionalAdditionalCost additionalCost;
protected OptionalAdditionalCost entwineCost;
protected Set<String> activations = new HashSet<>(); // same logic as KickerAbility: activations per zoneChangeCounter
public EntwineAbility(String manaString) {
super(Zone.STACK, null);
this.additionalCost = new OptionalAdditionalCostImpl(keywordText, reminderText, new ManaCostsImpl(manaString));
addEntwineCostAndSetup(new OptionalAdditionalCostImpl(keywordText, reminderText, new ManaCostsImpl(manaString)));
}
public EntwineAbility(Cost cost) {
@ -49,14 +47,20 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
public EntwineAbility(Cost cost, String reminderText) {
super(Zone.STACK, null);
this.additionalCost = new OptionalAdditionalCostImpl(keywordText, "&mdash;", reminderText, cost);
addEntwineCostAndSetup(new OptionalAdditionalCostImpl(keywordText, "&mdash;", reminderText, cost));
setRuleAtTheTop(true);
}
private void addEntwineCostAndSetup(OptionalAdditionalCost newCost) {
this.entwineCost = newCost;
this.entwineCost.setCostType(VariableCostType.ADDITIONAL);
}
public EntwineAbility(final EntwineAbility ability) {
super(ability);
if (ability.additionalCost != null) {
this.additionalCost = ability.additionalCost.copy();
if (ability.entwineCost != null) {
this.entwineCost = ability.entwineCost.copy();
}
this.activations.addAll(ability.activations);
}
@ -77,32 +81,40 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
return;
}
this.resetCosts(game, ability);
if (additionalCost == null) {
this.resetEntwine(game, ability);
if (entwineCost == null) {
return;
}
if (additionalCost.canPay(ability, this, ability.getControllerId(), game)
&& player.chooseUse(Outcome.Benefit, "Pay " + additionalCost.getText(false) + " ?", ability, game)) {
addCostsToAbility(additionalCost, ability);
activateCost(game, ability);
// AI can use it
if (entwineCost.canPay(ability, this, ability.getControllerId(), game)
&& player.chooseUse(Outcome.Benefit, "Pay " + entwineCost.getText(false) + " ?", ability, game)) {
for (Iterator it = ((Costs) entwineCost).iterator(); it.hasNext(); ) {
Cost cost = (Cost) it.next();
if (cost instanceof ManaCostsImpl) {
ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy());
} else {
ability.getCosts().add(cost.copy());
}
}
activateEntwine(game, ability);
}
}
@Override
public String getRule() {
StringBuilder sb = new StringBuilder();
if (additionalCost != null) {
sb.append(additionalCost.getText(false));
sb.append(' ').append(additionalCost.getReminderText());
if (entwineCost != null) {
sb.append(entwineCost.getText(false));
sb.append(' ').append(entwineCost.getReminderText());
}
return sb.toString();
}
@Override
public String getCastMessageSuffix() {
if (additionalCost != null) {
return additionalCost.getCastSuffixMessage(0);
if (entwineCost != null) {
return entwineCost.getCastSuffixMessage(0);
} else {
return "";
}
@ -119,20 +131,16 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM
ability.getModes().setMaxModes(maxModes);
}
private void addCostsToAbility(Cost cost, Ability ability) {
ability.addCost(cost.copy());
}
private void resetCosts(Game game, Ability source) {
if (additionalCost != null) {
additionalCost.reset();
private void resetEntwine(Game game, Ability source) {
if (entwineCost != null) {
entwineCost.reset();
}
String key = getActivationKey(source, game);
this.activations.remove(key);
}
private void activateCost(Game game, Ability source) {
private void activateEntwine(Game game, Ability source) {
String key = getActivationKey(source, game);
this.activations.add(key);
}

View file

@ -95,17 +95,24 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
}
public final OptionalAdditionalCost addKickerCost(String manaString) {
OptionalAdditionalCost kickerCost = new OptionalAdditionalCostImpl(
OptionalAdditionalCost newCost = new OptionalAdditionalCostImpl(
keywordText, reminderText, new ManaCostsImpl(manaString));
kickerCosts.add(kickerCost);
return kickerCost;
addKickerCostAndSetup(newCost);
return newCost;
}
public final OptionalAdditionalCost addKickerCost(Cost cost) {
OptionalAdditionalCost kickerCost = new OptionalAdditionalCostImpl(
OptionalAdditionalCost newCost = new OptionalAdditionalCostImpl(
keywordText, "-", reminderText, cost);
kickerCosts.add(kickerCost);
return kickerCost;
addKickerCostAndSetup(newCost);
return newCost;
}
private void addKickerCostAndSetup(OptionalAdditionalCost newCost) {
this.kickerCosts.add(newCost);
this.kickerCosts.forEach(cost -> {
cost.setCostType(VariableCostType.ADDITIONAL);
});
}
public void resetKicker(Game game, Ability source) {
@ -250,6 +257,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
"Pay " + times + kickerCost.getText(false) + " ?", ability, game)) {
this.activateKicker(kickerCost, ability, game);
if (kickerCost instanceof Costs) {
// as multiple costs
for (Iterator itKickerCost = ((Costs) kickerCost).iterator(); itKickerCost.hasNext(); ) {
Object kickerCostObject = itKickerCost.next();
if ((kickerCostObject instanceof Costs)) {
@ -262,6 +270,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
}
}
} else {
// as single cost
addKickerCostsToAbility(kickerCost, ability, game);
}
again = kickerCost.isRepeatable();
@ -275,7 +284,8 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
}
private void addKickerCostsToAbility(Cost cost, Ability ability, Game game) {
// can contains multiple costs from multikicker ability
// can contain multiple costs from multikicker ability
// must be additional cost type
if (cost instanceof ManaCostsImpl) {
ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy());
} else {

View file

@ -1,11 +1,13 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.BeginningOfUpkeepTriggeredAbility;
import mage.abilities.condition.common.SuspendedCondition;
import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.costs.mana.VariableManaCost;
@ -26,7 +28,6 @@ import mage.target.targetpointer.FixedTarget;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import mage.ApprovingObject;
/**
* 502.59. Suspend
@ -108,16 +109,16 @@ import mage.ApprovingObject;
*/
public class SuspendAbility extends SpecialAction {
private String ruleText;
private final String ruleText;
private boolean gainedTemporary;
/**
* Gives the card the SuspendAbility
*
* @param suspend - amount of time counters, if Integer.MAX_VALUE is set
* there will be {X} costs and X counters added
* @param cost - null is used for temporary gained suspend ability
* @param card - card that has the suspend ability
* there will be {X} costs and X counters added
* @param cost - null is used for temporary gained suspend ability
* @param card - card that has the suspend ability
*/
public SuspendAbility(int suspend, ManaCost cost, Card card) {
this(suspend, cost, card, false);
@ -129,7 +130,7 @@ public class SuspendAbility extends SpecialAction {
this.addEffect(new SuspendExileEffect(suspend));
this.usesStack = false;
if (suspend == Integer.MAX_VALUE) {
VariableManaCost xCosts = new VariableManaCost();
VariableManaCost xCosts = new VariableManaCost(VariableCostType.ALTERNATIVE);
xCosts.setMinX(1);
this.addManaCost(xCosts);
cost = new ManaCostsImpl("{X}" + cost.getText());
@ -138,7 +139,7 @@ public class SuspendAbility extends SpecialAction {
if (cost != null) {
sb.append(suspend == Integer.MAX_VALUE ? "X" : suspend).append("&mdash;")
.append(cost.getText()).append(suspend
== Integer.MAX_VALUE ? ". X can't be 0." : "");
== Integer.MAX_VALUE ? ". X can't be 0." : "");
if (!shortRule) {
sb.append(" <i>(Rather than cast this card from your hand, pay ")
.append(cost.getText())
@ -191,7 +192,7 @@ public class SuspendAbility extends SpecialAction {
UUID exileId = (UUID) game.getState().getValue("SuspendExileId" + controllerId.toString());
if (exileId == null) {
exileId = UUID.randomUUID();
game.getState().setValue("SuspendExileId" + controllerId.toString(), exileId);
game.getState().setValue("SuspendExileId" + controllerId, exileId);
}
return exileId;
}
@ -212,7 +213,7 @@ public class SuspendAbility extends SpecialAction {
return new ActivationStatus(object.isInstant(game)
|| object.hasAbility(FlashAbility.getInstance(), game)
|| null != game.getContinuousEffects().asThough(sourceId,
AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game)
AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game)
|| game.canPlaySorcery(playerId), null);
}
@ -306,7 +307,7 @@ class SuspendPlayCardAbility extends TriggeredAbilityImpl {
@Override
public String getRule() {
return "When the last time counter is removed from this card ({this}), "
+ "if it's removed from the game, " ;
+ "if it's removed from the game, ";
}
@Override
@ -428,7 +429,7 @@ class SuspendBeginningOfUpkeepInterveningIfTriggeredAbility extends ConditionalI
public SuspendBeginningOfUpkeepInterveningIfTriggeredAbility() {
super(new BeginningOfUpkeepTriggeredAbility(Zone.EXILED, new RemoveCounterSourceEffect(CounterType.TIME.createInstance()),
TargetController.YOU, false),
TargetController.YOU, false),
SuspendedCondition.instance,
"At the beginning of your upkeep, if this card ({this}) is suspended, remove a time counter from it.");
this.setRuleVisible(false);

View file

@ -2,6 +2,7 @@ package mage.game.command.emblems;
import mage.abilities.Ability;
import mage.abilities.common.LimitedTimesPerTurnActivatedAbility;
import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.common.DiscardCardCost;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.effects.OneShotEffect;
@ -18,12 +19,12 @@ import mage.constants.Zone;
import mage.game.Game;
import mage.game.command.Emblem;
import mage.game.permanent.token.EmptyToken;
import mage.game.permanent.token.Token;
import mage.game.permanent.token.custom.CreatureToken;
import mage.util.CardUtil;
import mage.util.RandomUtil;
import java.util.List;
import mage.game.permanent.token.Token;
import mage.game.permanent.token.custom.CreatureToken;
/**
* @author spjspj
@ -37,7 +38,7 @@ public final class MomirEmblem extends Emblem {
// {X}, Discard a card: Create a token that's a copy of a creature card with converted mana cost X chosen at random.
// Activate this ability only any time you could cast a sorcery and only once each turn.
LimitedTimesPerTurnActivatedAbility ability = new LimitedTimesPerTurnActivatedAbility(Zone.COMMAND, new MomirEffect(), new VariableManaCost());
LimitedTimesPerTurnActivatedAbility ability = new LimitedTimesPerTurnActivatedAbility(Zone.COMMAND, new MomirEffect(), new VariableManaCost(VariableCostType.NORMAL));
ability.addCost(new DiscardCardCost());
ability.setTiming(TimingRule.SORCERY);
this.getAbilities().add(ability);
@ -65,7 +66,7 @@ class MomirEffect extends OneShotEffect {
int value = source.getManaCostsToPay().getX();
if (game.isSimulation()) {
// Create dummy token to prevent multiple DB find cards what causes H2 java.lang.IllegalStateException if AI cancels calculation because of time out
Token token = new CreatureToken(value, value +1);
Token token = new CreatureToken(value, value + 1);
token.putOntoBattlefield(1, game, source, source.getControllerId(), false, false);
return true;
}

View file

@ -6,6 +6,7 @@ import mage.ManaSymbol;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.*;
import mage.abilities.dynamicvalue.DynamicValue;
@ -648,7 +649,7 @@ public final class ManaUtil {
*/
public static ManaCost createManaCost(int genericManaCount, boolean payAsX) {
if (payAsX) {
VariableManaCost xCost = new VariableManaCost();
VariableManaCost xCost = new VariableManaCost(VariableCostType.NORMAL);
xCost.setAmount(genericManaCount, genericManaCount, false);
return xCost;
} else {