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

@ -1863,8 +1863,11 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public int announceXMana(int min, int max, String message, Game game, Ability ability) {
log.debug("announceXMana");
//TODO: improve this
// current logic - use max possible mana
// TODO: add good/bad effects support
// TODO: add simple game simulations like declare blocker?
int numAvailable = getAvailableManaProducers(game).size() - ability.getManaCosts().manaValue();
if (numAvailable < 0) {
numAvailable = 0;
@ -1881,12 +1884,17 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variablCost) {
log.debug("announceXCost");
// current logic - use random non-zero value
// TODO: add good/bad effects support
// TODO: remove random logic
int value = RandomUtil.nextInt(CardUtil.overflowInc(max, 1));
if (value < min) {
value = min;
}
if (value < max) {
// do not use zero values
value++;
}
return value;

View file

@ -1709,6 +1709,8 @@ public class HumanPlayer extends PlayerImpl {
if (response.getInteger() != null) {
break;
}
// TODO: add response verify here
}
if (response.getInteger() != null) {

View file

@ -1,43 +1,30 @@
package mage.cards.a;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.common.DiscardTargetCost;
import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster;
import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.discard.LookTargetHandChooseDiscardEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.target.common.TargetCardInHand;
import mage.target.common.TargetOpponent;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author fireshoes
* @author fireshoes, JayDi85
*/
public final class AbandonHope extends CardImpl {
public AbandonHope(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{1}{B}");
// As an additional cost to cast Abandon Hope, discard X cards.
Ability ability = new SimpleStaticAbility(
Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X cards")
);
ability.setRuleAtTheTop(true);
this.addAbility(ability);
// As an additional cost to cast this spell, discard X cards.
DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_CARDS);
// Look at target opponent's hand and choose X cards from it. That player discards those cards.
this.getSpellAbility().addEffect(new LookTargetHandChooseDiscardEffect(false, GetXValue.instance, StaticFilters.FILTER_CARD_CARDS));
this.getSpellAbility().addTarget(new TargetOpponent());
this.getSpellAbility().setCostAdjuster(AbandonHopeAdjuster.instance);
}
private AbandonHope(final AbandonHope card) {
@ -49,15 +36,3 @@ public final class AbandonHope extends CardImpl {
return new AbandonHope(this);
}
}
enum AbandonHopeAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0);
if (xValue > 0) {
ability.addCost(new DiscardTargetCost(new TargetCardInHand(xValue, xValue, StaticFilters.FILTER_CARD_CARDS)));
}
}
}

View file

@ -1,27 +1,18 @@
package mage.cards.a;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.common.DiscardTargetCost;
import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.ReturnToHandTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.target.common.TargetCardInHand;
import mage.target.common.TargetCreaturePermanent;
import mage.target.targetadjustment.XTargetsCountAdjuster;
import mage.util.CardUtil;
import java.util.UUID;
/**
*
* @author jeffwadsworth
*/
public final class AetherTide extends CardImpl {
@ -29,10 +20,8 @@ public final class AetherTide extends CardImpl {
public AetherTide(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{U}");
// As an additional cost to cast Aether Tide, discard X creature cards.
Ability ability = new SimpleStaticAbility(Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X creature cards"));
ability.setRuleAtTheTop(true);
this.addAbility(ability);
// As an additional cost to cast this spell, discard X creature cards.
DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_CREATURES);
// Return X target creatures to their owners' hands.
Effect effect = new ReturnToHandTargetEffect();
@ -40,8 +29,6 @@ public final class AetherTide extends CardImpl {
this.getSpellAbility().addEffect(effect);
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
this.getSpellAbility().setTargetAdjuster(new XTargetsCountAdjuster());
this.getSpellAbility().setCostAdjuster(AetherTideCostAdjuster.instance);
}
private AetherTide(final AetherTide card) {
@ -53,15 +40,3 @@ public final class AetherTide extends CardImpl {
return new AetherTide(this);
}
}
enum AetherTideCostAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0);
if (xValue > 0) {
ability.addCost(new DiscardTargetCost(new TargetCardInHand(xValue, xValue, StaticFilters.FILTER_CARD_CREATURES)));
}
}
}

View file

@ -5,7 +5,6 @@ import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
@ -36,12 +35,8 @@ public final class AladdinsLamp extends CardImpl {
// {X}, {T}: The next time you would draw a card this turn, instead look at the top X cards of your library, put all but one of them on the bottom of your library in a random order, then draw a card. X can't be 0.
Ability ability = new SimpleActivatedAbility(new AladdinsLampEffect(), new ManaCostsImpl<>("{X}"));
ability.addCost(new TapSourceCost());
for (Object cost : ability.getManaCosts()) {
if (cost instanceof VariableManaCost) {
((VariableManaCost) cost).setMinX(1);
break;
}
}
ability.setVariableCostsMinMax(1, Integer.MAX_VALUE);
// TODO: add costAdjuster for min/max values due library size
this.addAbility(ability);
}

View file

@ -71,7 +71,7 @@ enum ArmMountedAnchorAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
// checking state
if (HeckbentCondition.instance.apply(game, ability)) {
CardUtil.reduceCost(ability, 2);

View file

@ -1,25 +1,28 @@
package mage.cards.b;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.EarlyTargetCost;
import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.InfoEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetOpponent;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.UUID;
/**
*
* @author awjackson
* @author awjackson, JayDi85
*/
public final class BargainingTable extends CardImpl {
@ -27,13 +30,10 @@ public final class BargainingTable extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{5}");
// {X}, {T}: Draw a card. X is the number of cards in an opponent's hand.
Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new ManaCostsImpl<>("{X}"));
Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new BargainingTableXCost());
ability.addCost(new TapSourceCost());
ability.addEffect(new InfoEffect("X is the number of cards in an opponent's hand"));
// You choose an opponent on announcement. This is not targeted, but a choice is still made.
// This choice is made before determining the value for X that is used in the cost. (2004-10-04)
ability.addTarget(new TargetOpponent(true));
ability.setCostAdjuster(BargainingTableAdjuster.instance);
ability.setCostAdjuster(BargainingTableCostAdjuster.instance);
this.addAbility(ability);
}
@ -47,26 +47,70 @@ public final class BargainingTable extends CardImpl {
}
}
enum BargainingTableAdjuster implements CostAdjuster {
class BargainingTableXCost extends VariableManaCost implements EarlyTargetCost {
// You choose an opponent on announcement. This is not targeted, but a choice is still made.
// This choice is made before determining the value for X that is used in the cost.
// (2004-10-04)
public BargainingTableXCost() {
super(VariableCostType.NORMAL, 1);
}
public BargainingTableXCost(final BargainingTableXCost cost) {
super(cost);
}
@Override
public void chooseTarget(Game game, Ability source, Player controller) {
Target targetOpponent = new TargetOpponent(true);
controller.choose(Outcome.Benefit, targetOpponent, source, game);
addTarget(targetOpponent);
}
@Override
public BargainingTableXCost copy() {
return new BargainingTableXCost(this);
}
}
enum BargainingTableCostAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
int handSize = Integer.MAX_VALUE;
if (game.inCheckPlayableState()) {
for (UUID playerId : CardUtil.getAllPossibleTargets(ability, game)) {
Player player = game.getPlayer(playerId);
if (player != null) {
handSize = Math.min(handSize, player.getHand().size());
}
}
} else {
Player player = game.getPlayer(ability.getFirstTarget());
if (player != null) {
handSize = player.getHand().size();
}
public void prepareX(Ability ability, Game game) {
// make sure early target used
BargainingTableXCost cost = ability.getManaCostsToPay().getVariableCosts().stream()
.filter(c -> c instanceof BargainingTableXCost)
.map(c -> (BargainingTableXCost) c)
.findFirst()
.orElse(null);
if (cost == null) {
throw new IllegalArgumentException("Wrong code usage: cost item lost");
}
if (game.inCheckPlayableState()) {
// possible X
int minHandSize = game.getOpponents(ability.getControllerId(), true).stream()
.map(game::getPlayer)
.filter(Objects::nonNull)
.mapToInt(p -> p.getHand().size())
.min()
.orElse(0);
int maxHandSize = game.getOpponents(ability.getControllerId(), true).stream()
.map(game::getPlayer)
.filter(Objects::nonNull)
.mapToInt(p -> p.getHand().size())
.max()
.orElse(Integer.MAX_VALUE);
ability.setVariableCostsMinMax(minHandSize, maxHandSize);
} else {
// real X
Player opponent = game.getPlayer(cost.getTargets().getFirstTarget());
if (opponent == null) {
throw new IllegalStateException("Wrong code usage: cost target lost");
}
ability.setVariableCostsValue(opponent.getHand().size());
}
ability.clearManaCostsToPay();
ability.addManaCostsToPay(new GenericManaCost(handSize));
}
}

View file

@ -19,10 +19,7 @@ import mage.abilities.hint.ValueHint;
import mage.abilities.keyword.TrampleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.constants.*;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.common.FilterCreaturePermanent;
@ -116,7 +113,7 @@ enum BaruWurmspeakerAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
int value = BaruWurmspeakerValue.instance.calculate(game, ability, null);
if (value > 0) {
CardUtil.reduceCost(ability, value);

View file

@ -58,7 +58,7 @@ enum BattlefieldButcherAdjuster implements CostAdjuster {
private static final Hint hint = new ValueHint("Creature cards in your graveyard", xValue);
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, xValue.calculate(game, ability, null));
}

View file

@ -3,20 +3,21 @@ package mage.cards.b;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.common.continuous.SetBasePowerToughnessEnchantedEffect;
import mage.abilities.keyword.EquipAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import mage.abilities.costs.mana.GenericManaCost;
import mage.constants.Outcome;
import mage.target.common.TargetControlledCreaturePermanent;
/**
* @author TheElk801
@ -35,7 +36,7 @@ public final class BeltOfGiantStrength extends CardImpl {
// Equip {10}. This ability costs {X} less to activate where X is the power of the creature it targets.
EquipAbility ability = new EquipAbility(Outcome.BoostCreature, new GenericManaCost(10), new TargetControlledCreaturePermanent(), false);
ability.setCostReduceText("This ability costs {X} less to activate, where X is the power of the creature it targets.");
ability.setCostAdjuster(BeltOfGiantStrengthAdjuster.instance);
ability.setCostAdjuster(BeltOfGiantStrengthCostAdjuster.instance);
this.addAbility(ability);
}
@ -49,33 +50,23 @@ public final class BeltOfGiantStrength extends CardImpl {
}
}
enum BeltOfGiantStrengthAdjuster implements CostAdjuster {
enum BeltOfGiantStrengthCostAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
int power;
if (game.inCheckPlayableState()) {
int maxPower = 0;
for (UUID permId : CardUtil.getAllPossibleTargets(ability, game)) {
Permanent permanent = game.getPermanent(permId);
if (permanent != null) {
int power = permanent.getPower().getValue();
if (power > maxPower) {
maxPower = power;
}
}
}
if (maxPower > 0) {
CardUtil.reduceCost(ability, maxPower);
}
power = CardUtil.getAllPossibleTargets(ability, game).stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.mapToInt(p -> p.getPower().getValue())
.max().orElse(0);
} else {
Permanent permanent = game.getPermanent(ability.getFirstTarget());
if (permanent != null) {
int power = permanent.getPower().getValue();
if (power > 0) {
CardUtil.reduceCost(ability, power);
}
}
power = Optional.ofNullable(game.getPermanent(ability.getFirstTarget()))
.map(p -> p.getPower().getValue())
.orElse(0);
}
CardUtil.reduceCost(ability, power);
}
}

View file

@ -60,7 +60,7 @@ enum BiteDownOnCrimeAdjuster implements CostAdjuster {
private static final OptionalAdditionalCost collectEvidenceCost = CollectEvidenceAbility.makeCost(6);
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
if (CollectedEvidenceCondition.instance.apply(game, ability)
|| (game.inCheckPlayableState() && collectEvidenceCost.canPay(ability, null, ability.getControllerId(), game))) {
CardUtil.reduceCost(ability, 2);

View file

@ -59,7 +59,7 @@ enum CallerOfTheHuntAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void prepareCost(Ability ability, Game game) {
if (game.inCheckPlayableState()) {
return;
}
@ -103,6 +103,7 @@ enum CallerOfTheHuntAdjuster implements CostAdjuster {
game.getState().setValue(sourceObject.getId() + "_type", maxSubType);
} else {
// human choose
// TODO: need early target cost instead dialog here
Effect effect = new ChooseCreatureTypeEffect(Outcome.Benefit);
effect.apply(game, ability);
}

View file

@ -4,6 +4,7 @@ import java.util.UUID;
import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.costs.CostImpl;
import mage.abilities.triggers.BeginningOfCombatTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.Cost;
@ -130,7 +131,7 @@ enum CaptainAmericaFirstAvengerValue implements DynamicValue {
}
}
class CaptainAmericaFirstAvengerUnattachCost extends EarlyTargetCost {
class CaptainAmericaFirstAvengerUnattachCost extends CostImpl implements EarlyTargetCost {
private static final FilterPermanent filter = new FilterEquipmentPermanent("equipment attached to this creature");
private static final FilterPermanent subfilter = new FilterControlledPermanent("{this}");

View file

@ -25,7 +25,7 @@ public final class ChanneledForce extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{U}{R}");
// As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS));
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Target player draws X cards. Channeled Force deals X damage to up to one target creature or planeswalker.
this.getSpellAbility().addEffect(new ChanneledForceEffect());

View file

@ -72,7 +72,7 @@ enum CrownOfGondorAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
if (MonarchIsSourceControllerCondition.instance.apply(game, ability)) {
CardUtil.reduceCost(ability, 3);
}

View file

@ -62,7 +62,7 @@ enum DeepwoodDenizenAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, DeepwoodDenizenValue.instance.calculate(game, ability, null));
}
}

View file

@ -5,6 +5,8 @@ import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl;
import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.common.DiscardTargetCost;
import mage.abilities.costs.common.DiscardXTargetCost;
import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster;
import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.common.DamageAllEffect;
import mage.abilities.effects.common.SacrificeAllEffect;
@ -12,6 +14,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledLandPermanent;
import mage.filter.common.FilterCreaturePermanent;
import mage.game.Game;
@ -28,8 +31,8 @@ public final class DevastatingDreams extends CardImpl {
public DevastatingDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{R}{R}");
// As an additional cost to cast Devastating Dreams, discard X cards at random.
this.getSpellAbility().addCost(new DevastatingDreamsAdditionalCost());
// As an additional cost to cast this spell, discard X cards at random.
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true).withRandom());
// Each player sacrifices X lands.
this.getSpellAbility().addEffect(new SacrificeAllEffect(GetXValue.instance, new FilterControlledLandPermanent("lands")));

View file

@ -1,12 +1,13 @@
package mage.cards.e;
import mage.ApprovingObject;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
@ -23,7 +24,6 @@ import mage.players.Player;
import mage.target.TargetCard;
import java.util.UUID;
import mage.ApprovingObject;
/**
* @author LevelX2
@ -44,7 +44,7 @@ public final class EliteArcanist extends CardImpl {
// {X}, {T}: Copy the exiled card. You may cast the copy without paying its mana cost. X is the converted mana cost of the exiled card.
Ability ability = new SimpleActivatedAbility(new EliteArcanistCopyEffect(), new ManaCostsImpl<>("{X}"));
ability.addCost(new TapSourceCost());
ability.setCostAdjuster(EliteArcanistAdjuster.instance);
ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance);
this.addAbility(ability);
}
@ -58,29 +58,6 @@ public final class EliteArcanist extends CardImpl {
}
}
enum EliteArcanistAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
Permanent sourcePermanent = game.getPermanent(ability.getSourceId());
if (sourcePermanent == null
|| sourcePermanent.getImprinted() == null
|| sourcePermanent.getImprinted().isEmpty()) {
return;
}
Card imprintedInstant = game.getCard(sourcePermanent.getImprinted().get(0));
if (imprintedInstant == null) {
return;
}
int cmc = imprintedInstant.getManaValue();
if (cmc > 0) {
ability.clearManaCostsToPay();
ability.addManaCostsToPay(new GenericManaCost(cmc));
}
}
}
class EliteArcanistImprintEffect extends OneShotEffect {
private static final FilterCard filter = new FilterCard("instant card from your hand");

View file

@ -64,7 +64,7 @@ enum EsquireOfTheKingAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
if (game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_CREATURE_LEGENDARY, ability, game, 1)) {
CardUtil.reduceCost(ability, 2);
}

View file

@ -80,8 +80,7 @@ enum EtheriumPteramanderAdjuster implements CostAdjuster {
}
@Override
public void adjustCosts(Ability ability, Game game) {
int count = artifactCount.calculate(game, ability, null);
CardUtil.reduceCost(ability, count);
public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, artifactCount.calculate(game, ability, null));
}
}

View file

@ -2,11 +2,11 @@ package mage.cards.f;
import mage.abilities.Ability;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.CostModificationType;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
@ -24,8 +24,8 @@ public final class Fireball extends CardImpl {
public Fireball(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{R}");
// Fireball deals X damage divided evenly, rounded down, among any number of target creatures and/or players.
// Fireball costs 1 more to cast for each target beyond the first.
// This spell costs {1} more to cast for each target beyond the first.
// Fireball deals X damage divided evenly, rounded down, among any number of targets.
this.getSpellAbility().addTarget(new FireballTargetCreatureOrPlayer(0, Integer.MAX_VALUE));
this.getSpellAbility().addEffect(new FireballEffect());
this.getSpellAbility().setCostAdjuster(FireballAdjuster.instance);
@ -45,10 +45,10 @@ enum FireballAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void increaseCost(Ability ability, Game game) {
int numTargets = ability.getTargets().isEmpty() ? 0 : ability.getTargets().get(0).getTargets().size();
if (numTargets > 1) {
ability.addManaCostsToPay(new GenericManaCost(numTargets - 1));
CardUtil.increaseCost(ability, numTargets - 1);
}
}
}

View file

@ -9,6 +9,7 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
@ -25,8 +26,8 @@ public final class Firestorm extends CardImpl {
public Firestorm(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}");
// As an additional cost to cast Firestorm, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true));
// As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Firestorm deals X damage to each of X target creatures and/or players.
this.getSpellAbility().addEffect(new FirestormEffect());

View file

@ -92,7 +92,7 @@ enum FugitiveCodebreakerAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, FugitiveCodebreakerDisguiseAbility.xValue.calculate(game, ability, null));
}
}

View file

@ -55,9 +55,9 @@ enum GhostfireBladeAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
// checking state
public void reduceCost(Ability ability, Game game) {
if (game.inCheckPlayableState()) {
// possible
if (CardUtil
.getAllPossibleTargets(ability, game)
.stream()
@ -67,6 +67,7 @@ enum GhostfireBladeAdjuster implements CostAdjuster {
return;
}
} else {
// real
Permanent permanent = game.getPermanent(ability.getFirstTarget());
if (permanent == null || !permanent.getColor(game).isColorless()) {
return;

View file

@ -74,7 +74,7 @@ enum GrimGiganotosaurusAdjuster implements CostAdjuster {
private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(filter);
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller != null) {
CardUtil.reduceCost(ability, xValue.calculate(game, ability, null));

View file

@ -61,7 +61,7 @@ enum HamletGluttonAdjuster implements CostAdjuster {
private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost();
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
if (BargainedCondition.instance.apply(game, ability)
|| (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) {
CardUtil.reduceCost(ability, 2);

View file

@ -2,9 +2,8 @@ package mage.cards.h;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.OneShotEffect;
import mage.cards.*;
@ -29,9 +28,8 @@ public final class HelmOfObedience extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}");
// {X}, {T}: Target opponent puts cards from the top of their library into their graveyard until a creature card or X cards are put into that graveyard this way, whichever comes first. If a creature card is put into that graveyard this way, sacrifice Helm of Obedience and put that card onto the battlefield under your control. X can't be 0.
VariableManaCost xCosts = new VariableManaCost(VariableCostType.NORMAL);
xCosts.setMinX(1);
Ability ability = new SimpleActivatedAbility(new HelmOfObedienceEffect(), xCosts);
Ability ability = new SimpleActivatedAbility(new HelmOfObedienceEffect(), new ManaCostsImpl<>("{X}"));
ability.setVariableCostsMinMax(1, Integer.MAX_VALUE);
ability.addCost(new TapSourceCost());
ability.addTarget(new TargetOpponent());
this.addAbility(ability);

View file

@ -117,6 +117,10 @@ final class HinataDawnCrownedEffectUtility
public static int getTargetCount(Game game, Ability abilityToModify)
{
if (game.inCheckPlayableState()) {
abilityToModify.getTargets().stream()
.mapToInt(a -> !a.isRequired() ? 0 : a.getMinNumberOfTargets())
.min()
.orElse(0);
Optional<Integer> max = abilityToModify.getTargets().stream().map(x -> x.getMaxNumberOfTargets()).max(Integer::compare);
int allPossibleSize = CardUtil.getAllPossibleTargets(abilityToModify, game).size();
return max.isPresent() ?

View file

@ -49,7 +49,7 @@ enum HurkylsFinalMeditationAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void increaseCost(Ability ability, Game game) {
if (!game.isActivePlayer(ability.getControllerId())) {
CardUtil.increaseCost(ability, 3);
}

View file

@ -82,7 +82,7 @@ enum HyldasCrownOfWinterAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
if (ability.getControllerId().equals(game.getActivePlayerId())) {
CardUtil.reduceCost(ability, 1);
}

View file

@ -52,7 +52,7 @@ enum IceOutAdjuster implements CostAdjuster {
private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost();
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
if (BargainedCondition.instance.apply(game, ability)
|| (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) {
CardUtil.reduceCost(ability, 1);

View file

@ -25,7 +25,7 @@ public final class InsidiousDreams extends CardImpl {
public InsidiousDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{B}");
// As an additional cost to cast Insidious Dreams, discard X cards.
// As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Search your library for X cards. Then shuffle your library and put those cards on top of it in any order.

View file

@ -54,7 +54,7 @@ enum JohannsStopgapAdjuster implements CostAdjuster {
private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost();
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
if (BargainedCondition.instance.apply(game, ability)
|| (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) {
CardUtil.reduceCost(ability, 2);

View file

@ -60,7 +60,7 @@ enum KamiOfJealousThirstAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void increaseCost(Ability ability, Game game) {
int amount = CardsDrawnThisTurnDynamicValue.instance.calculate(game, ability, null);
if (amount >= 3) {
CardUtil.adjustCost(ability, new ManaCostsImpl<>("{4}{B}"), false);

View file

@ -1,40 +1,43 @@
package mage.cards.k;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.common.DiscardTargetCost;
import mage.abilities.costs.CostImpl;
import mage.abilities.costs.EarlyTargetCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.ComparisonType;
import mage.constants.Zone;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.ManaValuePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetAnyTarget;
import mage.target.common.TargetCardInHand;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author anonymous
* @author JayDi85
*/
public final class KnollspineInvocation extends CardImpl {
private static final FilterCard filter = new FilterCard("a card with mana value X");
protected static final FilterCard filter = new FilterCard("a card with mana value X");
public KnollspineInvocation(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}{R}");
// {X}, Discard a card with converted mana cost X: Knollspine Invocation deals X damage to any target.
// {X}, Discard a card with mana value X: This enchantment deals X damage to any target.
Ability ability = new SimpleActivatedAbility(new DamageTargetEffect(GetXValue.instance, true), new ManaCostsImpl<>("{X}"));
ability.addCost(new DiscardTargetCost(new TargetCardInHand(filter)));
ability.addCost(new KnollspineInvocationDiscardCost());
ability.addTarget(new TargetAnyTarget());
ability.setCostAdjuster(KnollspineInvocationAdjuster.instance);
this.addAbility(ability);
@ -50,22 +53,103 @@ public final class KnollspineInvocation extends CardImpl {
}
}
class KnollspineInvocationDiscardCost extends CostImpl implements EarlyTargetCost {
// discard card with early target selection, so {X} mana cost can be setup after choose
public KnollspineInvocationDiscardCost() {
super();
this.text = "Discard a card with mana value X";
}
public KnollspineInvocationDiscardCost(final KnollspineInvocationDiscardCost cost) {
super(cost);
}
@Override
public KnollspineInvocationDiscardCost copy() {
return new KnollspineInvocationDiscardCost(this);
}
@Override
public void chooseTarget(Game game, Ability source, Player controller) {
Target target = new TargetCardInHand().withChooseHint("to discard with mana value for X");
controller.choose(Outcome.Discard, target, source, game);
addTarget(target);
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
Player controller = game.getPlayer(controllerId);
return controller != null && !controller.getHand().isEmpty();
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
this.paid = false;
Player controller = game.getPlayer(controllerId);
if (controller == null) {
return false;
}
Card card = controller.getHand().get(this.getTargets().getFirstTarget(), game);
if (card == null) {
return false;
}
this.paid = controller.discard(card, true, source, game);
return this.paid;
}
}
enum KnollspineInvocationAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0);
for (Cost cost : ability.getCosts()) {
if (!(cost instanceof DiscardTargetCost)) {
continue;
}
DiscardTargetCost discardCost = (DiscardTargetCost) cost;
discardCost.getTargets().clear();
FilterCard adjustedFilter = new FilterCard("a card with mana value X");
adjustedFilter.add(new ManaValuePredicate(ComparisonType.EQUAL_TO, xValue));
discardCost.addTarget(new TargetCardInHand(adjustedFilter));
public void prepareX(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller == null) {
return;
}
// make sure early target used
VariableManaCost costX = ability.getManaCostsToPay().stream()
.filter(c -> c instanceof VariableManaCost)
.map(c -> (VariableManaCost) c)
.findFirst()
.orElse(null);
if (costX == null) {
throw new IllegalArgumentException("Wrong code usage: costX lost");
}
KnollspineInvocationDiscardCost costDiscard = ability.getCosts().stream()
.filter(c -> c instanceof KnollspineInvocationDiscardCost)
.map(c -> (KnollspineInvocationDiscardCost) c)
.findFirst()
.orElse(null);
if (costDiscard == null) {
throw new IllegalArgumentException("Wrong code usage: costDiscard lost");
}
if (game.inCheckPlayableState()) {
// possible X
int minManaValue = controller.getHand().getCards(game).stream()
.mapToInt(MageObject::getManaValue)
.min()
.orElse(0);
int maxManaValue = controller.getHand().getCards(game).stream()
.mapToInt(MageObject::getManaValue)
.max()
.orElse(0);
ability.setVariableCostsMinMax(minManaValue, maxManaValue);
} else {
// real X
Card card = controller.getHand().get(costDiscard.getTargets().getFirstTarget(), game);
if (card == null) {
throw new IllegalStateException("Wrong code usage: card to discard lost");
}
ability.setVariableCostsValue(card.getManaValue());
}
}
}

View file

@ -48,7 +48,7 @@ enum LoreseekersStoneAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void increaseCost(Ability ability, Game game) {
Player player = game.getPlayer(ability.getControllerId());
if (player != null) {
CardUtil.increaseCost(ability, player.getHand().size());

View file

@ -64,7 +64,7 @@ enum MariposaMilitaryBaseAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, SourceControllerCountersCount.RAD.calculate(game, ability, null));
}
}

View file

@ -66,7 +66,7 @@ enum MirrorOfGaladrielAdjuster implements CostAdjuster {
}
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
int value = game.getBattlefield().count(
StaticFilters.FILTER_CONTROLLED_CREATURE_LEGENDARY,
ability.getControllerId(), ability, game

View file

@ -74,7 +74,7 @@ enum MobilizedDistrictAdjuster implements CostAdjuster {
}
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller != null) {
int count = cardsCount.calculate(game, ability, null);

View file

@ -8,6 +8,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.target.common.TargetCreatureOrPlaneswalker;
import mage.target.targetadjustment.XTargetsCountAdjuster;
@ -21,8 +22,8 @@ public final class NahirisWrath extends CardImpl {
public NahirisWrath(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{R}");
// As an additional cost to cast Nahiri's Wrath, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true));
// As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Nahiri's Wrath deals damage equal to the total converted mana cost of the discarded cards to each of up to X target creatures and/or planeswalkers.
Effect effect = new DamageTargetEffect(DiscardCostCardManaValue.instance);

View file

@ -6,11 +6,9 @@ import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.common.ExileFromGraveCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.dynamicvalue.common.SignInversionDynamicValue;
@ -86,16 +84,12 @@ enum NecropolisFiendCostAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void prepareX(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller == null) {
return;
}
for (VariableCost variableCost : ability.getManaCostsToPay().getVariableCosts()) {
if (variableCost instanceof VariableManaCost) {
((VariableManaCost) variableCost).setMaxX(controller.getGraveyard().size());
}
}
ability.setVariableCostsMinMax(0, controller.getGraveyard().size());
}
}

View file

@ -63,7 +63,7 @@ enum NemesisOfMortalsAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller != null) {
CardUtil.reduceCost(ability, controller.getGraveyard().count(StaticFilters.FILTER_CARD_CREATURE, game));

View file

@ -22,8 +22,8 @@ public final class NostalgicDreams extends CardImpl {
public NostalgicDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{G}{G}");
// As an additional cost to cast Nostalgic Dreams, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true));
// As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Return X target cards from your graveyard to your hand.
Effect effect = new ReturnFromGraveyardToHandTargetEffect();

View file

@ -3,7 +3,6 @@ package mage.cards.o;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.InfoEffect;
import mage.cards.*;
@ -28,7 +27,7 @@ public final class OpenTheWay extends CardImpl {
this.addAbility(new SimpleStaticAbility(
Zone.ALL, new InfoEffect("X can't be greater than the number of players in the game")
).setRuleAtTheTop(true));
this.getSpellAbility().setCostAdjuster(OpenTheWayAdjuster.instance);
this.getSpellAbility().setCostAdjuster(OpenTheWayCostAdjuster.instance);
// Reveal cards from the top of your library until you reveal X land cards. Put those land cards onto the battlefield tapped and the rest on the bottom of your library in a random order.
this.getSpellAbility().addEffect(new OpenTheWayEffect());
@ -44,14 +43,12 @@ public final class OpenTheWay extends CardImpl {
}
}
enum OpenTheWayAdjuster implements CostAdjuster {
enum OpenTheWayCostAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
int playerCount = game.getPlayers().size();
CardUtil.castStream(ability.getCosts().stream(), VariableManaCost.class)
.forEach(cost -> cost.setMaxX(playerCount));
public void prepareX(Ability ability, Game game) {
ability.setVariableCostsMinMax(0, game.getState().getPlayersInRange(ability.getControllerId(), game, true).size());
}
}

View file

@ -33,7 +33,6 @@ import mage.util.CardUtil;
import java.util.UUID;
/**
*
* @author Arketec
*/
public final class OsgirTheReconstructor extends CardImpl {
@ -81,15 +80,15 @@ enum OsgirTheReconstructorCostAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void prepareCost(Ability ability, Game game) {
int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0);
Player controller = game.getPlayer(ability.getControllerId());
if (controller == null) {
return;
}
FilterCard filter = new FilterArtifactCard("an artifact card with mana value "+xValue+" from your graveyard");
FilterCard filter = new FilterArtifactCard("an artifact card with mana value " + xValue + " from your graveyard");
filter.add(new ManaValuePredicate(ComparisonType.EQUAL_TO, xValue));
for (Cost cost: ability.getCosts()) {
for (Cost cost : ability.getCosts()) {
if (cost instanceof ExileFromGraveCost) {
cost.getTargets().set(0, new TargetCardInYourGraveyard(filter));
}
@ -104,7 +103,7 @@ class OsgirTheReconstructorCreateArtifactTokensEffect extends OneShotEffect {
this.staticText = "Create two tokens that are copies of the exiled card.";
}
private OsgirTheReconstructorCreateArtifactTokensEffect(final OsgirTheReconstructorCreateArtifactTokensEffect effect) {
private OsgirTheReconstructorCreateArtifactTokensEffect(final OsgirTheReconstructorCreateArtifactTokensEffect effect) {
super(effect);
}
@ -127,11 +126,11 @@ class OsgirTheReconstructorCreateArtifactTokensEffect extends OneShotEffect {
effect.setTargetPointer(new FixedTarget(card.getId(), game.getState().getZoneChangeCounter(card.getId())));
effect.apply(game, source);
return true;
return true;
}
@Override
public OsgirTheReconstructorCreateArtifactTokensEffect copy() {
public OsgirTheReconstructorCreateArtifactTokensEffect copy() {
return new OsgirTheReconstructorCreateArtifactTokensEffect(this);
}
}

View file

@ -46,7 +46,7 @@ enum PhyrexianPurgeCostAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void increaseCost(Ability ability, Game game) {
int numTargets = ability.getTargets().get(0).getTargets().size();
if (numTargets > 0) {
ability.addCost(new PayLifeCost(numTargets * 3));

View file

@ -81,7 +81,7 @@ enum PlateArmorAdjuster implements CostAdjuster {
}
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller != null) {
int count = equipmentCount.calculate(game, ability, null);

View file

@ -4,11 +4,8 @@ import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
@ -42,10 +39,10 @@ public final class PrototypePortal extends CardImpl {
.setAbilityWord(AbilityWord.IMPRINT)
);
// {X}, {tap}: Create a token that's a copy of the exiled card. X is the converted mana cost of that card.
// {X}, {T}: Create a token that's a copy of the exiled card. X is the converted mana cost of that card.
Ability ability = new SimpleActivatedAbility(new PrototypePortalCreateTokenEffect(), new ManaCostsImpl<>("{X}"));
ability.addCost(new TapSourceCost());
ability.setCostAdjuster(PrototypePortalAdjuster.instance);
ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance);
this.addAbility(ability);
}
@ -59,31 +56,6 @@ public final class PrototypePortal extends CardImpl {
}
}
enum PrototypePortalAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
Permanent card = game.getPermanent(ability.getSourceId());
if (card != null) {
if (!card.getImprinted().isEmpty()) {
Card imprinted = game.getCard(card.getImprinted().get(0));
if (imprinted != null) {
ability.clearManaCostsToPay();
ability.addManaCostsToPay(new GenericManaCost(imprinted.getManaValue()));
}
}
}
// no {X} anymore as we already have imprinted the card with defined manacost
for (ManaCost cost : ability.getManaCostsToPay()) {
if (cost instanceof VariableCost) {
cost.setPaid();
}
}
}
}
class PrototypePortalEffect extends OneShotEffect {
PrototypePortalEffect() {

View file

@ -70,7 +70,7 @@ enum PteramanderAdjuster implements CostAdjuster {
}
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
int count = cardsCount.calculate(game, ability, null);
CardUtil.reduceCost(ability, count);
}

View file

@ -56,7 +56,7 @@ enum QuestForTheNecropolisAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
int amount = Optional
.ofNullable(ability.getSourcePermanentIfItStillExists(game))
.map(permanent -> permanent.getCounters(game).getCount(CounterType.QUEST))

View file

@ -69,7 +69,7 @@ enum RazorlashTransmograntAdjuster implements CostAdjuster {
}
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
if (makeMap(game, ability).values().stream().anyMatch(x -> x >= 4)) {
CardUtil.reduceCost(ability, 4);
}

View file

@ -20,7 +20,7 @@ public final class RestlessDreams extends CardImpl {
public RestlessDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{B}");
// As an additional cost to cast Restless Dreams, discard X cards.
// As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Return X target creature cards from your graveyard to your hand.

View file

@ -65,7 +65,7 @@ enum SanctumOfTranquilLightAdjuster implements CostAdjuster {
}
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller != null) {
CardUtil.reduceCost(ability, count.calculate(game, ability, null));

View file

@ -1,23 +1,15 @@
package mage.cards.s;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.common.DiscardTargetCost;
import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster;
import mage.abilities.dynamicvalue.common.CardsInControllerHandCount;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.DestroyTargetEffect;
import mage.abilities.effects.common.InfoEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Zone;
import mage.filter.common.FilterLandCard;
import mage.game.Game;
import mage.target.common.TargetCardInHand;
import mage.filter.StaticFilters;
import mage.target.common.TargetLandPermanent;
import mage.target.targetadjustment.XTargetsCountAdjuster;
import mage.util.CardUtil;
import java.util.UUID;
@ -29,10 +21,8 @@ public final class ScorchedEarth extends CardImpl {
public ScorchedEarth(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{R}");
// As an additional cost to cast Scorched Earth, discard X land cards.
Ability ability = new SimpleStaticAbility(Zone.ALL, new InfoEffect("as an additional cost to cast this spell, discard X land cards"));
ability.setRuleAtTheTop(true);
this.addAbility(ability);
// As an additional cost to cast this spell, discard X land cards.
DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_LANDS);
// Destroy X target lands.
Effect effect = new DestroyTargetEffect();
@ -40,7 +30,7 @@ public final class ScorchedEarth extends CardImpl {
this.getSpellAbility().addTarget(new TargetLandPermanent());
this.getSpellAbility().addEffect(effect);
this.getSpellAbility().setTargetAdjuster(new XTargetsCountAdjuster());
this.getSpellAbility().setCostAdjuster(ScorchedEarthCostAdjuster.instance);
this.getSpellAbility().addHint(CardsInControllerHandCount.LANDS.getHint());
}
private ScorchedEarth(final ScorchedEarth card) {
@ -52,15 +42,3 @@ public final class ScorchedEarth extends CardImpl {
return new ScorchedEarth(this);
}
}
enum ScorchedEarthCostAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0);
if (xValue > 0) {
ability.addCost(new DiscardTargetCost(new TargetCardInHand(xValue, xValue, new FilterLandCard("land cards"))));
}
}
}

View file

@ -60,7 +60,7 @@ enum SeafloorStalkerAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller != null) {
int count = PartyCount.instance.calculate(game, ability, null);

View file

@ -55,7 +55,7 @@ enum SewerCrocodileAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
if (DifferentManaValuesInGraveCondition.FIVE.apply(game, ability)) {
CardUtil.reduceCost(ability, 3);
}

View file

@ -7,6 +7,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.filter.common.FilterCreaturePermanent;
import java.util.UUID;
@ -19,8 +20,8 @@ public final class SickeningDreams extends CardImpl {
public SickeningDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{B}");
// As an additional cost to cast Sickening Dreams, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true));
// As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Sickening Dreams deals X damage to each creature and each player.
this.getSpellAbility().addEffect(new DamageEverythingEffect(GetXValue.instance, new FilterCreaturePermanent()));

View file

@ -61,7 +61,7 @@ enum SkeletalScryingAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void prepareCost(Ability ability, Game game) {
int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0);
if (xValue > 0) {
ability.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard(xValue, xValue, StaticFilters.FILTER_CARDS_FROM_YOUR_GRAVEYARD)));

View file

@ -3,11 +3,8 @@ package mage.cards.s;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenCopyTargetEffect;
@ -45,7 +42,7 @@ public final class SoulFoundry extends CardImpl {
// {X}, {T}: Create a token that's a copy of the exiled card. X is the converted mana cost of that card.
Ability ability = new SimpleActivatedAbility(new SoulFoundryEffect(), new ManaCostsImpl<>("{X}"));
ability.addCost(new TapSourceCost());
ability.setCostAdjuster(SoulFoundryAdjuster.instance);
ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance);
this.addAbility(ability);
}
@ -59,31 +56,6 @@ public final class SoulFoundry extends CardImpl {
}
}
enum SoulFoundryAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
Permanent sourcePermanent = game.getPermanent(ability.getSourceId());
if (sourcePermanent != null) {
if (!sourcePermanent.getImprinted().isEmpty()) {
Card imprinted = game.getCard(sourcePermanent.getImprinted().get(0));
if (imprinted != null) {
ability.clearManaCostsToPay();
ability.addManaCostsToPay(new GenericManaCost(imprinted.getManaValue()));
}
}
}
// no {X} anymore as we already have imprinted the card with defined manacost
for (ManaCost cost : ability.getManaCostsToPay()) {
if (cost instanceof VariableCost) {
cost.setPaid();
}
}
}
}
class SoulFoundryImprintEffect extends OneShotEffect {
private static final FilterCard filter = new FilterCard("creature card from your hand");

View file

@ -61,7 +61,7 @@ enum TamiyosLogbookAdjuster implements CostAdjuster {
);
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game);
CardUtil.reduceCost(ability, count);
}

