mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
Reworked cost adjuster logic for better support of X and cost modification effects:
Improves: * refactor: split CostAdjuster logic in multiple parts - prepare X, prepare cost, increase cost, reduce cost; * refactor: improved VariableManaCost to support min/max values, playable and AI calculations, test framework; * refactor: improved EarlyTargetCost to support mana costs too (related to #13023); * refactor: migrated some cards with CostAdjuster and X to EarlyTargetCost (Knollspine Invocation, etc - related to #13023); * refactor: added shared code for "As an additional cost to cast this spell, discard X creature cards"; * refactor: added shared code for "X is the converted mana cost of the exiled card"; * tests: added dozens tests with cost adjusters; Bug fixes: * game: fixed that some cards with CostAdjuster ignore min/max limits for X (allow to choose any X, example: Scorched Earth, Open The Way); * game: fixed that some cards ask to announce already defined X values (example: Bargaining Table); * game: fixed that some cards with CostAdjuster do not support combo with other cost modification effects; * game, gui: fixed missing game logs about predefined X values; * game, gui: fixed wrong X icon for predefined X values; Test framework: * test framework: added X min/max check for wrong values; * test framework: added X min/max info in miss X value announce; * test framework: added check to find duplicated effect bugs (see assertNoDuplicatedEffects); Cards: * Open The Way - fixed that it allow to choose any X without limits (close #12810); * Unbound Flourishing - improved combo support for activated abilities with predefined X mana costs like Bargaining Table;
This commit is contained in:
parent
13a832ae00
commit
bae3089abb
100 changed files with 1519 additions and 449 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -49,15 +36,3 @@ public final class AbandonHope extends CardImpl {
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
BargainingTableXCost cost = ability.getManaCostsToPay().getVariableCosts().stream()
|
||||||
|
.filter(c -> c instanceof BargainingTableXCost)
|
||||||
|
.map(c -> (BargainingTableXCost) c)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (cost == null) {
|
||||||
|
throw new IllegalArgumentException("Wrong code usage: cost item lost");
|
||||||
|
}
|
||||||
|
|
||||||
if (game.inCheckPlayableState()) {
|
if (game.inCheckPlayableState()) {
|
||||||
for (UUID playerId : CardUtil.getAllPossibleTargets(ability, game)) {
|
// possible X
|
||||||
Player player = game.getPlayer(playerId);
|
int minHandSize = game.getOpponents(ability.getControllerId(), true).stream()
|
||||||
if (player != null) {
|
.map(game::getPlayer)
|
||||||
handSize = Math.min(handSize, player.getHand().size());
|
.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 {
|
} else {
|
||||||
Player player = game.getPlayer(ability.getFirstTarget());
|
// real X
|
||||||
if (player != null) {
|
Player opponent = game.getPlayer(cost.getTargets().getFirstTarget());
|
||||||
handSize = player.getHand().size();
|
if (opponent == null) {
|
||||||
|
throw new IllegalStateException("Wrong code usage: cost target lost");
|
||||||
}
|
}
|
||||||
|
ability.setVariableCostsValue(opponent.getHand().size());
|
||||||
}
|
}
|
||||||
ability.clearManaCostsToPay();
|
|
||||||
ability.addManaCostsToPay(new GenericManaCost(handSize));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")));
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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() ?
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -52,15 +42,3 @@ public final class ScorchedEarth extends CardImpl {
|
||||||
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"))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()));
|
||||||
|
|
|
||||||
|
|
@ -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)));
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
||||||
|
|
|
||||||
|
|
@ -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 spell’s mana cost or that
|
||||||
|
// ability’s 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
|
||||||
|
}
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
if (variableManaCost.wasAnnounced()) {
|
||||||
|
// announce by rules
|
||||||
|
xValue = variableManaCost.getAmount();
|
||||||
|
} else {
|
||||||
|
// announce by player
|
||||||
xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(),
|
xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(),
|
||||||
"Announce the value for " + variableManaCost.getText(), game, this);
|
"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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package mage.abilities.costs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author jayDi85
|
||||||
|
*/
|
||||||
|
public interface MinMaxVariableCost extends VariableCost {
|
||||||
|
|
||||||
|
int getMinX();
|
||||||
|
|
||||||
|
void setMinX(int minX);
|
||||||
|
|
||||||
|
int getMaxX();
|
||||||
|
|
||||||
|
void setMaxX(int maxX);
|
||||||
|
}
|
||||||
|
|
@ -10,11 +10,20 @@ import mage.players.Player;
|
||||||
import mage.target.common.TargetCardInHand;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package mage.abilities.costs.costadjusters;
|
||||||
|
|
||||||
|
import mage.abilities.Ability;
|
||||||
|
import mage.abilities.common.SimpleStaticAbility;
|
||||||
|
import mage.abilities.costs.CostAdjuster;
|
||||||
|
import mage.abilities.costs.common.DiscardTargetCost;
|
||||||
|
import mage.abilities.effects.common.InfoEffect;
|
||||||
|
import mage.cards.Card;
|
||||||
|
import mage.constants.Zone;
|
||||||
|
import mage.filter.FilterCard;
|
||||||
|
import mage.game.Game;
|
||||||
|
import mage.players.Player;
|
||||||
|
import mage.target.common.TargetCardInHand;
|
||||||
|
import mage.util.CardUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to setup discard cost with {X} mana cost
|
||||||
|
* <p>
|
||||||
|
* If you don't have {X} then use DiscardXTargetCost instead
|
||||||
|
* <p>
|
||||||
|
* Example:
|
||||||
|
* - {X}{1}{B}
|
||||||
|
* - As an additional cost to cast this spell, discard X cards.
|
||||||
|
*
|
||||||
|
* @author JayDi85
|
||||||
|
*/
|
||||||
|
public class DiscardXCardsCostAdjuster implements CostAdjuster {
|
||||||
|
|
||||||
|
private final FilterCard filter;
|
||||||
|
|
||||||
|
private DiscardXCardsCostAdjuster(FilterCard filter) {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepareX(Ability ability, Game game) {
|
||||||
|
Player controller = game.getPlayer(ability.getControllerId());
|
||||||
|
if (controller == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int minX = 0;
|
||||||
|
int maxX = controller.getHand().getCards(this.filter, ability.getControllerId(), ability, game).size();
|
||||||
|
ability.setVariableCostsMinMax(minX, maxX);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepareCost(Ability ability, Game game) {
|
||||||
|
int x = CardUtil.getSourceCostsTagX(game, ability, -1);
|
||||||
|
if (x >= 0) {
|
||||||
|
ability.addCost(new DiscardTargetCost(new TargetCardInHand(x, x, this.filter)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addAdjusterAndMessage(Card card, FilterCard filter) {
|
||||||
|
addAdjusterAndMessage(card, filter, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addAdjusterAndMessage(Card card, FilterCard filter, boolean isRandom) {
|
||||||
|
if (card.getSpellAbility().getManaCosts().getVariableCosts().isEmpty()) {
|
||||||
|
// how to fix: use DiscardXTargetCost
|
||||||
|
throw new IllegalArgumentException("Wrong code usage: that's cost adjuster must be used with {X} in mana costs only - " + card);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ability ability = new SimpleStaticAbility(
|
||||||
|
Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X " + filter.getMessage())
|
||||||
|
);
|
||||||
|
ability.setRuleAtTheTop(true);
|
||||||
|
card.addAbility(ability);
|
||||||
|
|
||||||
|
card.getSpellAbility().setCostAdjuster(new DiscardXCardsCostAdjuster(filter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ public enum DomainAdjuster implements CostAdjuster {
|
||||||
instance;
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,23 +25,30 @@ 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;
|
||||||
|
if (game.inCheckPlayableState()) {
|
||||||
|
// possible
|
||||||
|
reduceCount = 2 * cardCount;
|
||||||
|
} else {
|
||||||
|
// real - need to choose
|
||||||
|
// TODO: need early target cost instead dialog here
|
||||||
|
int toExile = cardCount == 0 ? 0 : player.getAmount(
|
||||||
0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game
|
0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game
|
||||||
) : 0;
|
);
|
||||||
|
reduceCount = 2 * toExile;
|
||||||
if (toExile > 0) {
|
if (toExile > 0) {
|
||||||
ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter)));
|
ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter)));
|
||||||
CardUtil.reduceCost(ability, 2 * toExile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CardUtil.reduceCost(ability, 2 * reduceCount);
|
||||||
|
}
|
||||||
|
|
||||||
public static final void addAdjusterAndMessage(Card card, FilterCard filter) {
|
public static final void addAdjusterAndMessage(Card card, FilterCard filter) {
|
||||||
card.addAbility(new SimpleStaticAbility(
|
card.addAbility(new SimpleStaticAbility(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package mage.abilities.costs.costadjusters;
|
||||||
|
|
||||||
|
import mage.abilities.Ability;
|
||||||
|
import mage.abilities.costs.CostAdjuster;
|
||||||
|
import mage.cards.Card;
|
||||||
|
import mage.game.Game;
|
||||||
|
import mage.game.permanent.Permanent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for {X} mana cost that must be replaced by imprinted mana value
|
||||||
|
* <p>
|
||||||
|
* Example:
|
||||||
|
* - Elite Arcanist
|
||||||
|
* - {X}, {T}: Copy the exiled card. ... X is the converted mana cost of the exiled card.
|
||||||
|
*
|
||||||
|
* @author JayDi85
|
||||||
|
*/
|
||||||
|
public enum ImprintedManaValueXCostAdjuster implements CostAdjuster {
|
||||||
|
instance;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepareX(Ability ability, Game game) {
|
||||||
|
int manaValue = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
Permanent sourcePermanent = game.getPermanent(ability.getSourceId());
|
||||||
|
if (sourcePermanent != null
|
||||||
|
&& sourcePermanent.getImprinted() != null
|
||||||
|
&& !sourcePermanent.getImprinted().isEmpty()) {
|
||||||
|
Card imprintedInstant = game.getCard(sourcePermanent.getImprinted().get(0));
|
||||||
|
if (imprintedInstant != null) {
|
||||||
|
manaValue = imprintedInstant.getManaValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ability.setVariableCostsValue(manaValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,10 +29,8 @@ public enum LegendaryCreatureCostAdjuster implements CostAdjuster {
|
||||||
);
|
);
|
||||||
|
|
||||||
@Override
|
@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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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++;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue