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:
Oleg Agafonov 2025-04-08 22:39:10 +04:00
parent 13a832ae00
commit bae3089abb
100 changed files with 1519 additions and 449 deletions

View file

@ -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);
}
}
}