View file

@ -170,7 +170,7 @@ enum ThroneOfEldraineAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void prepareCost(Ability ability, Game game) {
ObjectColor color = (ObjectColor) game.getState().getValue(ability.getSourceId() + "_color");
if (color == null) {
return;

View file

@ -81,7 +81,7 @@ enum TowashiGuideBotAdjuster implements CostAdjuster {
}
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, game.getBattlefield().count(
filter, ability.getControllerId(), ability, game
));

View file

@ -20,7 +20,7 @@ public final class TurbulentDreams extends CardImpl {
public TurbulentDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{U}{U}");
// As an additional cost to cast Turbulent Dreams, discard X cards.
// As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Return X target nonland permanents to their owners' hands.

View file

@ -61,7 +61,7 @@ enum UrgentNecropsyAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void prepareCost(Ability ability, Game game) {
int xValue = ability
.getTargets()
.stream()

View file

@ -21,8 +21,8 @@ public final class VengefulDreams extends CardImpl {
public VengefulDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{W}{W}");
// As an additional cost to cast Vengeful Dreams, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true));
// As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Exile X target attacking creatures.
Effect effect = new ExileTargetEffect();

View file

@ -65,7 +65,7 @@ enum VindictiveFlamestokerAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
int amount = Optional
.ofNullable(ability.getSourcePermanentIfItStillExists(game))
.filter(Objects::nonNull)

View file

@ -132,7 +132,7 @@ enum VoldarenEstateCostAdjuster implements CostAdjuster {
}
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, vampireCount.calculate(game, ability, null));
}
}

View file

@ -72,7 +72,7 @@ enum VoodooDollAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void prepareCost(Ability ability, Game game) {
Permanent sourcePermanent = game.getPermanent(ability.getSourceId());
if (sourcePermanent != null) {
int pin = sourcePermanent.getCounters(game).getCount(CounterType.PIN);

View file

@ -101,7 +101,7 @@ enum WaytaTrainerProdigyAdjuster implements CostAdjuster {
instance;
@Override
public void adjustCosts(Ability ability, Game game) {
public void reduceCost(Ability ability, Game game) {
if (game.inCheckPlayableState()) {
int controllerTargets = 0; //number of possible targets controlled by the ability's controller
for (UUID permId : CardUtil.getAllPossibleTargets(ability, game)) {

View file

@ -0,0 +1,587 @@
package org.mage.test.cards.cost.additional;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.constants.CardType;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.token.custom.CreatureToken;
import mage.util.CardUtil;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
import java.util.Arrays;
/**
* @author JayDi85
*/
public class AdjusterCostTest extends CardTestPlayerBaseWithAIHelps {
private void prepareCustomCardInHand(String cardName, String spellManaCost, CostAdjuster costAdjuster) {
SpellAbility spellAbility = new SpellAbility(new ManaCostsImpl<>(spellManaCost), cardName);
if (costAdjuster != null) {
spellAbility.setCostAdjuster(costAdjuster);
}
addCustomCardWithAbility(
cardName,
playerA,
null,
spellAbility,
CardType.ENCHANTMENT,
spellManaCost,
Zone.HAND
);
}
private void prepareCustomPermanent(String cardName, String abilityName, String abilityManaCost, CostAdjuster costAdjuster) {
Ability ability = new SimpleActivatedAbility(
new CreateTokenEffect(new CreatureToken(1, 1).withName("test token")).setText(abilityName),
new ManaCostsImpl<>(abilityManaCost)
);
if (costAdjuster != null) {
ability.setCostAdjuster(costAdjuster);
}
addCustomCardWithAbility(cardName, playerA, ability);
}
@Test
public void test_DistributeValues() {
// make sure it can distribute values between min and max and skip useless values (example: mana optimization)
Assert.assertEquals(Arrays.asList(), CardUtil.distributeValues(0, 0, 0));
Assert.assertEquals(Arrays.asList(), CardUtil.distributeValues(0, -10, 10));
Assert.assertEquals(Arrays.asList(), CardUtil.distributeValues(0, Integer.MIN_VALUE, Integer.MAX_VALUE));
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 0));
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 1));
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 2));
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 3));
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 9));
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(2, 0, 0));
Assert.assertEquals(Arrays.asList(0, 1), CardUtil.distributeValues(2, 0, 1));
Assert.assertEquals(Arrays.asList(0, 2), CardUtil.distributeValues(2, 0, 2));
Assert.assertEquals(Arrays.asList(0, 3), CardUtil.distributeValues(2, 0, 3));
Assert.assertEquals(Arrays.asList(0, 9), CardUtil.distributeValues(2, 0, 9));
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(3, 0, 0));
Assert.assertEquals(Arrays.asList(0, 1), CardUtil.distributeValues(3, 0, 1));
Assert.assertEquals(Arrays.asList(0, 1, 2), CardUtil.distributeValues(3, 0, 2));
Assert.assertEquals(Arrays.asList(0, 2, 3), CardUtil.distributeValues(3, 0, 3));
Assert.assertEquals(Arrays.asList(0, 5, 9), CardUtil.distributeValues(3, 0, 9));
Assert.assertEquals(Arrays.asList(10, 15, 20), CardUtil.distributeValues(3, 10, 20));
Assert.assertEquals(Arrays.asList(10, 16, 21), CardUtil.distributeValues(3, 10, 21));
Assert.assertEquals(Arrays.asList(10, 16, 22), CardUtil.distributeValues(3, 10, 22));
Assert.assertEquals(Arrays.asList(10, 17, 23), CardUtil.distributeValues(3, 10, 23));
Assert.assertEquals(Arrays.asList(10, 20, 29), CardUtil.distributeValues(3, 10, 29));
Assert.assertEquals(Arrays.asList(10), CardUtil.distributeValues(5, 10, 10));
Assert.assertEquals(Arrays.asList(10, 11), CardUtil.distributeValues(5, 10, 11));
Assert.assertEquals(Arrays.asList(10, 11, 12), CardUtil.distributeValues(5, 10, 12));
Assert.assertEquals(Arrays.asList(10, 11, 12, 13), CardUtil.distributeValues(5, 10, 13));
Assert.assertEquals(Arrays.asList(10, 11, 13, 14, 15), CardUtil.distributeValues(5, 10, 15));
Assert.assertEquals(Arrays.asList(10, 13, 15, 18, 20), CardUtil.distributeValues(5, 10, 20));
}
@Test
public void test_X_SpellAbility() {
prepareCustomCardInHand("test card", "{X}{1}", null);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "test card");
setChoice(playerA, "X=2");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "test card", 1);
}
@Test
public void test_X_ActivatedAbility() {
prepareCustomPermanent("test card", "test ability", "{X}{1}", null);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}{1}:");
setChoice(playerA, "X=2");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "test token", 1);
}
@Test
public void test_prepareX_SpellAbility_TestFrameworkMustCatchLimits() {
prepareCustomCardInHand("test card", "{X}{1}", new CostAdjuster() {
@Override
public void prepareX(Ability ability, Game game) {
ability.setVariableCostsMinMax(0, 1);
}
});
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "test card");
setChoice(playerA, "X=2");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
try {
execute();
} catch (AssertionError e) {
Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("Found wrong X value = 2"));
Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("from 0 to 1"));
return;
}
Assert.fail("test must fail");
}
@Test
@Ignore // TODO: AI must support game simulations for X choice, see announceXMana
public void test_prepareX_SpellAbility_AI() {
prepareCustomCardInHand("test card", "{X}{1}", new CostAdjuster() {
@Override
public void prepareX(Ability ability, Game game) {
ability.setVariableCostsMinMax(0, 10);
}
});
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
// AI must play card and use min good value
// it's bad to set X=1 for battlefield score cause card will give same score for X=0, X=1
aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "test card", 1);
assertTappedCount("Forest", true, 1); // must choose X=0
}
@Test
public void test_prepareX_ActivatedAbility_TestFrameworkMustCatchLimits() {
prepareCustomPermanent("test card", "test ability", "{X}{1}", new CostAdjuster() {
@Override
public void prepareX(Ability ability, Game game) {
ability.setVariableCostsMinMax(0, 1);
}
});
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}{1}:");
setChoice(playerA, "X=2");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
try {
execute();
} catch (AssertionError e) {
Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("Found wrong X value = 2"));
Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("from 0 to 1"));
return;
}
Assert.fail("test must fail");
}
@Test
@Ignore // TODO: AI must support game simulations for X choice, see announceXMana
// TODO: implement AI and add tests for non-mana X values (announceXCost)
public void test_prepareX_ActivatedAbility_AI() {
prepareCustomPermanent("test card", "test ability", "{X}{1}", new CostAdjuster() {
@Override
public void prepareX(Ability ability, Game game) {
ability.setVariableCostsMinMax(0, 10);
}
});
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
// AI must activate ability with min good value for X
// it's bad to set X=1 for battlefield score cause card will give same score for X=0, X=1
aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "test token", 1);
assertTappedCount("Forest", true, 1); // must choose X=0
}
@Test
public void test_prepareX_SpellAbility_ScorchedEarth_PayZero() {
// with X announce
// As an additional cost to cast this spell, discard X land cards.
// Destroy X target lands.
addCard(Zone.HAND, playerA, "Scorched Earth"); // {X}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 0 + 1);
addCard(Zone.HAND, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerB, "Forest", 10);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scorched Earth");
setChoice(playerA, "X=0");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
}
@Test
public void test_prepareX_SpellAbility_ScorchedEarth_PaySome() {
// with X announce
// As an additional cost to cast this spell, discard X land cards.
// Destroy X target lands.
addCard(Zone.HAND, playerA, "Scorched Earth"); // {X}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2 + 1);
addCard(Zone.HAND, playerA, "Island", 10);
addCard(Zone.BATTLEFIELD, playerB, "Forest", 10);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scorched Earth");
setChoice(playerA, "X=2");
addTarget(playerA, "Forest", 2); // to destroy
setChoice(playerA, "Island", 2); // discard cost
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
}
@Test
public void test_prepareX_SpellAbility_ScorchedEarth_PayAll() {
// with X announce
// As an additional cost to cast this spell, discard X land cards.
// Destroy X target lands.
addCard(Zone.HAND, playerA, "Scorched Earth"); // {X}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10 + 1);
addCard(Zone.HAND, playerA, "Island", 10);
addCard(Zone.BATTLEFIELD, playerB, "Forest", 10);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scorched Earth");
setChoice(playerA, "X=10");
addTarget(playerA, "Forest", 10); // to destroy
setChoice(playerA, "Island", 10); // discard cost
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
}
@Test
public void test_prepareX_ActivatedAbility_BargainingTable_PayZero() {
// with direct X (without announce)
// {X}, {T}: Draw a card. X is the number of cards in an opponent's hand.
addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
//
// no cards in opponent's hand
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:");
setChoice(playerA, playerB.getName());
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, 1);
}
@Test
public void test_prepareX_ActivatedAbility_BargainingTable_PaySome() {
// with direct X (without announce)
// {X}, {T}: Draw a card. X is the number of cards in an opponent's hand.
addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
//
addCard(Zone.HAND, playerB, "Forest", 3);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:");
setChoice(playerA, playerB.getName());
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, 1);
}
@Test
public void test_prepareX_ActivatedAbility_BargainingTable_CantPay() {
// with direct X (without announce)
// {X}, {T}: Draw a card. X is the number of cards in an opponent's hand.
addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
//
addCard(Zone.HAND, playerB, "Forest", 3);
// must not request opponent choice because it must see min hand size as 3
checkPlayableAbility("must not able to activate due lack of mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:", false);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, 0);
}
@Test
public void test_prepareX_ActivatedAbility_BargainingTable_UnboundFlourishingMustCopy() {
// with direct X (without announce)
// {X}, {T}: Draw a card. X is the number of cards in an opponent's hand.
addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
//
addCard(Zone.HAND, playerB, "Forest", 3);
//
// Whenever you cast a permanent spell with a mana cost that contains {X}, double the value of X.
// Whenever you cast an instant or sorcery spell or activate an ability, if that spells mana cost or that
// abilitys activation cost contains {X}, copy that spell or ability. You may choose new targets for the copy.
addCard(Zone.BATTLEFIELD, playerA, "Unbound Flourishing", 1);
// Unbound Flourishing must see {X} mana cost and duplicate ability on stack
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:");
setChoice(playerA, playerB.getName());
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, 2); // from original and copied abilities
}
@Test
public void test_prepareX_NecropolisFiend() {
// {X}, {T}, Exile X cards from your graveyard: Target creature gets -X/-X until end of turn.
addCard(Zone.BATTLEFIELD, playerA, "Necropolis Fiend");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 3);
//
addCard(Zone.BATTLEFIELD, playerB, "Ancient Bronze Dragon"); // 7/7
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}, Exile");
setChoice(playerA, "X=2");
addTarget(playerA, "Ancient Bronze Dragon"); // to -2/-2
setChoice(playerA, "Grizzly Bears", 2);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPowerToughness(playerB, "Ancient Bronze Dragon", 7 - 2, 7 - 2);
}
@Test
public void test_prepareX_OpenTheWay() {
skipInitShuffling();
// X can't be greater than the number of players in the game.
// Reveal cards from the top of your library until you reveal X land cards.
// Put those land cards onto the battlefield tapped and the rest on the bottom of your library in a random order.
addCard(Zone.HAND, playerA, "Open the Way"); // {X}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 + 2);
addCard(Zone.LIBRARY, playerA, "Island", 5);
// min/max test require multiple tests (see above), so just disable setChoice and look at logs for good limits
// example: Message: Announce the value for {X} (any value)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Open the Way");
setChoice(playerA, "X=2");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Island", 2);
}
@Test
public void test_prepareX_KnollspineInvocation() {
skipInitShuffling();
// {X}, Discard a card with mana value X: This enchantment deals X damage to any target.
addCard(Zone.BATTLEFIELD, playerA, "Knollspine Invocation");
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
//
addCard(Zone.LIBRARY, playerA, "Grizzly Bears", 1); // {1}{G}
// turn 1 - can't play due empty hand
checkPlayableAbility("no cards to discard", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard", false);
// turn 3 - can't play due no mana
activateManaAbility(3, PhaseStep.UPKEEP, playerA, "{T}: Add {G}", 2);
checkPlayableAbility("no mana to activate", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard", false);
// turn 5 - can play
checkPlayableAbility("must able to activate", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard", true);
activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard");
setChoice(playerA, "Grizzly Bears"); // discard
addTarget(playerA, playerB); // damage
setStrictChooseMode(true);
setStopAt(5, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 2);
}
@Test
public void test_prepareX_EliteArcanist() {
// When Elite Arcanist enters the battlefield, you may exile an instant card from your hand.
// {X}, {T}: Copy the exiled card. You may cast the copy without paying its mana cost. X is the converted mana cost of the exiled card.
addCard(Zone.HAND, playerA, "Elite Arcanist"); // {3}{U}
addCard(Zone.BATTLEFIELD, playerA, "Island", 4 + 1);
addCard(Zone.HAND, playerA, "Lightning Bolt", 1);
// turn 1
// prepare arcanist
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Elite Arcanist");
setChoice(playerA, true); // use exile
setChoice(playerA, "Lightning Bolt"); // to exile and copy later
// turn 3
// cast copy
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}: Copy");
setChoice(playerA, true); // cast copy
addTarget(playerA, playerB); // damage
setStrictChooseMode(true);
setStopAt(3, PhaseStep.END_TURN);
execute();
assertTappedCount("Island", true, 1); // used to cast copy for {1}
assertLife(playerB, 20 - 3);
}
@Test
public void test_modifyCost_Fireball() {
// This spell costs {1} more to cast for each target beyond the first.
// Fireball deals X damage divided evenly, rounded down, among any number of targets.
addCard(Zone.HAND, playerA, "Fireball"); // {X}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 1); // 3 for x=2 cast, 1 for x2 targets
//
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 3); // 1/1
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fireball");
setChoice(playerA, "X=2");
addTarget(playerA, "Arbor Elf^Arbor Elf");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertTappedCount("Mountain", true, 3 + 1); // 3 for x=2 cast, 1 for x2 targets
assertGraveyardCount(playerA, "Arbor Elf", 2);
assertPermanentCount(playerA, "Arbor Elf", 1);
}
@Test
public void test_modifyCost_DeepwoodDenizen() {
// {5}{G}, {T}: Draw a card. This ability costs {1} less to activate for each +1/+1 counter on creatures you control.
addCard(Zone.BATTLEFIELD, playerA, "Deepwood Denizen");
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6 - 3);
//
// +1: Distribute three +1/+1 counters among one, two, or three target creatures you control
addCard(Zone.BATTLEFIELD, playerA, "Ajani, Mentor of Heroes", 1);
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1);
checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}, {T}: Draw", false);
// add +3 counters and get -3 cost decrease
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Distribute");
addTargetAmount(playerA, "Arbor Elf", 2);
addTargetAmount(playerA, "Deepwood Denizen", 1);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPlayableAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}, {T}: Draw", true);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}, {T}: Draw");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, 1); // +1 from draw ability
}
@Test
public void test_modifyCost_BaruWurmspeaker() {
// Wurms you control get +2/+2 and have trample.
// {7}{G}, {T}: Create a 4/4 green Wurm creature token. This ability costs {X} less to activate, where X is the greatest power among Wurms you control.
addCard(Zone.BATTLEFIELD, playerA, "Baru, Wurmspeaker");
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
//
// When this creature dies, create a 3/3 colorless Phyrexian Wurm artifact creature token with deathtouch
// and a 3/3 colorless Phyrexian Wurm artifact creature token with lifelink.
addCard(Zone.HAND, playerA, "Wurmcoil Engine", 1); // {6}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{7}{G}, {T}: Create", false);
// turn 1
// prepare wurm and get -8 cost decrease (6 wurm + 2 boost)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wurmcoil Engine");
// turn 3
waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN, playerA);
checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "{7}{G}, {T}: Create", true);
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{7}{G}, {T}: Create");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.END_TURN);
execute();
assertTokenCount(playerA, "Wurm Token", 1);
assertTappedCount("Forest", true, 1);
assertTappedCount("Mountain", true, 0);
}
@Test
public void test_Other_AbandonHope() {
// used both modify and cost reduction in one cost adjuster
// As an additional cost to cast this spell, discard X cards.
// Look at target opponent's hand and choose X cards from it. That player discards those cards.
addCard(Zone.HAND, playerA, "Abandon Hope"); // {X}{1}{B}
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2 + 2);
addCard(Zone.HAND, playerA, "Forest", 2);
addCard(Zone.HAND, playerB, "Grizzly Bears", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Abandon Hope");
setChoice(playerA, "X=2");
addTarget(playerA, playerB);
setChoice(playerA, "Forest^Forest"); // discard cost
setChoice(playerA, "Grizzly Bears^Grizzly Bears"); // discard from hand
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerA, "Forest", 2);
assertGraveyardCount(playerB, "Grizzly Bears", 2);
}
// additional tasks to improve code base:
// TODO: OsgirTheReconstructorCostAdjuster - migrate to EarlyTargetCost
// TODO: SkeletalScryingAdjuster - migrate to EarlyTargetCost
// TODO: NecropolisFiend - migrate to EarlyTargetCost
// TODO: KnollspineInvocation - migrate to EarlyTargetCost
// TODO: ExileCardsFromHandAdjuster - need rework to remove dialog from inside, e.g. migrate to EarlyTargetCost?
// TODO: CallerOfTheHuntAdjuster - research and add test
// TODO: VoodooDoll - research and add test
}

