mirror of
https://github.com/magefree/mage.git
synced 2025-12-25 21:12:04 -08:00
Reworked cost adjuster logic for better support of X and cost modification effects:
Improves: * refactor: split CostAdjuster logic in multiple parts - prepare X, prepare cost, increase cost, reduce cost; * refactor: improved VariableManaCost to support min/max values, playable and AI calculations, test framework; * refactor: improved EarlyTargetCost to support mana costs too (related to #13023); * refactor: migrated some cards with CostAdjuster and X to EarlyTargetCost (Knollspine Invocation, etc - related to #13023); * refactor: added shared code for "As an additional cost to cast this spell, discard X creature cards"; * refactor: added shared code for "X is the converted mana cost of the exiled card"; * tests: added dozens tests with cost adjusters; Bug fixes: * game: fixed that some cards with CostAdjuster ignore min/max limits for X (allow to choose any X, example: Scorched Earth, Open The Way); * game: fixed that some cards ask to announce already defined X values (example: Bargaining Table); * game: fixed that some cards with CostAdjuster do not support combo with other cost modification effects; * game, gui: fixed missing game logs about predefined X values; * game, gui: fixed wrong X icon for predefined X values; Test framework: * test framework: added X min/max check for wrong values; * test framework: added X min/max info in miss X value announce; * test framework: added check to find duplicated effect bugs (see assertNoDuplicatedEffects); Cards: * Open The Way - fixed that it allow to choose any X without limits (close #12810); * Unbound Flourishing - improved combo support for activated abilities with predefined X mana costs like Bargaining Table;
This commit is contained in:
parent
13a832ae00
commit
bae3089abb
100 changed files with 1519 additions and 449 deletions
|
|
@ -126,7 +126,7 @@ public interface Ability extends Controllable, Serializable {
|
|||
/**
|
||||
* Gets all {@link ManaCosts} associated with this ability. These returned
|
||||
* costs should never be modified as they represent the base costs before
|
||||
* any modifications.
|
||||
* any modifications (only cost adjusters can change it, e.g. set min/max values)
|
||||
*
|
||||
* @return All {@link ManaCosts} that must be paid.
|
||||
*/
|
||||
|
|
@ -151,6 +151,17 @@ public interface Ability extends Controllable, Serializable {
|
|||
|
||||
void addManaCostsToPay(ManaCost manaCost);
|
||||
|
||||
/**
|
||||
* Helper method to setup actual min/max limits of current X costs BEFORE player's X announcement
|
||||
*/
|
||||
void setVariableCostsMinMax(int min, int max);
|
||||
|
||||
/**
|
||||
* Helper method to replace X by direct value BEFORE player's X announcement
|
||||
* If you need additional target for X then use CostAdjuster + EarlyTargetCost (example: Bargaining Table)
|
||||
*/
|
||||
void setVariableCostsValue(int xValue);
|
||||
|
||||
/**
|
||||
* Gets a map of the cost tags (set while casting/activating) of this ability, can be null if no tags have been set yet.
|
||||
* Does NOT return the source permanent's tags.
|
||||
|
|
@ -356,7 +367,7 @@ public interface Ability extends Controllable, Serializable {
|
|||
* - for dies triggers - override and use TriggeredAbilityImpl.isInUseableZoneDiesTrigger inside + set setLeavesTheBattlefieldTrigger(true)
|
||||
*
|
||||
* @param sourceObject can be null for static continues effects checking like rules modification (example: Yixlid Jailer)
|
||||
* @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder)
|
||||
* @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder)
|
||||
*/
|
||||
boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event);
|
||||
|
||||
|
|
@ -533,11 +544,25 @@ public interface Ability extends Controllable, Serializable {
|
|||
|
||||
void adjustTargets(Game game);
|
||||
|
||||
/**
|
||||
* Dynamic X and cost modification, see CostAdjuster for more details on usage
|
||||
*/
|
||||
Ability setCostAdjuster(CostAdjuster costAdjuster);
|
||||
|
||||
CostAdjuster getCostAdjuster();
|
||||
/**
|
||||
* Prepare {X} settings for announce
|
||||
*/
|
||||
void adjustX(Game game);
|
||||
|
||||
void adjustCosts(Game game);
|
||||
/**
|
||||
* Prepare costs (generate due game state or announce)
|
||||
*/
|
||||
void adjustCostsPrepare(Game game);
|
||||
|
||||
/**
|
||||
* Apply additional cost modifications logic/effects
|
||||
*/
|
||||
void adjustCostsModify(Game game, CostModificationType costModificationType);
|
||||
|
||||
List<Hint> getHints();
|
||||
|
||||
|
|
|
|||
|
|
@ -291,6 +291,8 @@ public abstract class AbilityImpl implements Ability {
|
|||
|
||||
// fused or spliced spells contain multiple abilities (e.g. fused, left, right)
|
||||
// optional costs and cost modification must be applied only to the first/main ability
|
||||
// TODO: need tests with X announced costs, cost modification effects, CostAdjuster, early cost target, etc
|
||||
// can be bugged due multiple calls (not all code parts below use isMainPartAbility)
|
||||
boolean isMainPartAbility = !CardUtil.isFusedPartAbility(this, game);
|
||||
|
||||
/* 20220908 - 601.2b
|
||||
|
|
@ -326,6 +328,16 @@ public abstract class AbilityImpl implements Ability {
|
|||
return false;
|
||||
}
|
||||
|
||||
// 20241022 - 601.2b
|
||||
// Choose targets for costs that have to be chosen early
|
||||
// Not yet included in 601.2b but this is where it will be
|
||||
handleChooseCostTargets(game, controller);
|
||||
|
||||
// prepare dynamic costs (must be called before any x announce)
|
||||
if (isMainPartAbility) {
|
||||
adjustX(game);
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
@ -402,7 +414,7 @@ public abstract class AbilityImpl implements Ability {
|
|||
// Note: ActivatedAbility does include SpellAbility & PlayLandAbility, but those should be able to be canceled too.
|
||||
boolean canCancel = this instanceof ActivatedAbility && controller.isHuman();
|
||||
if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, canCancel)) {
|
||||
// was canceled during targer selection
|
||||
// was canceled during target selection
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -428,7 +440,7 @@ public abstract class AbilityImpl implements Ability {
|
|||
|
||||
//20101001 - 601.2e
|
||||
if (isMainPartAbility) {
|
||||
adjustCosts(game); // still needed for CostAdjuster objects (to handle some types of dynamic costs)
|
||||
// adjustX already called before any announces
|
||||
game.getContinuousEffects().costModification(this, game);
|
||||
}
|
||||
|
||||
|
|
@ -726,6 +738,11 @@ public abstract class AbilityImpl implements Ability {
|
|||
((EarlyTargetCost) cost).chooseTarget(game, this, controller);
|
||||
}
|
||||
}
|
||||
for (ManaCost cost : getManaCostsToPay()) {
|
||||
if (cost instanceof EarlyTargetCost && cost.getTargets().isEmpty()) {
|
||||
((EarlyTargetCost) cost).chooseTarget(game, this, controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -764,8 +781,15 @@ public abstract class AbilityImpl implements Ability {
|
|||
if (!variableManaCost.isPaid()) { // should only happen for human players
|
||||
int xValue;
|
||||
if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) {
|
||||
xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(),
|
||||
"Announce the value for " + variableManaCost.getText(), game, this);
|
||||
if (variableManaCost.wasAnnounced()) {
|
||||
// announce by rules
|
||||
xValue = variableManaCost.getAmount();
|
||||
} else {
|
||||
// announce by player
|
||||
xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(),
|
||||
"Announce the value for " + variableManaCost.getText(), game, this);
|
||||
}
|
||||
|
||||
int amountMana = xValue * variableManaCost.getXInstancesCount();
|
||||
StringBuilder manaString = threadLocalBuilder.get();
|
||||
if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) {
|
||||
|
|
@ -1072,6 +1096,60 @@ public abstract class AbilityImpl implements Ability {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVariableCostsMinMax(int min, int max) {
|
||||
// modify all values (mtg rules allow only one type of X, so min/max must be shared between all X instances)
|
||||
|
||||
// base cost
|
||||
for (ManaCost cost : getManaCosts()) {
|
||||
if (cost instanceof MinMaxVariableCost) {
|
||||
MinMaxVariableCost minMaxCost = (MinMaxVariableCost) cost;
|
||||
minMaxCost.setMinX(min);
|
||||
minMaxCost.setMaxX(max);
|
||||
}
|
||||
}
|
||||
|
||||
// prepared cost
|
||||
for (ManaCost cost : getManaCostsToPay()) {
|
||||
if (cost instanceof MinMaxVariableCost) {
|
||||
MinMaxVariableCost minMaxCost = (MinMaxVariableCost) cost;
|
||||
minMaxCost.setMinX(min);
|
||||
minMaxCost.setMaxX(max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVariableCostsValue(int xValue) {
|
||||
// only mana cost supported
|
||||
|
||||
// base cost
|
||||
boolean foundBaseCost = false;
|
||||
for (ManaCost cost : getManaCosts()) {
|
||||
if (cost instanceof VariableManaCost) {
|
||||
foundBaseCost = true;
|
||||
((VariableManaCost) cost).setMinX(xValue);
|
||||
((VariableManaCost) cost).setMaxX(xValue);
|
||||
((VariableManaCost) cost).setAmount(xValue, xValue, false);
|
||||
}
|
||||
}
|
||||
|
||||
// prepared cost
|
||||
boolean foundPreparedCost = false;
|
||||
for (ManaCost cost : getManaCostsToPay()) {
|
||||
if (cost instanceof VariableManaCost) {
|
||||
foundPreparedCost = true;
|
||||
((VariableManaCost) cost).setMinX(xValue);
|
||||
((VariableManaCost) cost).setMaxX(xValue);
|
||||
((VariableManaCost) cost).setAmount(xValue, xValue, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundPreparedCost || !foundBaseCost) {
|
||||
throw new IllegalArgumentException("Wrong code usage: auto-announced X values allowed in mana costs only");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addEffect(Effect effect) {
|
||||
if (effect != null) {
|
||||
|
|
@ -1676,17 +1754,6 @@ public abstract class AbilityImpl implements Ability {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic cost modification for ability.<br>
|
||||
* Example: if it need stack related info (like real targets) then must
|
||||
* check two states (game.inCheckPlayableState): <br>
|
||||
* 1. In playable state it must check all possible use cases (e.g. allow to
|
||||
* reduce on any available target and modes) <br>
|
||||
* 2. In real cast state it must check current use case (e.g. real selected
|
||||
* targets and modes)
|
||||
*
|
||||
* @param costAdjuster
|
||||
*/
|
||||
@Override
|
||||
public AbilityImpl setCostAdjuster(CostAdjuster costAdjuster) {
|
||||
this.costAdjuster = costAdjuster;
|
||||
|
|
@ -1694,14 +1761,23 @@ public abstract class AbilityImpl implements Ability {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CostAdjuster getCostAdjuster() {
|
||||
return costAdjuster;
|
||||
public void adjustX(Game game) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.prepareX(this, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Game game) {
|
||||
public void adjustCostsPrepare(Game game) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.adjustCosts(this, game);
|
||||
costAdjuster.prepareCost(this, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustCostsModify(Game game, CostModificationType costModificationType) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.modifyCost(this, game, costModificationType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,86 @@
|
|||
package mage.abilities.costs;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.CostModificationType;
|
||||
import mage.game.Game;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
* Dynamic costs implementation to control {X} or other costs, can be used in spells and abilities
|
||||
* <p>
|
||||
* Possible use cases:
|
||||
* - define {X} costs like X cards to discard (mana and non-mana values);
|
||||
* - define {X} limits before announce (to help in UX and AI logic)
|
||||
* - define any dynamic costs
|
||||
* - use as simple cost increase/reduce effect
|
||||
* <p>
|
||||
* Calls order by game engine:
|
||||
* - ... early cost target selection for EarlyTargetCost ...
|
||||
* - prepareX
|
||||
* - ... x announce ...
|
||||
* - prepareCost
|
||||
* - increaseCost
|
||||
* - reduceCost
|
||||
* - ... normal target selection and payment ...
|
||||
*
|
||||
* @author TheElk801, JayDi85
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CostAdjuster extends Serializable {
|
||||
|
||||
/**
|
||||
* Must check playable and real cast states.
|
||||
* Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState):
|
||||
* 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes)
|
||||
* 2. In real cast state it must check current use case (e.g. real selected targets and modes)
|
||||
*
|
||||
* @param ability
|
||||
* @param game
|
||||
* Prepare {X} costs settings or define auto-announced mana values
|
||||
* <p>
|
||||
* Usage example:
|
||||
* - define auto-announced mana value {X} by ability.setVariableCostsValue
|
||||
* - define possible {X} settings by ability.setVariableCostsMinMax
|
||||
*/
|
||||
void adjustCosts(Ability ability, Game game);
|
||||
default void prepareX(Ability ability, Game game) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare any dynamic costs
|
||||
* <p>
|
||||
* Usage example:
|
||||
* - add real cost after {X} mana value announce by CardUtil.getSourceCostsTagX
|
||||
* - add dynamic cost from game data
|
||||
*/
|
||||
default void prepareCost(Ability ability, Game game) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple cost reduction effect
|
||||
*/
|
||||
default void reduceCost(Ability ability, Game game) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple cost increase effect
|
||||
*/
|
||||
default void increaseCost(Ability ability, Game game) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation. Override reduceCost or increaseCost instead
|
||||
* TODO: make it private after java 9+ migrate
|
||||
*/
|
||||
default void modifyCost(Ability ability, Game game, CostModificationType costModificationType) {
|
||||
switch (costModificationType) {
|
||||
case REDUCE_COST:
|
||||
reduceCost(ability, game);
|
||||
break;
|
||||
case INCREASE_COST:
|
||||
increaseCost(ability, game);
|
||||
break;
|
||||
case SET_COST:
|
||||
// do nothing
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown mod type: " + costModificationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,20 +6,11 @@ import mage.players.Player;
|
|||
|
||||
/**
|
||||
* @author Grath
|
||||
* Costs which extend this class need to have targets chosen, and those targets must be chosen during 601.2b step.
|
||||
* <p>
|
||||
* Support 601.2b rules for ealry target choice before X announce and other actions
|
||||
*/
|
||||
public abstract class EarlyTargetCost extends CostImpl {
|
||||
public interface EarlyTargetCost {
|
||||
|
||||
protected EarlyTargetCost() {
|
||||
super();
|
||||
}
|
||||
void chooseTarget(Game game, Ability source, Player controller);
|
||||
|
||||
protected EarlyTargetCost(final EarlyTargetCost cost) {
|
||||
super(cost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract EarlyTargetCost copy();
|
||||
|
||||
public abstract void chooseTarget(Game game, Ability source, Player controller);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package mage.abilities.costs;
|
||||
|
||||
/**
|
||||
* @author jayDi85
|
||||
*/
|
||||
public interface MinMaxVariableCost extends VariableCost {
|
||||
|
||||
int getMinX();
|
||||
|
||||
void setMinX(int minX);
|
||||
|
||||
int getMaxX();
|
||||
|
||||
void setMaxX(int maxX);
|
||||
}
|
||||
|
|
@ -10,11 +10,20 @@ import mage.players.Player;
|
|||
import mage.target.common.TargetCardInHand;
|
||||
|
||||
/**
|
||||
* Used to setup discard cost WITHOUT {X} mana cost
|
||||
* <p>
|
||||
* If you have {X} in spell's mana cost then use DiscardXCardsCostAdjuster instead
|
||||
* <p>
|
||||
* Example:
|
||||
* - {2}{U}{R}
|
||||
* - As an additional cost to cast this spell, discard X cards.
|
||||
*
|
||||
* @author LevelX2
|
||||
*/
|
||||
public class DiscardXTargetCost extends VariableCostImpl {
|
||||
|
||||
protected FilterCard filter;
|
||||
protected boolean isRandom = false;
|
||||
|
||||
public DiscardXTargetCost(FilterCard filter) {
|
||||
this(filter, false);
|
||||
|
|
@ -30,6 +39,12 @@ public class DiscardXTargetCost extends VariableCostImpl {
|
|||
protected DiscardXTargetCost(final DiscardXTargetCost cost) {
|
||||
super(cost);
|
||||
this.filter = cost.filter;
|
||||
this.isRandom = cost.isRandom;
|
||||
}
|
||||
|
||||
public DiscardXTargetCost withRandom() {
|
||||
this.isRandom = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -49,6 +64,6 @@ public class DiscardXTargetCost extends VariableCostImpl {
|
|||
@Override
|
||||
public Cost getFixedCostsFromAnnouncedValue(int xValue) {
|
||||
TargetCardInHand target = new TargetCardInHand(xValue, filter);
|
||||
return new DiscardTargetCost(target);
|
||||
return new DiscardTargetCost(target, this.isRandom);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ public class PayLoyaltyCost extends CostImpl {
|
|||
|
||||
int loyaltyCost = amount;
|
||||
|
||||
// apply cost modification
|
||||
// apply dynamic costs and cost modification
|
||||
if (ability instanceof LoyaltyAbility) {
|
||||
LoyaltyAbility copiedAbility = ((LoyaltyAbility) ability).copy();
|
||||
copiedAbility.adjustCosts(game);
|
||||
copiedAbility.adjustX(game);
|
||||
game.getContinuousEffects().costModification(copiedAbility, game);
|
||||
loyaltyCost = 0;
|
||||
for (Cost cost : copiedAbility.getCosts()) {
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@ public class PayVariableLoyaltyCost extends VariableCostImpl {
|
|||
|
||||
int maxValue = permanent.getCounters(game).getCount(CounterType.LOYALTY);
|
||||
|
||||
// apply cost modification
|
||||
// apply dynamic costs and cost modification
|
||||
if (source instanceof LoyaltyAbility) {
|
||||
LoyaltyAbility copiedAbility = ((LoyaltyAbility) source).copy();
|
||||
copiedAbility.adjustCosts(game);
|
||||
copiedAbility.adjustX(game);
|
||||
game.getContinuousEffects().costModification(copiedAbility, game);
|
||||
for (Cost cost : copiedAbility.getCosts()) {
|
||||
if (cost instanceof PayVariableLoyaltyCost) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public enum CommanderManaValueAdjuster implements CostAdjuster {
|
|||
instance;
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Ability ability, Game game) {
|
||||
public void reduceCost(Ability ability, Game game) {
|
||||
CardUtil.reduceCost(ability, GreatestCommanderManaValue.instance.calculate(game, ability, null));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package mage.abilities.costs.costadjusters;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.costs.CostAdjuster;
|
||||
import mage.abilities.costs.common.DiscardTargetCost;
|
||||
import mage.abilities.effects.common.InfoEffect;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.Zone;
|
||||
import mage.filter.FilterCard;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
import mage.target.common.TargetCardInHand;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* Used to setup discard cost with {X} mana cost
|
||||
* <p>
|
||||
* If you don't have {X} then use DiscardXTargetCost instead
|
||||
* <p>
|
||||
* Example:
|
||||
* - {X}{1}{B}
|
||||
* - As an additional cost to cast this spell, discard X cards.
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class DiscardXCardsCostAdjuster implements CostAdjuster {
|
||||
|
||||
private final FilterCard filter;
|
||||
|
||||
private DiscardXCardsCostAdjuster(FilterCard filter) {
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareX(Ability ability, Game game) {
|
||||
Player controller = game.getPlayer(ability.getControllerId());
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int minX = 0;
|
||||
int maxX = controller.getHand().getCards(this.filter, ability.getControllerId(), ability, game).size();
|
||||
ability.setVariableCostsMinMax(minX, maxX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareCost(Ability ability, Game game) {
|
||||
int x = CardUtil.getSourceCostsTagX(game, ability, -1);
|
||||
if (x >= 0) {
|
||||
ability.addCost(new DiscardTargetCost(new TargetCardInHand(x, x, this.filter)));
|
||||
}
|
||||
}
|
||||
|
||||
public static void addAdjusterAndMessage(Card card, FilterCard filter) {
|
||||
addAdjusterAndMessage(card, filter, false);
|
||||
}
|
||||
|
||||
public static void addAdjusterAndMessage(Card card, FilterCard filter, boolean isRandom) {
|
||||
if (card.getSpellAbility().getManaCosts().getVariableCosts().isEmpty()) {
|
||||
// how to fix: use DiscardXTargetCost
|
||||
throw new IllegalArgumentException("Wrong code usage: that's cost adjuster must be used with {X} in mana costs only - " + card);
|
||||
}
|
||||
|
||||
Ability ability = new SimpleStaticAbility(
|
||||
Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X " + filter.getMessage())
|
||||
);
|
||||
ability.setRuleAtTheTop(true);
|
||||
card.addAbility(ability);
|
||||
|
||||
card.getSpellAbility().setCostAdjuster(new DiscardXCardsCostAdjuster(filter));
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ public enum DomainAdjuster implements CostAdjuster {
|
|||
instance;
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Ability ability, Game game) {
|
||||
public void reduceCost(Ability ability, Game game) {
|
||||
CardUtil.reduceCost(ability, DomainValue.REGULAR.calculate(game, ability, null));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,22 +25,29 @@ public class ExileCardsFromHandAdjuster implements CostAdjuster {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Ability ability, Game game) {
|
||||
if (game.inCheckPlayableState()) {
|
||||
return;
|
||||
}
|
||||
public void reduceCost(Ability ability, Game game) {
|
||||
Player player = game.getPlayer(ability.getControllerId());
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int cardCount = player.getHand().count(filter, game);
|
||||
int toExile = cardCount > 0 ? player.getAmount(
|
||||
0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game
|
||||
) : 0;
|
||||
if (toExile > 0) {
|
||||
ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter)));
|
||||
CardUtil.reduceCost(ability, 2 * toExile);
|
||||
int reduceCount;
|
||||
if (game.inCheckPlayableState()) {
|
||||
// possible
|
||||
reduceCount = 2 * cardCount;
|
||||
} else {
|
||||
// real - need to choose
|
||||
// TODO: need early target cost instead dialog here
|
||||
int toExile = cardCount == 0 ? 0 : player.getAmount(
|
||||
0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game
|
||||
);
|
||||
reduceCount = 2 * toExile;
|
||||
if (toExile > 0) {
|
||||
ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter)));
|
||||
}
|
||||
}
|
||||
CardUtil.reduceCost(ability, 2 * reduceCount);
|
||||
}
|
||||
|
||||
public static final void addAdjusterAndMessage(Card card, FilterCard filter) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package mage.abilities.costs.costadjusters;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.costs.CostAdjuster;
|
||||
import mage.cards.Card;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
||||
/**
|
||||
* Used for {X} mana cost that must be replaced by imprinted mana value
|
||||
* <p>
|
||||
* Example:
|
||||
* - Elite Arcanist
|
||||
* - {X}, {T}: Copy the exiled card. ... X is the converted mana cost of the exiled card.
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public enum ImprintedManaValueXCostAdjuster implements CostAdjuster {
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public void prepareX(Ability ability, Game game) {
|
||||
int manaValue = Integer.MAX_VALUE;
|
||||
|
||||
Permanent sourcePermanent = game.getPermanent(ability.getSourceId());
|
||||
if (sourcePermanent != null
|
||||
&& sourcePermanent.getImprinted() != null
|
||||
&& !sourcePermanent.getImprinted().isEmpty()) {
|
||||
Card imprintedInstant = game.getCard(sourcePermanent.getImprinted().get(0));
|
||||
if (imprintedInstant != null) {
|
||||
manaValue = imprintedInstant.getManaValue();
|
||||
}
|
||||
}
|
||||
|
||||
ability.setVariableCostsValue(manaValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,10 +29,8 @@ public enum LegendaryCreatureCostAdjuster implements CostAdjuster {
|
|||
);
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Ability ability, Game game) {
|
||||
int count = game.getBattlefield().count(
|
||||
filter, ability.getControllerId(), ability, game
|
||||
);
|
||||
public void reduceCost(Ability ability, Game game) {
|
||||
int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game);
|
||||
if (count > 0) {
|
||||
CardUtil.reduceCost(ability, count);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public interface ManaCost extends Cost {
|
|||
ManaOptions getOptions();
|
||||
|
||||
/**
|
||||
* Return all options for paying the mana cost (this) while taking into accoutn if the player can pay life.
|
||||
* Return all options for paying the mana cost (this) while taking into account if the player can pay life.
|
||||
* Used to correctly highlight (or not) spells with Phyrexian mana depending on if the player can pay life costs.
|
||||
* <p>
|
||||
* E.g. Tezzeret's Gambit has a cost of {3}{U/P}.
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ManaOptions getOptions() {
|
||||
public final ManaOptions getOptions() {
|
||||
return getOptions(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
|
|||
this.add(new ColoredManaCost(ColoredManaSymbol.lookup(symbol.charAt(0))));
|
||||
} else // check X wasn't added before
|
||||
if (modifierForX == 0) {
|
||||
// count X occurence
|
||||
// count X occurrence
|
||||
for (String s : symbols) {
|
||||
if (s.equals("X")) {
|
||||
modifierForX++;
|
||||
|
|
|
|||
|
|
@ -3,17 +3,19 @@ package mage.abilities.costs.mana;
|
|||
import mage.Mana;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.VariableCost;
|
||||
import mage.abilities.costs.MinMaxVariableCost;
|
||||
import mage.abilities.costs.VariableCostType;
|
||||
import mage.abilities.mana.ManaOptions;
|
||||
import mage.constants.ColoredManaSymbol;
|
||||
import mage.filter.FilterMana;
|
||||
import mage.game.Game;
|
||||
import mage.players.ManaPool;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||
*/
|
||||
public final class VariableManaCost extends ManaCostImpl implements VariableCost {
|
||||
public class VariableManaCost extends ManaCostImpl implements MinMaxVariableCost {
|
||||
|
||||
// variable mana cost usage on 2019-06-20:
|
||||
// 1. as X value in spell/ability cast (announce X, set VariableManaCost as paid and add generic mana to pay instead)
|
||||
|
|
@ -23,7 +25,7 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
|
|||
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)
|
||||
protected boolean wasAnnounced = false;
|
||||
protected boolean wasAnnounced = false; // X was announced by player or auto-defined by CostAdjuster
|
||||
|
||||
protected FilterMana filter; // mana filter that can be used for that cost
|
||||
protected int minX = 0;
|
||||
|
|
@ -59,6 +61,18 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
|
|||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManaOptions getOptions(boolean canPayLifeCost) {
|
||||
ManaOptions res = new ManaOptions();
|
||||
|
||||
// limit mana options for better performance
|
||||
CardUtil.distributeValues(10, getMinX(), getMaxX()).forEach(value -> {
|
||||
res.add(Mana.GenericMana(value));
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) {
|
||||
// X mana cost always pays as generic mana
|
||||
|
|
@ -86,6 +100,10 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
|
|||
return this.isColorlessPaid(xPay);
|
||||
}
|
||||
|
||||
public boolean wasAnnounced() {
|
||||
return this.wasAnnounced;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VariableManaCost getUnpaid() {
|
||||
return this;
|
||||
|
|
@ -122,18 +140,22 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
|
|||
return this.xInstancesCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinX() {
|
||||
return minX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMinX(int minX) {
|
||||
this.minX = minX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxX() {
|
||||
return maxX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxX(int maxX) {
|
||||
this.maxX = maxX;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,27 @@ package mage.abilities.dynamicvalue.common;
|
|||
import mage.abilities.Ability;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.hint.Hint;
|
||||
import mage.abilities.hint.ValueHint;
|
||||
import mage.filter.FilterCard;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
|
||||
public enum CardsInControllerHandCount implements DynamicValue {
|
||||
instance;
|
||||
|
||||
instance(StaticFilters.FILTER_CARD_CARDS), // TODO: replace usage to ANY
|
||||
ANY(StaticFilters.FILTER_CARD_CARDS),
|
||||
CREATURES(StaticFilters.FILTER_CARD_CREATURES),
|
||||
LANDS(StaticFilters.FILTER_CARD_LANDS);
|
||||
|
||||
FilterCard filter;
|
||||
ValueHint hint;
|
||||
|
||||
CardsInControllerHandCount(FilterCard filter) {
|
||||
this.filter = filter;
|
||||
this.hint = new ValueHint(filter.getMessage() + " in your hand", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int calculate(Game game, Ability sourceAbility, Effect effect) {
|
||||
|
|
@ -22,16 +38,20 @@ public enum CardsInControllerHandCount implements DynamicValue {
|
|||
|
||||
@Override
|
||||
public CardsInControllerHandCount copy() {
|
||||
return CardsInControllerHandCount.instance;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "cards in your hand";
|
||||
return this.filter.getMessage() + " in your hand";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "1";
|
||||
}
|
||||
|
||||
public Hint getHint() {
|
||||
return this.hint;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -688,6 +688,8 @@ public class ContinuousEffects implements Serializable {
|
|||
* the battlefield for
|
||||
* {@link CostModificationEffect cost modification effects} and applies them
|
||||
* if necessary.
|
||||
* <p>
|
||||
* Warning, don't forget to call ability.adjustX before any cost modifications
|
||||
*
|
||||
* @param abilityToModify
|
||||
* @param game
|
||||
|
|
@ -695,6 +697,10 @@ public class ContinuousEffects implements Serializable {
|
|||
public void costModification(Ability abilityToModify, Game game) {
|
||||
List<CostModificationEffect> costEffects = getApplicableCostModificationEffects(game);
|
||||
|
||||
// add dynamic costs from X and other places
|
||||
abilityToModify.adjustCostsPrepare(game);
|
||||
|
||||
abilityToModify.adjustCostsModify(game, CostModificationType.INCREASE_COST);
|
||||
for (CostModificationEffect effect : costEffects) {
|
||||
if (effect.getModificationType() == CostModificationType.INCREASE_COST) {
|
||||
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId());
|
||||
|
|
@ -706,6 +712,7 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
abilityToModify.adjustCostsModify(game, CostModificationType.REDUCE_COST);
|
||||
for (CostModificationEffect effect : costEffects) {
|
||||
if (effect.getModificationType() == CostModificationType.REDUCE_COST) {
|
||||
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId());
|
||||
|
|
@ -717,6 +724,7 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
abilityToModify.adjustCostsModify(game, CostModificationType.SET_COST);
|
||||
for (CostModificationEffect effect : costEffects) {
|
||||
if (effect.getModificationType() == CostModificationType.SET_COST) {
|
||||
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId());
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ public class LookTargetHandChooseDiscardEffect extends OneShotEffect {
|
|||
}
|
||||
TargetCard target = new TargetCardInHand(upTo ? 0 : num, num, filter);
|
||||
if (controller.choose(Outcome.Discard, player.getHand(), target, source, game)) {
|
||||
// TODO: must fizzle discard effect on not full choice
|
||||
// - tests: affected (allow to choose and discard 1 instead 2)
|
||||
// - real game: need to check
|
||||
player.discard(new CardsImpl(target.getTargets()), false, source, game);
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -132,6 +132,8 @@ public class SuspendAbility extends SpecialAction {
|
|||
this.addEffect(new SuspendExileEffect(suspend));
|
||||
this.usesStack = false;
|
||||
if (suspend == Integer.MAX_VALUE) {
|
||||
// example: Suspend X-{X}{W}{W}. X can't be 0.
|
||||
// TODO: replace by costAdjuster for shared logic
|
||||
VariableManaCost xCosts = new VariableManaCost(VariableCostType.ALTERNATIVE);
|
||||
xCosts.setMinX(1);
|
||||
this.addCost(xCosts);
|
||||
|
|
|
|||
|
|
@ -426,6 +426,16 @@ public class StackAbility extends StackObjectImpl implements Ability {
|
|||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVariableCostsMinMax(int min, int max) {
|
||||
ability.setVariableCostsMinMax(min, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVariableCostsValue(int xValue) {
|
||||
ability.setVariableCostsValue(xValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getCostsTagMap() {
|
||||
return ability.getCostsTagMap();
|
||||
|
|
@ -778,14 +788,23 @@ public class StackAbility extends StackObjectImpl implements Ability {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CostAdjuster getCostAdjuster() {
|
||||
return costAdjuster;
|
||||
public void adjustX(Game game) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.prepareX(this, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Game game) {
|
||||
public void adjustCostsPrepare(Game game) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.adjustCosts(this, game);
|
||||
costAdjuster.prepareCost(this, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustCostsModify(Game game, CostModificationType costModificationType) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.modifyCost(this, game, costModificationType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -743,10 +743,14 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
|
||||
boolean shuffleCardsToLibrary(Card card, Game game, Ability source);
|
||||
|
||||
// set the value for X mana spells and abilities
|
||||
/**
|
||||
* Set the value for X mana spells and abilities
|
||||
*/
|
||||
int announceXMana(int min, int max, String message, Game game, Ability ability);
|
||||
|
||||
// set the value for non mana X costs
|
||||
/**
|
||||
* Set the value for non mana X costs
|
||||
*/
|
||||
int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variableCost);
|
||||
|
||||
// TODO: rework to use pair's list of effect + ability instead string's map
|
||||
|
|
|
|||
|
|
@ -3679,8 +3679,11 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
if (!copy.canActivate(playerId, game).canActivate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// apply dynamic costs and cost modification
|
||||
copy.adjustX(game);
|
||||
if (availableMana != null) {
|
||||
copy.adjustCosts(game);
|
||||
// TODO: need research, why it look at availableMana here - can delete condition?
|
||||
game.getContinuousEffects().costModification(copy, game);
|
||||
}
|
||||
boolean canBeCastRegularly = true;
|
||||
|
|
@ -3891,7 +3894,8 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
copyAbility = ability.copy();
|
||||
copyAbility.clearManaCostsToPay();
|
||||
copyAbility.addManaCostsToPay(manaCosts.copy());
|
||||
copyAbility.adjustCosts(game);
|
||||
// apply dynamic costs and cost modification
|
||||
copyAbility.adjustX(game);
|
||||
game.getContinuousEffects().costModification(copyAbility, game);
|
||||
|
||||
// reduced all cost
|
||||
|
|
@ -3963,12 +3967,9 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
// alternative cost reduce
|
||||
copyAbility = ability.copy();
|
||||
copyAbility.clearManaCostsToPay();
|
||||
// TODO: IDE warning:
|
||||
// Unchecked assignment: 'mage.abilities.costs.mana.ManaCosts' to
|
||||
// 'java.util.Collection<? extends mage.abilities.costs.mana.ManaCost>'.
|
||||
// Reason: 'manaCosts' has raw type, so result of copy is erased
|
||||
copyAbility.addManaCostsToPay(manaCosts.copy());
|
||||
copyAbility.adjustCosts(game);
|
||||
// apply dynamic costs and cost modification
|
||||
copyAbility.adjustX(game);
|
||||
game.getContinuousEffects().costModification(copyAbility, game);
|
||||
|
||||
// reduced all cost
|
||||
|
|
|
|||
|
|
@ -25,8 +25,13 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster {
|
|||
|
||||
@Override
|
||||
public void adjustTargets(Ability ability, Game game) {
|
||||
Target newTarget = blueprintTarget.copy();
|
||||
int count = dynamicValue.calculate(game, ability, ability.getEffects().get(0));
|
||||
ability.getTargets().clear();
|
||||
if (count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Target newTarget = blueprintTarget.copy();
|
||||
newTarget.setMaxNumberOfTargets(count);
|
||||
Filter filter = newTarget.getFilter();
|
||||
if (blueprintTarget.getMinNumberOfTargets() != 0) {
|
||||
|
|
@ -35,7 +40,6 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster {
|
|||
} else {
|
||||
newTarget.withTargetName(filter.getMessage() + " (up to " + count + " targets)");
|
||||
}
|
||||
ability.getTargets().clear();
|
||||
ability.addTarget(newTarget);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1079,6 +1079,53 @@ public final class CardUtil {
|
|||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static Set<UUID> getAllPossibleTargets(Cost cost, Game game, Ability source) {
|
||||
return cost.getTargets()
|
||||
.stream()
|
||||
.map(t -> t.possibleTargets(source.getControllerId(), source, game))
|
||||
.flatMap(Collection::stream)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute values between min and max and make sure that the values will be evenly distributed
|
||||
* Use it to limit possible values list like mana options
|
||||
*/
|
||||
public static List<Integer> distributeValues(int count, int min, int max) {
|
||||
List<Integer> res = new ArrayList<>();
|
||||
if (count <= 0 || min > max) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (min == max) {
|
||||
res.add(min);
|
||||
return res;
|
||||
}
|
||||
|
||||
int range = max - min + 1;
|
||||
|
||||
// low possible amount
|
||||
if (range <= count) {
|
||||
for (int i = 0; i < range; i++) {
|
||||
res.add(min + i);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// big possible amount, so skip some values
|
||||
double step = (double) (max - min) / (count - 1);
|
||||
for (int i = 0; i < count; i++) {
|
||||
res.add(min + (int) Math.round(i * step));
|
||||
}
|
||||
// make sure first and last elements are good
|
||||
res.set(0, min);
|
||||
if (res.size() > 1) {
|
||||
res.set(res.size() - 1, max);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* For finding the spell or ability on the stack for "becomes the target" triggers.
|
||||
*
|
||||
|
|
@ -1908,6 +1955,10 @@ public final class CardUtil {
|
|||
return defaultValue;
|
||||
}
|
||||
|
||||
public static int getSourceCostsTagX(Game game, Ability source, int defaultValue) {
|
||||
return getSourceCostsTag(game, source, "X", defaultValue);
|
||||
}
|
||||
|
||||
public static String addCostVerb(String text) {
|
||||
if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) {
|
||||
return text;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue