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

View file

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

View file

@ -1,43 +1,30 @@
package mage.cards.a; package mage.cards.a;
import mage.abilities.Ability; import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.common.DiscardTargetCost;
import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.discard.LookTargetHandChooseDiscardEffect; import mage.abilities.effects.common.discard.LookTargetHandChooseDiscardEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Zone;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.Game;
import mage.target.common.TargetCardInHand;
import mage.target.common.TargetOpponent; import mage.target.common.TargetOpponent;
import mage.util.CardUtil;
import java.util.UUID; import java.util.UUID;
/** /**
* @author fireshoes * @author fireshoes, JayDi85
*/ */
public final class AbandonHope extends CardImpl { public final class AbandonHope extends CardImpl {
public AbandonHope(UUID ownerId, CardSetInfo setInfo) { public AbandonHope(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{1}{B}"); super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{1}{B}");
// As an additional cost to cast Abandon Hope, discard X cards. // As an additional cost to cast this spell, discard X cards.
Ability ability = new SimpleStaticAbility( DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_CARDS);
Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X cards")
);
ability.setRuleAtTheTop(true);
this.addAbility(ability);
// Look at target opponent's hand and choose X cards from it. That player discards those 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().addEffect(new LookTargetHandChooseDiscardEffect(false, GetXValue.instance, StaticFilters.FILTER_CARD_CARDS));
this.getSpellAbility().addTarget(new TargetOpponent()); this.getSpellAbility().addTarget(new TargetOpponent());
this.getSpellAbility().setCostAdjuster(AbandonHopeAdjuster.instance);
} }
private AbandonHope(final AbandonHope card) { private AbandonHope(final AbandonHope card) {
@ -48,16 +35,4 @@ public final class AbandonHope extends CardImpl {
public AbandonHope copy() { public AbandonHope copy() {
return new AbandonHope(this); 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; package mage.cards.a;
import mage.abilities.Ability; import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.common.DiscardTargetCost;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.abilities.effects.common.ReturnToHandTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Zone;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.Game;
import mage.target.common.TargetCardInHand;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import mage.target.targetadjustment.XTargetsCountAdjuster; import mage.target.targetadjustment.XTargetsCountAdjuster;
import mage.util.CardUtil;
import java.util.UUID; import java.util.UUID;
/** /**
*
* @author jeffwadsworth * @author jeffwadsworth
*/ */
public final class AetherTide extends CardImpl { public final class AetherTide extends CardImpl {
@ -29,10 +20,8 @@ public final class AetherTide extends CardImpl {
public AetherTide(UUID ownerId, CardSetInfo setInfo) { public AetherTide(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{U}"); super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{U}");
// As an additional cost to cast Aether Tide, discard X creature cards. // As an additional cost to cast this spell, discard X creature cards.
Ability ability = new SimpleStaticAbility(Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X creature cards")); DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_CREATURES);
ability.setRuleAtTheTop(true);
this.addAbility(ability);
// Return X target creatures to their owners' hands. // Return X target creatures to their owners' hands.
Effect effect = new ReturnToHandTargetEffect(); Effect effect = new ReturnToHandTargetEffect();
@ -40,8 +29,6 @@ public final class AetherTide extends CardImpl {
this.getSpellAbility().addEffect(effect); this.getSpellAbility().addEffect(effect);
this.getSpellAbility().addTarget(new TargetCreaturePermanent()); this.getSpellAbility().addTarget(new TargetCreaturePermanent());
this.getSpellAbility().setTargetAdjuster(new XTargetsCountAdjuster()); this.getSpellAbility().setTargetAdjuster(new XTargetsCountAdjuster());
this.getSpellAbility().setCostAdjuster(AetherTideCostAdjuster.instance);
} }
private AetherTide(final AetherTide card) { private AetherTide(final AetherTide card) {
@ -53,15 +40,3 @@ public final class AetherTide extends CardImpl {
return new AetherTide(this); 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.common.SimpleActivatedAbility;
import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.ReplacementEffectImpl;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; 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. // {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 ability = new SimpleActivatedAbility(new AladdinsLampEffect(), new ManaCostsImpl<>("{X}"));
ability.addCost(new TapSourceCost()); ability.addCost(new TapSourceCost());
for (Object cost : ability.getManaCosts()) { ability.setVariableCostsMinMax(1, Integer.MAX_VALUE);
if (cost instanceof VariableManaCost) { // TODO: add costAdjuster for min/max values due library size
((VariableManaCost) cost).setMinX(1);
break;
}
}
this.addAbility(ability); this.addAbility(ability);
} }

View file

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

View file

@ -1,25 +1,28 @@
package mage.cards.b; package mage.cards.b;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.CostAdjuster; 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.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.InfoEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetOpponent; import mage.target.common.TargetOpponent;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.UUID;
/** /**
* * @author awjackson, JayDi85
* @author awjackson
*/ */
public final class BargainingTable extends CardImpl { public final class BargainingTable extends CardImpl {
@ -27,13 +30,10 @@ public final class BargainingTable extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{5}"); super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{5}");
// {X}, {T}: Draw a card. X is the number of cards in an opponent's hand. // {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.addCost(new TapSourceCost());
ability.addEffect(new InfoEffect("X is the number of cards in an opponent's hand")); 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. ability.setCostAdjuster(BargainingTableCostAdjuster.instance);
// 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);
this.addAbility(ability); 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; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void prepareX(Ability ability, Game game) {
int handSize = Integer.MAX_VALUE; // make sure early target used
if (game.inCheckPlayableState()) { BargainingTableXCost cost = ability.getManaCostsToPay().getVariableCosts().stream()
for (UUID playerId : CardUtil.getAllPossibleTargets(ability, game)) { .filter(c -> c instanceof BargainingTableXCost)
Player player = game.getPlayer(playerId); .map(c -> (BargainingTableXCost) c)
if (player != null) { .findFirst()
handSize = Math.min(handSize, player.getHand().size()); .orElse(null);
} if (cost == null) {
} throw new IllegalArgumentException("Wrong code usage: cost item lost");
} else { }
Player player = game.getPlayer(ability.getFirstTarget());
if (player != null) { if (game.inCheckPlayableState()) {
handSize = player.getHand().size(); // 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.abilities.keyword.TrampleAbility;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.*;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.filter.FilterPermanent; import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
@ -116,7 +113,7 @@ enum BaruWurmspeakerAdjuster implements CostAdjuster {
instance; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
int value = BaruWurmspeakerValue.instance.calculate(game, ability, null); int value = BaruWurmspeakerValue.instance.calculate(game, ability, null);
if (value > 0) { if (value > 0) {
CardUtil.reduceCost(ability, value); 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); private static final Hint hint = new ValueHint("Creature cards in your graveyard", xValue);
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, xValue.calculate(game, ability, null)); 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.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.CostAdjuster; import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.common.continuous.SetBasePowerToughnessEnchantedEffect; import mage.abilities.effects.common.continuous.SetBasePowerToughnessEnchantedEffect;
import mage.abilities.keyword.EquipAbility; import mage.abilities.keyword.EquipAbility;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType; import mage.constants.SubType;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.target.common.TargetControlledCreaturePermanent;
import mage.util.CardUtil; import mage.util.CardUtil;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import mage.abilities.costs.mana.GenericManaCost;
import mage.constants.Outcome;
import mage.target.common.TargetControlledCreaturePermanent;
/** /**
* @author TheElk801 * @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. // 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); 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.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); this.addAbility(ability);
} }
@ -49,33 +50,23 @@ public final class BeltOfGiantStrength extends CardImpl {
} }
} }
enum BeltOfGiantStrengthAdjuster implements CostAdjuster { enum BeltOfGiantStrengthCostAdjuster implements CostAdjuster {
instance; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
int power;
if (game.inCheckPlayableState()) { if (game.inCheckPlayableState()) {
int maxPower = 0; power = CardUtil.getAllPossibleTargets(ability, game).stream()
for (UUID permId : CardUtil.getAllPossibleTargets(ability, game)) { .map(game::getPermanent)
Permanent permanent = game.getPermanent(permId); .filter(Objects::nonNull)
if (permanent != null) { .mapToInt(p -> p.getPower().getValue())
int power = permanent.getPower().getValue(); .max().orElse(0);
if (power > maxPower) {
maxPower = power;
}
}
}
if (maxPower > 0) {
CardUtil.reduceCost(ability, maxPower);
}
} else { } else {
Permanent permanent = game.getPermanent(ability.getFirstTarget()); power = Optional.ofNullable(game.getPermanent(ability.getFirstTarget()))
if (permanent != null) { .map(p -> p.getPower().getValue())
int power = permanent.getPower().getValue(); .orElse(0);
if (power > 0) {
CardUtil.reduceCost(ability, power);
}
}
} }
CardUtil.reduceCost(ability, power);
} }
} }

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.costs.CostImpl;
import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; import mage.abilities.triggers.BeginningOfCombatTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.Cost; 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 filter = new FilterEquipmentPermanent("equipment attached to this creature");
private static final FilterPermanent subfilter = new FilterControlledPermanent("{this}"); 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}"); super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{U}{R}");
// As an additional cost to cast this spell, discard X cards. // 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. // Target player draws X cards. Channeled Force deals X damage to up to one target creature or planeswalker.
this.getSpellAbility().addEffect(new ChanneledForceEffect()); this.getSpellAbility().addEffect(new ChanneledForceEffect());

View file

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

View file

@ -62,7 +62,7 @@ enum DeepwoodDenizenAdjuster implements CostAdjuster {
instance; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, DeepwoodDenizenValue.instance.calculate(game, ability, null)); 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.VariableCostImpl;
import mage.abilities.costs.VariableCostType; import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.common.DiscardTargetCost; 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.dynamicvalue.common.GetXValue;
import mage.abilities.effects.common.DamageAllEffect; import mage.abilities.effects.common.DamageAllEffect;
import mage.abilities.effects.common.SacrificeAllEffect; import mage.abilities.effects.common.SacrificeAllEffect;
@ -12,6 +14,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.filter.FilterCard; import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledLandPermanent; import mage.filter.common.FilterControlledLandPermanent;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
import mage.game.Game; import mage.game.Game;
@ -28,8 +31,8 @@ public final class DevastatingDreams extends CardImpl {
public DevastatingDreams(UUID ownerId, CardSetInfo setInfo) { public DevastatingDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{R}{R}"); super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{R}{R}");
// As an additional cost to cast Devastating Dreams, discard X cards at random. // As an additional cost to cast this spell, discard X cards at random.
this.getSpellAbility().addCost(new DevastatingDreamsAdditionalCost()); this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true).withRandom());
// Each player sacrifices X lands. // Each player sacrifices X lands.
this.getSpellAbility().addEffect(new SacrificeAllEffect(GetXValue.instance, new FilterControlledLandPermanent("lands"))); this.getSpellAbility().addEffect(new SacrificeAllEffect(GetXValue.instance, new FilterControlledLandPermanent("lands")));

View file

@ -1,12 +1,13 @@
package mage.cards.e; package mage.cards.e;
import mage.ApprovingObject;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.CostAdjuster; import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.common.TapSourceCost; 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.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.cards.Card; import mage.cards.Card;
@ -23,7 +24,6 @@ import mage.players.Player;
import mage.target.TargetCard; import mage.target.TargetCard;
import java.util.UUID; import java.util.UUID;
import mage.ApprovingObject;
/** /**
* @author LevelX2 * @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. // {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 ability = new SimpleActivatedAbility(new EliteArcanistCopyEffect(), new ManaCostsImpl<>("{X}"));
ability.addCost(new TapSourceCost()); ability.addCost(new TapSourceCost());
ability.setCostAdjuster(EliteArcanistAdjuster.instance); ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance);
this.addAbility(ability); 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 { class EliteArcanistImprintEffect extends OneShotEffect {
private static final FilterCard filter = new FilterCard("instant card from your hand"); private static final FilterCard filter = new FilterCard("instant card from your hand");

View file

@ -64,7 +64,7 @@ enum EsquireOfTheKingAdjuster implements CostAdjuster {
instance; instance;
@Override @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)) { if (game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_CREATURE_LEGENDARY, ability, game, 1)) {
CardUtil.reduceCost(ability, 2); CardUtil.reduceCost(ability, 2);
} }

View file

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

View file

@ -2,11 +2,11 @@ package mage.cards.f;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.costs.CostAdjuster; import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.CostModificationType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
@ -24,8 +24,8 @@ public final class Fireball extends CardImpl {
public Fireball(UUID ownerId, CardSetInfo setInfo) { public Fireball(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{R}"); 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. // This spell costs {1} more to cast for each target beyond the first.
// Fireball 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().addTarget(new FireballTargetCreatureOrPlayer(0, Integer.MAX_VALUE));
this.getSpellAbility().addEffect(new FireballEffect()); this.getSpellAbility().addEffect(new FireballEffect());
this.getSpellAbility().setCostAdjuster(FireballAdjuster.instance); this.getSpellAbility().setCostAdjuster(FireballAdjuster.instance);
@ -45,10 +45,10 @@ enum FireballAdjuster implements CostAdjuster {
instance; instance;
@Override @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(); int numTargets = ability.getTargets().isEmpty() ? 0 : ability.getTargets().get(0).getTargets().size();
if (numTargets > 1) { 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.CardType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.filter.FilterCard; import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
@ -25,8 +26,8 @@ public final class Firestorm extends CardImpl {
public Firestorm(UUID ownerId, CardSetInfo setInfo) { public Firestorm(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}"); super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}");
// As an additional cost to cast Firestorm, discard X cards. // As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Firestorm deals X damage to each of X target creatures and/or players. // Firestorm deals X damage to each of X target creatures and/or players.
this.getSpellAbility().addEffect(new FirestormEffect()); this.getSpellAbility().addEffect(new FirestormEffect());

View file

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

View file

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

View file

@ -74,7 +74,7 @@ enum GrimGiganotosaurusAdjuster implements CostAdjuster {
private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(filter); private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(filter);
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId()); Player controller = game.getPlayer(ability.getControllerId());
if (controller != null) { if (controller != null) {
CardUtil.reduceCost(ability, xValue.calculate(game, ability, 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(); private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost();
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
if (BargainedCondition.instance.apply(game, ability) if (BargainedCondition.instance.apply(game, ability)
|| (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) {
CardUtil.reduceCost(ability, 2); CardUtil.reduceCost(ability, 2);

View file

@ -2,9 +2,8 @@ package mage.cards.h;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.common.TapSourceCost; 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.dynamicvalue.common.GetXValue;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.cards.*; import mage.cards.*;
@ -29,9 +28,8 @@ public final class HelmOfObedience extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}"); 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. // {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); Ability ability = new SimpleActivatedAbility(new HelmOfObedienceEffect(), new ManaCostsImpl<>("{X}"));
xCosts.setMinX(1); ability.setVariableCostsMinMax(1, Integer.MAX_VALUE);
Ability ability = new SimpleActivatedAbility(new HelmOfObedienceEffect(), xCosts);
ability.addCost(new TapSourceCost()); ability.addCost(new TapSourceCost());
ability.addTarget(new TargetOpponent()); ability.addTarget(new TargetOpponent());
this.addAbility(ability); this.addAbility(ability);

View file

@ -117,6 +117,10 @@ final class HinataDawnCrownedEffectUtility
public static int getTargetCount(Game game, Ability abilityToModify) public static int getTargetCount(Game game, Ability abilityToModify)
{ {
if (game.inCheckPlayableState()) { 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); Optional<Integer> max = abilityToModify.getTargets().stream().map(x -> x.getMaxNumberOfTargets()).max(Integer::compare);
int allPossibleSize = CardUtil.getAllPossibleTargets(abilityToModify, game).size(); int allPossibleSize = CardUtil.getAllPossibleTargets(abilityToModify, game).size();
return max.isPresent() ? return max.isPresent() ?

View file

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

View file

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

View file

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

View file

@ -25,7 +25,7 @@ public final class InsidiousDreams extends CardImpl {
public InsidiousDreams(UUID ownerId, CardSetInfo setInfo) { public InsidiousDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{B}"); 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)); 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. // 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(); private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost();
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
if (BargainedCondition.instance.apply(game, ability) if (BargainedCondition.instance.apply(game, ability)
|| (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) {
CardUtil.reduceCost(ability, 2); CardUtil.reduceCost(ability, 2);

View file

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

View file

@ -1,40 +1,43 @@
package mage.cards.k; package mage.cards.k;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
import mage.abilities.costs.CostAdjuster; 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.ManaCostsImpl;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.effects.common.DamageTargetEffect;
import mage.cards.Card;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.ComparisonType; import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.FilterCard; import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.ManaValuePredicate;
import mage.game.Game; import mage.game.Game;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetAnyTarget; import mage.target.common.TargetAnyTarget;
import mage.target.common.TargetCardInHand; import mage.target.common.TargetCardInHand;
import mage.util.CardUtil;
import java.util.UUID; import java.util.UUID;
/** /**
* @author anonymous * @author JayDi85
*/ */
public final class KnollspineInvocation extends CardImpl { 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) { public KnollspineInvocation(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}{R}"); 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 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.addTarget(new TargetAnyTarget());
ability.setCostAdjuster(KnollspineInvocationAdjuster.instance); ability.setCostAdjuster(KnollspineInvocationAdjuster.instance);
this.addAbility(ability); 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 { enum KnollspineInvocationAdjuster implements CostAdjuster {
instance; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void prepareX(Ability ability, Game game) {
int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); Player controller = game.getPlayer(ability.getControllerId());
for (Cost cost : ability.getCosts()) { if (controller == null) {
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));
return; 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; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void increaseCost(Ability ability, Game game) {
Player player = game.getPlayer(ability.getControllerId()); Player player = game.getPlayer(ability.getControllerId());
if (player != null) { if (player != null) {
CardUtil.increaseCost(ability, player.getHand().size()); CardUtil.increaseCost(ability, player.getHand().size());

View file

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

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.filter.FilterCard; import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.target.common.TargetCreatureOrPlaneswalker; import mage.target.common.TargetCreatureOrPlaneswalker;
import mage.target.targetadjustment.XTargetsCountAdjuster; import mage.target.targetadjustment.XTargetsCountAdjuster;
@ -21,8 +22,8 @@ public final class NahirisWrath extends CardImpl {
public NahirisWrath(UUID ownerId, CardSetInfo setInfo) { public NahirisWrath(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{R}"); super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{R}");
// As an additional cost to cast Nahiri's Wrath, discard X cards. // As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); 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. // 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); Effect effect = new DamageTargetEffect(DiscardCostCardManaValue.instance);

View file

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

View file

@ -63,7 +63,7 @@ enum NemesisOfMortalsAdjuster implements CostAdjuster {
instance; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
Player controller = game.getPlayer(ability.getControllerId()); Player controller = game.getPlayer(ability.getControllerId());
if (controller != null) { if (controller != null) {
CardUtil.reduceCost(ability, controller.getGraveyard().count(StaticFilters.FILTER_CARD_CREATURE, game)); 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) { public NostalgicDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{G}{G}"); super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{G}{G}");
// As an additional cost to cast Nostalgic Dreams, discard X cards. // As an additional cost to cast this spell, discard X cards.
this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Return X target cards from your graveyard to your hand. // Return X target cards from your graveyard to your hand.
Effect effect = new ReturnFromGraveyardToHandTargetEffect(); Effect effect = new ReturnFromGraveyardToHandTargetEffect();

View file

@ -3,7 +3,6 @@ package mage.cards.o;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.CostAdjuster; import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.InfoEffect;
import mage.cards.*; import mage.cards.*;
@ -28,7 +27,7 @@ public final class OpenTheWay extends CardImpl {
this.addAbility(new SimpleStaticAbility( this.addAbility(new SimpleStaticAbility(
Zone.ALL, new InfoEffect("X can't be greater than the number of players in the game") Zone.ALL, new InfoEffect("X can't be greater than the number of players in the game")
).setRuleAtTheTop(true)); ).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. // 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()); this.getSpellAbility().addEffect(new OpenTheWayEffect());
@ -44,14 +43,12 @@ public final class OpenTheWay extends CardImpl {
} }
} }
enum OpenTheWayAdjuster implements CostAdjuster { enum OpenTheWayCostAdjuster implements CostAdjuster {
instance; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void prepareX(Ability ability, Game game) {
int playerCount = game.getPlayers().size(); ability.setVariableCostsMinMax(0, game.getState().getPlayersInRange(ability.getControllerId(), game, true).size());
CardUtil.castStream(ability.getCosts().stream(), VariableManaCost.class)
.forEach(cost -> cost.setMaxX(playerCount));
} }
} }

View file

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

View file

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

View file

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

View file

@ -4,11 +4,8 @@ import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility; 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.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.cards.Card; import mage.cards.Card;
@ -42,10 +39,10 @@ public final class PrototypePortal extends CardImpl {
.setAbilityWord(AbilityWord.IMPRINT) .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 ability = new SimpleActivatedAbility(new PrototypePortalCreateTokenEffect(), new ManaCostsImpl<>("{X}"));
ability.addCost(new TapSourceCost()); ability.addCost(new TapSourceCost());
ability.setCostAdjuster(PrototypePortalAdjuster.instance); ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance);
this.addAbility(ability); 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 { class PrototypePortalEffect extends OneShotEffect {
PrototypePortalEffect() { PrototypePortalEffect() {

View file

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

View file

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

View file

@ -69,7 +69,7 @@ enum RazorlashTransmograntAdjuster implements CostAdjuster {
} }
@Override @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)) { if (makeMap(game, ability).values().stream().anyMatch(x -> x >= 4)) {
CardUtil.reduceCost(ability, 4); CardUtil.reduceCost(ability, 4);
} }

View file

@ -20,7 +20,7 @@ public final class RestlessDreams extends CardImpl {
public RestlessDreams(UUID ownerId, CardSetInfo setInfo) { public RestlessDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{B}"); 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)); this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Return X target creature cards from your graveyard to your hand. // Return X target creature cards from your graveyard to your hand.

View file

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

View file

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

View file

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

View file

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

View file

@ -61,7 +61,7 @@ enum SkeletalScryingAdjuster implements CostAdjuster {
instance; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void prepareCost(Ability ability, Game game) {
int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0);
if (xValue > 0) { if (xValue > 0) {
ability.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard(xValue, xValue, StaticFilters.FILTER_CARDS_FROM_YOUR_GRAVEYARD))); 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.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility; 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.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenCopyTargetEffect; 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. // {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 ability = new SimpleActivatedAbility(new SoulFoundryEffect(), new ManaCostsImpl<>("{X}"));
ability.addCost(new TapSourceCost()); ability.addCost(new TapSourceCost());
ability.setCostAdjuster(SoulFoundryAdjuster.instance); ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance);
this.addAbility(ability); 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 { class SoulFoundryImprintEffect extends OneShotEffect {
private static final FilterCard filter = new FilterCard("creature card from your hand"); private static final FilterCard filter = new FilterCard("creature card from your hand");

View file

@ -61,7 +61,7 @@ enum TamiyosLogbookAdjuster implements CostAdjuster {
); );
@Override @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); int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game);
CardUtil.reduceCost(ability, count); CardUtil.reduceCost(ability, count);
} }

View file

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

View file

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

View file

@ -20,7 +20,7 @@ public final class TurbulentDreams extends CardImpl {
public TurbulentDreams(UUID ownerId, CardSetInfo setInfo) { public TurbulentDreams(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{U}{U}"); 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)); this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true));
// Return X target nonland permanents to their owners' hands. // Return X target nonland permanents to their owners' hands.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -101,7 +101,7 @@ enum WaytaTrainerProdigyAdjuster implements CostAdjuster {
instance; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
if (game.inCheckPlayableState()) { if (game.inCheckPlayableState()) {
int controllerTargets = 0; //number of possible targets controlled by the ability's controller int controllerTargets = 0; //number of possible targets controlled by the ability's controller
for (UUID permId : CardUtil.getAllPossibleTargets(ability, game)) { 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 { 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 belt = "Belt of Giant Strength";
private static final String gigantosauras = "Gigantosaurus"; private static final String gigantosauras = "Gigantosaurus"; // 10/10
@Test @Test
public void testWithManaAvailable() { public void testWithManaAvailable() {

View file

@ -2927,6 +2927,7 @@ public class TestPlayer implements Player {
for (String choice : choices) { for (String choice : choices) {
if (choice.startsWith("X=")) { if (choice.startsWith("X=")) {
int xValue = Integer.parseInt(choice.substring(2)); int xValue = Integer.parseInt(choice.substring(2));
assertXMinMaxValue(game, ability, xValue, min, max);
choices.remove(choice); choices.remove(choice);
return xValue; return xValue;
} }
@ -2934,7 +2935,7 @@ public class TestPlayer implements Player {
} }
this.chooseStrictModeFailed("choice", game, getInfo(ability, game) this.chooseStrictModeFailed("choice", game, getInfo(ability, game)
+ "\nMessage: " + message); + "\nMessage: " + message + prepareXMaxInfo(min, max));
return computerPlayer.announceXMana(min, max, message, game, ability); return computerPlayer.announceXMana(min, max, message, game, ability);
} }
@ -2944,16 +2945,34 @@ public class TestPlayer implements Player {
if (!choices.isEmpty()) { if (!choices.isEmpty()) {
if (choices.get(0).startsWith("X=")) { if (choices.get(0).startsWith("X=")) {
int xValue = Integer.parseInt(choices.get(0).substring(2)); int xValue = Integer.parseInt(choices.get(0).substring(2));
assertXMinMaxValue(game, ability, xValue, min, max);
choices.remove(0); choices.remove(0);
return xValue; return xValue;
} }
} }
this.chooseStrictModeFailed("choice", game, getInfo(ability, game) this.chooseStrictModeFailed("choice", game, getInfo(ability, game)
+ "\nMessage: " + message); + "\nMessage: " + message + prepareXMaxInfo(min, max));
return computerPlayer.announceXCost(min, max, message, game, ability, null); 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 @Override
public int getAmount(int min, int max, String message, Game game) { public int getAmount(int min, int max, String message, Game game) {
assertAliasSupportInChoices(false); assertAliasSupportInChoices(false);

View file

@ -4,6 +4,9 @@ import mage.MageObject;
import mage.Mana; import mage.Mana;
import mage.ObjectColor; import mage.ObjectColor;
import mage.abilities.Ability; 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.Card;
import mage.cards.decks.Deck; import mage.cards.decks.Deck;
import mage.cards.decks.DeckCardLists; import mage.cards.decks.DeckCardLists;
@ -29,6 +32,7 @@ import mage.players.Player;
import mage.server.game.GameSessionPlayer; import mage.server.game.GameSessionPlayer;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.utils.StreamUtils;
import mage.utils.SystemUtil; import mage.utils.SystemUtil;
import mage.view.GameView; import mage.view.GameView;
import org.junit.Assert; import org.junit.Assert;
@ -320,6 +324,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
} }
assertAllCommandsUsed(); assertAllCommandsUsed();
//assertNoDuplicatedEffects();
} }
protected TestPlayer createNewPlayer(String playerName, RangeOfInfluence rangeOfInfluence) { 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) { public void assertActivePlayer(TestPlayer player) {
Assert.assertEquals("message", currentGame.getState().getActivePlayerId(), player.getId()); 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 * Gets all {@link ManaCosts} associated with this ability. These returned
* costs should never be modified as they represent the base costs before * 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. * @return All {@link ManaCosts} that must be paid.
*/ */
@ -151,6 +151,17 @@ public interface Ability extends Controllable, Serializable {
void addManaCostsToPay(ManaCost manaCost); 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. * 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. * 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) * - 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 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); boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event);
@ -533,11 +544,25 @@ public interface Ability extends Controllable, Serializable {
void adjustTargets(Game game); void adjustTargets(Game game);
/**
* Dynamic X and cost modification, see CostAdjuster for more details on usage
*/
Ability setCostAdjuster(CostAdjuster costAdjuster); 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(); 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) // 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 // 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); boolean isMainPartAbility = !CardUtil.isFusedPartAbility(this, game);
/* 20220908 - 601.2b /* 20220908 - 601.2b
@ -326,6 +328,16 @@ public abstract class AbilityImpl implements Ability {
return false; 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 // 20121001 - 601.2b
// If the spell has a variable cost that will be paid as it's being cast (such as an {X} in // 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. // 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. // Note: ActivatedAbility does include SpellAbility & PlayLandAbility, but those should be able to be canceled too.
boolean canCancel = this instanceof ActivatedAbility && controller.isHuman(); boolean canCancel = this instanceof ActivatedAbility && controller.isHuman();
if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, canCancel)) { if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, canCancel)) {
// was canceled during targer selection // was canceled during target selection
return false; return false;
} }
} }
@ -428,7 +440,7 @@ public abstract class AbilityImpl implements Ability {
//20101001 - 601.2e //20101001 - 601.2e
if (isMainPartAbility) { 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); game.getContinuousEffects().costModification(this, game);
} }
@ -726,6 +738,11 @@ public abstract class AbilityImpl implements Ability {
((EarlyTargetCost) cost).chooseTarget(game, this, controller); ((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 if (!variableManaCost.isPaid()) { // should only happen for human players
int xValue; int xValue;
if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) { if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) {
xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(), if (variableManaCost.wasAnnounced()) {
"Announce the value for " + variableManaCost.getText(), game, this); // 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(); int amountMana = xValue * variableManaCost.getXInstancesCount();
StringBuilder manaString = threadLocalBuilder.get(); StringBuilder manaString = threadLocalBuilder.get();
if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) { 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 @Override
public void addEffect(Effect effect) { public void addEffect(Effect effect) {
if (effect != null) { 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 @Override
public AbilityImpl setCostAdjuster(CostAdjuster costAdjuster) { public AbilityImpl setCostAdjuster(CostAdjuster costAdjuster) {
this.costAdjuster = costAdjuster; this.costAdjuster = costAdjuster;
@ -1694,14 +1761,23 @@ public abstract class AbilityImpl implements Ability {
} }
@Override @Override
public CostAdjuster getCostAdjuster() { public void adjustX(Game game) {
return costAdjuster; if (costAdjuster != null) {
costAdjuster.prepareX(this, game);
}
} }
@Override @Override
public void adjustCosts(Game game) { public void adjustCostsPrepare(Game game) {
if (costAdjuster != null) { 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; package mage.abilities.costs;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.constants.CostModificationType;
import mage.game.Game; import mage.game.Game;
import java.io.Serializable; 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 { public interface CostAdjuster extends Serializable {
/** /**
* Must check playable and real cast states. * Prepare {X} costs settings or define auto-announced mana values
* Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState): * <p>
* 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes) * Usage example:
* 2. In real cast state it must check current use case (e.g. real selected targets and modes) * - define auto-announced mana value {X} by ability.setVariableCostsValue
* * - define possible {X} settings by ability.setVariableCostsMinMax
* @param ability
* @param game
*/ */
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 * @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() { void chooseTarget(Game game, Ability source, Player controller);
super();
}
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; 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 * @author LevelX2
*/ */
public class DiscardXTargetCost extends VariableCostImpl { public class DiscardXTargetCost extends VariableCostImpl {
protected FilterCard filter; protected FilterCard filter;
protected boolean isRandom = false;
public DiscardXTargetCost(FilterCard filter) { public DiscardXTargetCost(FilterCard filter) {
this(filter, false); this(filter, false);
@ -30,6 +39,12 @@ public class DiscardXTargetCost extends VariableCostImpl {
protected DiscardXTargetCost(final DiscardXTargetCost cost) { protected DiscardXTargetCost(final DiscardXTargetCost cost) {
super(cost); super(cost);
this.filter = cost.filter; this.filter = cost.filter;
this.isRandom = cost.isRandom;
}
public DiscardXTargetCost withRandom() {
this.isRandom = true;
return this;
} }
@Override @Override
@ -49,6 +64,6 @@ public class DiscardXTargetCost extends VariableCostImpl {
@Override @Override
public Cost getFixedCostsFromAnnouncedValue(int xValue) { public Cost getFixedCostsFromAnnouncedValue(int xValue) {
TargetCardInHand target = new TargetCardInHand(xValue, filter); 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; int loyaltyCost = amount;
// apply cost modification // apply dynamic costs and cost modification
if (ability instanceof LoyaltyAbility) { if (ability instanceof LoyaltyAbility) {
LoyaltyAbility copiedAbility = ((LoyaltyAbility) ability).copy(); LoyaltyAbility copiedAbility = ((LoyaltyAbility) ability).copy();
copiedAbility.adjustCosts(game); copiedAbility.adjustX(game);
game.getContinuousEffects().costModification(copiedAbility, game); game.getContinuousEffects().costModification(copiedAbility, game);
loyaltyCost = 0; loyaltyCost = 0;
for (Cost cost : copiedAbility.getCosts()) { for (Cost cost : copiedAbility.getCosts()) {

View file

@ -59,10 +59,10 @@ public class PayVariableLoyaltyCost extends VariableCostImpl {
int maxValue = permanent.getCounters(game).getCount(CounterType.LOYALTY); int maxValue = permanent.getCounters(game).getCount(CounterType.LOYALTY);
// apply cost modification // apply dynamic costs and cost modification
if (source instanceof LoyaltyAbility) { if (source instanceof LoyaltyAbility) {
LoyaltyAbility copiedAbility = ((LoyaltyAbility) source).copy(); LoyaltyAbility copiedAbility = ((LoyaltyAbility) source).copy();
copiedAbility.adjustCosts(game); copiedAbility.adjustX(game);
game.getContinuousEffects().costModification(copiedAbility, game); game.getContinuousEffects().costModification(copiedAbility, game);
for (Cost cost : copiedAbility.getCosts()) { for (Cost cost : copiedAbility.getCosts()) {
if (cost instanceof PayVariableLoyaltyCost) { if (cost instanceof PayVariableLoyaltyCost) {

View file

@ -13,7 +13,7 @@ public enum CommanderManaValueAdjuster implements CostAdjuster {
instance; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, GreatestCommanderManaValue.instance.calculate(game, ability, null)); 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; instance;
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
CardUtil.reduceCost(ability, DomainValue.REGULAR.calculate(game, ability, null)); CardUtil.reduceCost(ability, DomainValue.REGULAR.calculate(game, ability, null));
} }
} }

View file

@ -25,22 +25,29 @@ public class ExileCardsFromHandAdjuster implements CostAdjuster {
} }
@Override @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
if (game.inCheckPlayableState()) {
return;
}
Player player = game.getPlayer(ability.getControllerId()); Player player = game.getPlayer(ability.getControllerId());
if (player == null) { if (player == null) {
return; return;
} }
int cardCount = player.getHand().count(filter, game); int cardCount = player.getHand().count(filter, game);
int toExile = cardCount > 0 ? player.getAmount( int reduceCount;
0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game if (game.inCheckPlayableState()) {
) : 0; // possible
if (toExile > 0) { reduceCount = 2 * cardCount;
ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter))); } else {
CardUtil.reduceCost(ability, 2 * toExile); // 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) { 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 @Override
public void adjustCosts(Ability ability, Game game) { public void reduceCost(Ability ability, Game game) {
int count = game.getBattlefield().count( int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game);
filter, ability.getControllerId(), ability, game
);
if (count > 0) { if (count > 0) {
CardUtil.reduceCost(ability, count); CardUtil.reduceCost(ability, count);
} }

View file

@ -32,7 +32,7 @@ public interface ManaCost extends Cost {
ManaOptions getOptions(); 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. * Used to correctly highlight (or not) spells with Phyrexian mana depending on if the player can pay life costs.
* <p> * <p>
* E.g. Tezzeret's Gambit has a cost of {3}{U/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 @Override
public ManaOptions getOptions() { public final ManaOptions getOptions() {
return getOptions(true); 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)))); this.add(new ColoredManaCost(ColoredManaSymbol.lookup(symbol.charAt(0))));
} else // check X wasn't added before } else // check X wasn't added before
if (modifierForX == 0) { if (modifierForX == 0) {
// count X occurence // count X occurrence
for (String s : symbols) { for (String s : symbols) {
if (s.equals("X")) { if (s.equals("X")) {
modifierForX++; modifierForX++;

View file

@ -3,17 +3,19 @@ package mage.abilities.costs.mana;
import mage.Mana; import mage.Mana;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCost; import mage.abilities.costs.MinMaxVariableCost;
import mage.abilities.costs.VariableCostType; import mage.abilities.costs.VariableCostType;
import mage.abilities.mana.ManaOptions;
import mage.constants.ColoredManaSymbol; import mage.constants.ColoredManaSymbol;
import mage.filter.FilterMana; import mage.filter.FilterMana;
import mage.game.Game; import mage.game.Game;
import mage.players.ManaPool; import mage.players.ManaPool;
import mage.util.CardUtil;
/** /**
* @author BetaSteward_at_googlemail.com, JayDi85 * @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: // 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) // 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 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 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 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 FilterMana filter; // mana filter that can be used for that cost
protected int minX = 0; protected int minX = 0;
@ -59,6 +61,18 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
return 0; 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 @Override
public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) { public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) {
// X mana cost always pays as generic mana // X mana cost always pays as generic mana
@ -86,6 +100,10 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
return this.isColorlessPaid(xPay); return this.isColorlessPaid(xPay);
} }
public boolean wasAnnounced() {
return this.wasAnnounced;
}
@Override @Override
public VariableManaCost getUnpaid() { public VariableManaCost getUnpaid() {
return this; return this;
@ -122,18 +140,22 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
return this.xInstancesCount; return this.xInstancesCount;
} }
@Override
public int getMinX() { public int getMinX() {
return minX; return minX;
} }
@Override
public void setMinX(int minX) { public void setMinX(int minX) {
this.minX = minX; this.minX = minX;
} }
@Override
public int getMaxX() { public int getMaxX() {
return maxX; return maxX;
} }
@Override
public void setMaxX(int maxX) { public void setMaxX(int maxX) {
this.maxX = maxX; this.maxX = maxX;
} }

View file

@ -3,11 +3,27 @@ package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect; 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.game.Game;
import mage.players.Player; import mage.players.Player;
public enum CardsInControllerHandCount implements DynamicValue { 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 @Override
public int calculate(Game game, Ability sourceAbility, Effect effect) { public int calculate(Game game, Ability sourceAbility, Effect effect) {
@ -22,16 +38,20 @@ public enum CardsInControllerHandCount implements DynamicValue {
@Override @Override
public CardsInControllerHandCount copy() { public CardsInControllerHandCount copy() {
return CardsInControllerHandCount.instance; return this;
} }
@Override @Override
public String getMessage() { public String getMessage() {
return "cards in your hand"; return this.filter.getMessage() + " in your hand";
} }
@Override @Override
public String toString() { public String toString() {
return "1"; return "1";
} }
public Hint getHint() {
return this.hint;
}
} }

View file

@ -688,6 +688,8 @@ public class ContinuousEffects implements Serializable {
* the battlefield for * the battlefield for
* {@link CostModificationEffect cost modification effects} and applies them * {@link CostModificationEffect cost modification effects} and applies them
* if necessary. * if necessary.
* <p>
* Warning, don't forget to call ability.adjustX before any cost modifications
* *
* @param abilityToModify * @param abilityToModify
* @param game * @param game
@ -695,6 +697,10 @@ public class ContinuousEffects implements Serializable {
public void costModification(Ability abilityToModify, Game game) { public void costModification(Ability abilityToModify, Game game) {
List<CostModificationEffect> costEffects = getApplicableCostModificationEffects(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) { for (CostModificationEffect effect : costEffects) {
if (effect.getModificationType() == CostModificationType.INCREASE_COST) { if (effect.getModificationType() == CostModificationType.INCREASE_COST) {
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId()); 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) { for (CostModificationEffect effect : costEffects) {
if (effect.getModificationType() == CostModificationType.REDUCE_COST) { if (effect.getModificationType() == CostModificationType.REDUCE_COST) {
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId()); 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) { for (CostModificationEffect effect : costEffects) {
if (effect.getModificationType() == CostModificationType.SET_COST) { if (effect.getModificationType() == CostModificationType.SET_COST) {
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId()); 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); TargetCard target = new TargetCardInHand(upTo ? 0 : num, num, filter);
if (controller.choose(Outcome.Discard, player.getHand(), target, source, game)) { 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); player.discard(new CardsImpl(target.getTargets()), false, source, game);
} }
return true; return true;

View file

@ -132,6 +132,8 @@ public class SuspendAbility extends SpecialAction {
this.addEffect(new SuspendExileEffect(suspend)); this.addEffect(new SuspendExileEffect(suspend));
this.usesStack = false; this.usesStack = false;
if (suspend == Integer.MAX_VALUE) { 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); VariableManaCost xCosts = new VariableManaCost(VariableCostType.ALTERNATIVE);
xCosts.setMinX(1); xCosts.setMinX(1);
this.addCost(xCosts); this.addCost(xCosts);

View file

@ -426,6 +426,16 @@ public class StackAbility extends StackObjectImpl implements Ability {
// Do nothing // Do nothing
} }
@Override
public void setVariableCostsMinMax(int min, int max) {
ability.setVariableCostsMinMax(min, max);
}
@Override
public void setVariableCostsValue(int xValue) {
ability.setVariableCostsValue(xValue);
}
@Override @Override
public Map<String, Object> getCostsTagMap() { public Map<String, Object> getCostsTagMap() {
return ability.getCostsTagMap(); return ability.getCostsTagMap();
@ -778,14 +788,23 @@ public class StackAbility extends StackObjectImpl implements Ability {
} }
@Override @Override
public CostAdjuster getCostAdjuster() { public void adjustX(Game game) {
return costAdjuster; if (costAdjuster != null) {
costAdjuster.prepareX(this, game);
}
} }
@Override @Override
public void adjustCosts(Game game) { public void adjustCostsPrepare(Game game) {
if (costAdjuster != null) { 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); 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); 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); 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 // 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()) { if (!copy.canActivate(playerId, game).canActivate()) {
return false; return false;
} }
// apply dynamic costs and cost modification
copy.adjustX(game);
if (availableMana != null) { if (availableMana != null) {
copy.adjustCosts(game); // TODO: need research, why it look at availableMana here - can delete condition?
game.getContinuousEffects().costModification(copy, game); game.getContinuousEffects().costModification(copy, game);
} }
boolean canBeCastRegularly = true; boolean canBeCastRegularly = true;
@ -3891,7 +3894,8 @@ public abstract class PlayerImpl implements Player, Serializable {
copyAbility = ability.copy(); copyAbility = ability.copy();
copyAbility.clearManaCostsToPay(); copyAbility.clearManaCostsToPay();
copyAbility.addManaCostsToPay(manaCosts.copy()); copyAbility.addManaCostsToPay(manaCosts.copy());
copyAbility.adjustCosts(game); // apply dynamic costs and cost modification
copyAbility.adjustX(game);
game.getContinuousEffects().costModification(copyAbility, game); game.getContinuousEffects().costModification(copyAbility, game);
// reduced all cost // reduced all cost
@ -3963,12 +3967,9 @@ public abstract class PlayerImpl implements Player, Serializable {
// alternative cost reduce // alternative cost reduce
copyAbility = ability.copy(); copyAbility = ability.copy();
copyAbility.clearManaCostsToPay(); 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.addManaCostsToPay(manaCosts.copy());
copyAbility.adjustCosts(game); // apply dynamic costs and cost modification
copyAbility.adjustX(game);
game.getContinuousEffects().costModification(copyAbility, game); game.getContinuousEffects().costModification(copyAbility, game);
// reduced all cost // reduced all cost

View file

@ -25,8 +25,13 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster {
@Override @Override
public void adjustTargets(Ability ability, Game game) { public void adjustTargets(Ability ability, Game game) {
Target newTarget = blueprintTarget.copy();
int count = dynamicValue.calculate(game, ability, ability.getEffects().get(0)); int count = dynamicValue.calculate(game, ability, ability.getEffects().get(0));
ability.getTargets().clear();
if (count <= 0) {
return;
}
Target newTarget = blueprintTarget.copy();
newTarget.setMaxNumberOfTargets(count); newTarget.setMaxNumberOfTargets(count);
Filter filter = newTarget.getFilter(); Filter filter = newTarget.getFilter();
if (blueprintTarget.getMinNumberOfTargets() != 0) { if (blueprintTarget.getMinNumberOfTargets() != 0) {
@ -35,7 +40,6 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster {
} else { } else {
newTarget.withTargetName(filter.getMessage() + " (up to " + count + " targets)"); newTarget.withTargetName(filter.getMessage() + " (up to " + count + " targets)");
} }
ability.getTargets().clear();
ability.addTarget(newTarget); ability.addTarget(newTarget);
} }
} }

View file

@ -1079,6 +1079,53 @@ public final class CardUtil {
.collect(Collectors.toSet()); .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. * For finding the spell or ability on the stack for "becomes the target" triggers.
* *
@ -1908,6 +1955,10 @@ public final class CardUtil {
return defaultValue; return defaultValue;
} }
public static int getSourceCostsTagX(Game game, Ability source, int defaultValue) {
return getSourceCostsTag(game, source, "X", defaultValue);
}
public static String addCostVerb(String text) { public static String addCostVerb(String text) {
if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) { if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) {
return text; return text;