View file

@ -13,8 +13,12 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
*/
public class BeltOfGiantStrengthTest extends CardTestPlayerBase {
/**
* Equipped creature has base power and toughness 10/10.
* Equip {10}. This ability costs {X} less to activate where X is the power of the creature it targets.
*/
private static final String belt = "Belt of Giant Strength";
private static final String gigantosauras = "Gigantosaurus";
private static final String gigantosauras = "Gigantosaurus"; // 10/10
@Test
public void testWithManaAvailable() {

View file

@ -2927,6 +2927,7 @@ public class TestPlayer implements Player {
for (String choice : choices) {
if (choice.startsWith("X=")) {
int xValue = Integer.parseInt(choice.substring(2));
assertXMinMaxValue(game, ability, xValue, min, max);
choices.remove(choice);
return xValue;
}
@ -2934,7 +2935,7 @@ public class TestPlayer implements Player {
}
this.chooseStrictModeFailed("choice", game, getInfo(ability, game)
+ "\nMessage: " + message);
+ "\nMessage: " + message + prepareXMaxInfo(min, max));
return computerPlayer.announceXMana(min, max, message, game, ability);
}
@ -2944,16 +2945,34 @@ public class TestPlayer implements Player {
if (!choices.isEmpty()) {
if (choices.get(0).startsWith("X=")) {
int xValue = Integer.parseInt(choices.get(0).substring(2));
assertXMinMaxValue(game, ability, xValue, min, max);
choices.remove(0);
return xValue;
}
}
this.chooseStrictModeFailed("choice", game, getInfo(ability, game)
+ "\nMessage: " + message);
+ "\nMessage: " + message + prepareXMaxInfo(min, max));
return computerPlayer.announceXCost(min, max, message, game, ability, null);
}
private String prepareXMaxInfo(int min, int max) {
if (min == 0 && max == Integer.MAX_VALUE) {
return " (any value)";
} else {
return String.format(" (from %s to %s)",
min,
(max == Integer.MAX_VALUE) ? "any" : String.valueOf(max)
);
}
}
private void assertXMinMaxValue(Game game, Ability source, int xValue, int min, int max) {
if (xValue < min || xValue > max) {
Assert.fail("Found wrong X value = " + xValue + ", for " + CardUtil.getSourceName(game, source) + ", must" + prepareXMaxInfo(min, max));
}
}
@Override
public int getAmount(int min, int max, String message, Game game) {
assertAliasSupportInChoices(false);

View file

@ -4,6 +4,9 @@ import mage.MageObject;
import mage.Mana;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.ContinuousEffectsList;
import mage.abilities.effects.Effect;
import mage.cards.Card;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckCardLists;
@ -29,6 +32,7 @@ import mage.players.Player;
import mage.server.game.GameSessionPlayer;
import mage.util.CardUtil;
import mage.util.ThreadUtils;
import mage.utils.StreamUtils;
import mage.utils.SystemUtil;
import mage.view.GameView;
import org.junit.Assert;
@ -320,6 +324,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
}
assertAllCommandsUsed();
//assertNoDuplicatedEffects();
}
protected TestPlayer createNewPlayer(String playerName, RangeOfInfluence rangeOfInfluence) {
@ -1702,6 +1708,57 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
}
}
/**
* Make sure game state do not contain any duplicated effects, e.g. all effect's durations are fine
*/
private void assertNoDuplicatedEffects() {
// to find bugs like https://github.com/magefree/mage/issues/12932
// TODO: simulate full end turn to end all effects and make it default?
// some effects can generate duplicated effects by design
// example: Tamiyo, Inquisitive Student + x2 attack in TamiyoInquisitiveStudentTest.test_PlusTwo
// +2: Until your next turn, whenever a creature attacks you or a planeswalker you control, it gets -1/-0 until end of turn.
// TODO: add targets and affected objects check to unique key?
// one effect can be used multiple times by different sources
// so use group key like: effect + ability + source
Map<String, List<ContinuousEffect>> groups = new HashMap<>();
for (ContinuousEffectsList layer : currentGame.getState().getContinuousEffects().allEffectsLists) {
for (Object effectObj : layer) {
ContinuousEffect effect = (ContinuousEffect) effectObj;
for (Object abilityObj : layer.getAbility(effect.getId())) {
Ability ability = (Ability) abilityObj;
MageObject sourceObject = currentGame.getObject(ability.getSourceId());
String groupKey = "effectClass_" + effect.getClass().getCanonicalName()
+ "_abilityClass_" + ability.getClass().getCanonicalName()
+ "_sourceName_" + (sourceObject == null ? "null" : sourceObject.getIdName());
List<ContinuousEffect> groupList = groups.getOrDefault(groupKey, null);
if (groupList == null) {
groupList = new ArrayList<>();
groups.put(groupKey, groupList);
}
groupList.add(effect);
}
}
}
// analyse
List<String> duplicatedGroups = groups.keySet().stream()
.filter(groupKey -> groups.get(groupKey).size() > 1)
.collect(Collectors.toList());
if (duplicatedGroups.size() > 0) {
System.out.println("Duplicated effect groups: " + duplicatedGroups.size());
duplicatedGroups.forEach(groupKey -> {
System.out.println("group " + groupKey + ": ");
groups.get(groupKey).forEach(e -> {
System.out.println(" - " + e.getId() + " - " + e.getDuration() + " - " + e);
});
});
Assert.fail("Found duplicated effects: " + duplicatedGroups.size());
}
}
public void assertActivePlayer(TestPlayer player) {
Assert.assertEquals("message", currentGame.getState().getActivePlayerId(), player.getId());
}

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -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()) {

View file

@ -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) {

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

@ -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}.

View file

@ -72,7 +72,7 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost {
}
@Override
public ManaOptions getOptions() {
public final ManaOptions getOptions() {
return getOptions(true);
}

View file

@ -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++;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

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

View file

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