diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index d21ecf58811..71ec32b1c21 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -1863,8 +1863,11 @@ public class ComputerPlayer extends PlayerImpl { @Override public int announceXMana(int min, int max, String message, Game game, Ability ability) { - log.debug("announceXMana"); - //TODO: improve this + // current logic - use max possible mana + + // TODO: add good/bad effects support + // TODO: add simple game simulations like declare blocker? + int numAvailable = getAvailableManaProducers(game).size() - ability.getManaCosts().manaValue(); if (numAvailable < 0) { numAvailable = 0; @@ -1881,12 +1884,17 @@ public class ComputerPlayer extends PlayerImpl { @Override public int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variablCost) { - log.debug("announceXCost"); + // current logic - use random non-zero value + + // TODO: add good/bad effects support + // TODO: remove random logic + int value = RandomUtil.nextInt(CardUtil.overflowInc(max, 1)); if (value < min) { value = min; } if (value < max) { + // do not use zero values value++; } return value; diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 2320626ce54..46bafaf3eb4 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -1709,6 +1709,8 @@ public class HumanPlayer extends PlayerImpl { if (response.getInteger() != null) { break; } + + // TODO: add response verify here } if (response.getInteger() != null) { diff --git a/Mage.Sets/src/mage/cards/a/AbandonHope.java b/Mage.Sets/src/mage/cards/a/AbandonHope.java index 16365813b9d..0aeae4b6229 100644 --- a/Mage.Sets/src/mage/cards/a/AbandonHope.java +++ b/Mage.Sets/src/mage/cards/a/AbandonHope.java @@ -1,43 +1,30 @@ package mage.cards.a; -import mage.abilities.Ability; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster; import mage.abilities.dynamicvalue.common.GetXValue; -import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.discard.LookTargetHandChooseDiscardEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Zone; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.target.common.TargetCardInHand; import mage.target.common.TargetOpponent; -import mage.util.CardUtil; import java.util.UUID; /** - * @author fireshoes + * @author fireshoes, JayDi85 */ public final class AbandonHope extends CardImpl { public AbandonHope(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{1}{B}"); - // As an additional cost to cast Abandon Hope, discard X cards. - Ability ability = new SimpleStaticAbility( - Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X cards") - ); - ability.setRuleAtTheTop(true); - this.addAbility(ability); + // As an additional cost to cast this spell, discard X cards. + DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_CARDS); // Look at target opponent's hand and choose X cards from it. That player discards those cards. this.getSpellAbility().addEffect(new LookTargetHandChooseDiscardEffect(false, GetXValue.instance, StaticFilters.FILTER_CARD_CARDS)); this.getSpellAbility().addTarget(new TargetOpponent()); - this.getSpellAbility().setCostAdjuster(AbandonHopeAdjuster.instance); } private AbandonHope(final AbandonHope card) { @@ -48,16 +35,4 @@ public final class AbandonHope extends CardImpl { public AbandonHope copy() { 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))); - } - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/a/AetherTide.java b/Mage.Sets/src/mage/cards/a/AetherTide.java index 885f74c69f0..eef2409f2b3 100644 --- a/Mage.Sets/src/mage/cards/a/AetherTide.java +++ b/Mage.Sets/src/mage/cards/a/AetherTide.java @@ -1,27 +1,18 @@ package mage.cards.a; -import mage.abilities.Ability; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster; import mage.abilities.effects.Effect; -import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Zone; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.target.common.TargetCardInHand; import mage.target.common.TargetCreaturePermanent; import mage.target.targetadjustment.XTargetsCountAdjuster; -import mage.util.CardUtil; import java.util.UUID; /** - * * @author jeffwadsworth */ public final class AetherTide extends CardImpl { @@ -29,10 +20,8 @@ public final class AetherTide extends CardImpl { public AetherTide(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{U}"); - // As an additional cost to cast Aether Tide, discard X creature cards. - Ability ability = new SimpleStaticAbility(Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X creature cards")); - ability.setRuleAtTheTop(true); - this.addAbility(ability); + // As an additional cost to cast this spell, discard X creature cards. + DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_CREATURES); // Return X target creatures to their owners' hands. Effect effect = new ReturnToHandTargetEffect(); @@ -40,8 +29,6 @@ public final class AetherTide extends CardImpl { this.getSpellAbility().addEffect(effect); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); this.getSpellAbility().setTargetAdjuster(new XTargetsCountAdjuster()); - this.getSpellAbility().setCostAdjuster(AetherTideCostAdjuster.instance); - } private AetherTide(final AetherTide card) { @@ -53,15 +40,3 @@ public final class AetherTide extends CardImpl { return new AetherTide(this); } } - -enum AetherTideCostAdjuster implements CostAdjuster { - instance; - - @Override - public void adjustCosts(Ability ability, Game game) { - int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); - if (xValue > 0) { - ability.addCost(new DiscardTargetCost(new TargetCardInHand(xValue, xValue, StaticFilters.FILTER_CARD_CREATURES))); - } - } -} diff --git a/Mage.Sets/src/mage/cards/a/AladdinsLamp.java b/Mage.Sets/src/mage/cards/a/AladdinsLamp.java index 44614e7ec56..a4f7611e448 100644 --- a/Mage.Sets/src/mage/cards/a/AladdinsLamp.java +++ b/Mage.Sets/src/mage/cards/a/AladdinsLamp.java @@ -5,7 +5,6 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.effects.ReplacementEffectImpl; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -36,12 +35,8 @@ public final class AladdinsLamp extends CardImpl { // {X}, {T}: The next time you would draw a card this turn, instead look at the top X cards of your library, put all but one of them on the bottom of your library in a random order, then draw a card. X can't be 0. Ability ability = new SimpleActivatedAbility(new AladdinsLampEffect(), new ManaCostsImpl<>("{X}")); ability.addCost(new TapSourceCost()); - for (Object cost : ability.getManaCosts()) { - if (cost instanceof VariableManaCost) { - ((VariableManaCost) cost).setMinX(1); - break; - } - } + ability.setVariableCostsMinMax(1, Integer.MAX_VALUE); + // TODO: add costAdjuster for min/max values due library size this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/a/ArmMountedAnchor.java b/Mage.Sets/src/mage/cards/a/ArmMountedAnchor.java index 1ee3a83718c..ca6455ad195 100644 --- a/Mage.Sets/src/mage/cards/a/ArmMountedAnchor.java +++ b/Mage.Sets/src/mage/cards/a/ArmMountedAnchor.java @@ -71,7 +71,7 @@ enum ArmMountedAnchorAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { // checking state if (HeckbentCondition.instance.apply(game, ability)) { CardUtil.reduceCost(ability, 2); diff --git a/Mage.Sets/src/mage/cards/b/BargainingTable.java b/Mage.Sets/src/mage/cards/b/BargainingTable.java index 3f280d7f552..62a928c6e58 100644 --- a/Mage.Sets/src/mage/cards/b/BargainingTable.java +++ b/Mage.Sets/src/mage/cards/b/BargainingTable.java @@ -1,25 +1,28 @@ package mage.cards.b; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.CostAdjuster; +import mage.abilities.costs.EarlyTargetCost; +import mage.abilities.costs.VariableCostType; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.InfoEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.Outcome; import mage.game.Game; import mage.players.Player; +import mage.target.Target; import mage.target.common.TargetOpponent; -import mage.util.CardUtil; + +import java.util.Objects; +import java.util.UUID; /** - * - * @author awjackson + * @author awjackson, JayDi85 */ public final class BargainingTable extends CardImpl { @@ -27,13 +30,10 @@ public final class BargainingTable extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{5}"); // {X}, {T}: Draw a card. X is the number of cards in an opponent's hand. - Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new ManaCostsImpl<>("{X}")); + Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new BargainingTableXCost()); ability.addCost(new TapSourceCost()); ability.addEffect(new InfoEffect("X is the number of cards in an opponent's hand")); - // You choose an opponent on announcement. This is not targeted, but a choice is still made. - // This choice is made before determining the value for X that is used in the cost. (2004-10-04) - ability.addTarget(new TargetOpponent(true)); - ability.setCostAdjuster(BargainingTableAdjuster.instance); + ability.setCostAdjuster(BargainingTableCostAdjuster.instance); this.addAbility(ability); } @@ -47,26 +47,70 @@ public final class BargainingTable extends CardImpl { } } -enum BargainingTableAdjuster implements CostAdjuster { +class BargainingTableXCost extends VariableManaCost implements EarlyTargetCost { + + // You choose an opponent on announcement. This is not targeted, but a choice is still made. + // This choice is made before determining the value for X that is used in the cost. + // (2004-10-04) + + public BargainingTableXCost() { + super(VariableCostType.NORMAL, 1); + } + + public BargainingTableXCost(final BargainingTableXCost cost) { + super(cost); + } + + @Override + public void chooseTarget(Game game, Ability source, Player controller) { + Target targetOpponent = new TargetOpponent(true); + controller.choose(Outcome.Benefit, targetOpponent, source, game); + addTarget(targetOpponent); + } + + @Override + public BargainingTableXCost copy() { + return new BargainingTableXCost(this); + } +} + +enum BargainingTableCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { - int handSize = Integer.MAX_VALUE; - if (game.inCheckPlayableState()) { - for (UUID playerId : CardUtil.getAllPossibleTargets(ability, game)) { - Player player = game.getPlayer(playerId); - if (player != null) { - handSize = Math.min(handSize, player.getHand().size()); - } - } - } else { - Player player = game.getPlayer(ability.getFirstTarget()); - if (player != null) { - handSize = player.getHand().size(); - } + public void prepareX(Ability ability, Game game) { + // make sure early target used + BargainingTableXCost cost = ability.getManaCostsToPay().getVariableCosts().stream() + .filter(c -> c instanceof BargainingTableXCost) + .map(c -> (BargainingTableXCost) c) + .findFirst() + .orElse(null); + if (cost == null) { + throw new IllegalArgumentException("Wrong code usage: cost item lost"); + } + + if (game.inCheckPlayableState()) { + // possible X + int minHandSize = game.getOpponents(ability.getControllerId(), true).stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .mapToInt(p -> p.getHand().size()) + .min() + .orElse(0); + int maxHandSize = game.getOpponents(ability.getControllerId(), true).stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .mapToInt(p -> p.getHand().size()) + .max() + .orElse(Integer.MAX_VALUE); + ability.setVariableCostsMinMax(minHandSize, maxHandSize); + } else { + // real X + Player opponent = game.getPlayer(cost.getTargets().getFirstTarget()); + if (opponent == null) { + throw new IllegalStateException("Wrong code usage: cost target lost"); + } + ability.setVariableCostsValue(opponent.getHand().size()); } - ability.clearManaCostsToPay(); - ability.addManaCostsToPay(new GenericManaCost(handSize)); } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java b/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java index d87af6d364b..c76fb6d540b 100644 --- a/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java +++ b/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java @@ -19,10 +19,7 @@ import mage.abilities.hint.ValueHint; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.SubType; -import mage.constants.SuperType; +import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterCreaturePermanent; @@ -116,7 +113,7 @@ enum BaruWurmspeakerAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int value = BaruWurmspeakerValue.instance.calculate(game, ability, null); if (value > 0) { CardUtil.reduceCost(ability, value); diff --git a/Mage.Sets/src/mage/cards/b/BattlefieldButcher.java b/Mage.Sets/src/mage/cards/b/BattlefieldButcher.java index ab752a8e2cb..6251116133f 100644 --- a/Mage.Sets/src/mage/cards/b/BattlefieldButcher.java +++ b/Mage.Sets/src/mage/cards/b/BattlefieldButcher.java @@ -58,7 +58,7 @@ enum BattlefieldButcherAdjuster implements CostAdjuster { private static final Hint hint = new ValueHint("Creature cards in your graveyard", xValue); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, xValue.calculate(game, ability, null)); } diff --git a/Mage.Sets/src/mage/cards/b/BeltOfGiantStrength.java b/Mage.Sets/src/mage/cards/b/BeltOfGiantStrength.java index cd9693c66f6..40acc7e97f6 100644 --- a/Mage.Sets/src/mage/cards/b/BeltOfGiantStrength.java +++ b/Mage.Sets/src/mage/cards/b/BeltOfGiantStrength.java @@ -3,20 +3,21 @@ package mage.cards.b; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.CostAdjuster; +import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.common.continuous.SetBasePowerToughnessEnchantedEffect; import mage.abilities.keyword.EquipAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.Outcome; import mage.constants.SubType; import mage.game.Game; -import mage.game.permanent.Permanent; +import mage.target.common.TargetControlledCreaturePermanent; import mage.util.CardUtil; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; -import mage.abilities.costs.mana.GenericManaCost; -import mage.constants.Outcome; -import mage.target.common.TargetControlledCreaturePermanent; /** * @author TheElk801 @@ -35,7 +36,7 @@ public final class BeltOfGiantStrength extends CardImpl { // Equip {10}. This ability costs {X} less to activate where X is the power of the creature it targets. EquipAbility ability = new EquipAbility(Outcome.BoostCreature, new GenericManaCost(10), new TargetControlledCreaturePermanent(), false); ability.setCostReduceText("This ability costs {X} less to activate, where X is the power of the creature it targets."); - ability.setCostAdjuster(BeltOfGiantStrengthAdjuster.instance); + ability.setCostAdjuster(BeltOfGiantStrengthCostAdjuster.instance); this.addAbility(ability); } @@ -49,33 +50,23 @@ public final class BeltOfGiantStrength extends CardImpl { } } -enum BeltOfGiantStrengthAdjuster implements CostAdjuster { +enum BeltOfGiantStrengthCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { + int power; if (game.inCheckPlayableState()) { - int maxPower = 0; - for (UUID permId : CardUtil.getAllPossibleTargets(ability, game)) { - Permanent permanent = game.getPermanent(permId); - if (permanent != null) { - int power = permanent.getPower().getValue(); - if (power > maxPower) { - maxPower = power; - } - } - } - if (maxPower > 0) { - CardUtil.reduceCost(ability, maxPower); - } + power = CardUtil.getAllPossibleTargets(ability, game).stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .mapToInt(p -> p.getPower().getValue()) + .max().orElse(0); } else { - Permanent permanent = game.getPermanent(ability.getFirstTarget()); - if (permanent != null) { - int power = permanent.getPower().getValue(); - if (power > 0) { - CardUtil.reduceCost(ability, power); - } - } + power = Optional.ofNullable(game.getPermanent(ability.getFirstTarget())) + .map(p -> p.getPower().getValue()) + .orElse(0); } + CardUtil.reduceCost(ability, power); } } diff --git a/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java b/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java index 98e460eadb2..b705cba0318 100644 --- a/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java +++ b/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java @@ -60,7 +60,7 @@ enum BiteDownOnCrimeAdjuster implements CostAdjuster { private static final OptionalAdditionalCost collectEvidenceCost = CollectEvidenceAbility.makeCost(6); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (CollectedEvidenceCondition.instance.apply(game, ability) || (game.inCheckPlayableState() && collectEvidenceCost.canPay(ability, null, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); diff --git a/Mage.Sets/src/mage/cards/c/CallerOfTheHunt.java b/Mage.Sets/src/mage/cards/c/CallerOfTheHunt.java index a486ea2e0b7..6b2043d66e7 100644 --- a/Mage.Sets/src/mage/cards/c/CallerOfTheHunt.java +++ b/Mage.Sets/src/mage/cards/c/CallerOfTheHunt.java @@ -59,7 +59,7 @@ enum CallerOfTheHuntAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { if (game.inCheckPlayableState()) { return; } @@ -103,6 +103,7 @@ enum CallerOfTheHuntAdjuster implements CostAdjuster { game.getState().setValue(sourceObject.getId() + "_type", maxSubType); } else { // human choose + // TODO: need early target cost instead dialog here Effect effect = new ChooseCreatureTypeEffect(Outcome.Benefit); effect.apply(game, ability); } diff --git a/Mage.Sets/src/mage/cards/c/CaptainAmericaFirstAvenger.java b/Mage.Sets/src/mage/cards/c/CaptainAmericaFirstAvenger.java index 34ecb776194..807a333291b 100644 --- a/Mage.Sets/src/mage/cards/c/CaptainAmericaFirstAvenger.java +++ b/Mage.Sets/src/mage/cards/c/CaptainAmericaFirstAvenger.java @@ -4,6 +4,7 @@ import java.util.UUID; import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; +import mage.abilities.costs.CostImpl; import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.Cost; @@ -130,7 +131,7 @@ enum CaptainAmericaFirstAvengerValue implements DynamicValue { } } -class CaptainAmericaFirstAvengerUnattachCost extends EarlyTargetCost { +class CaptainAmericaFirstAvengerUnattachCost extends CostImpl implements EarlyTargetCost { private static final FilterPermanent filter = new FilterEquipmentPermanent("equipment attached to this creature"); private static final FilterPermanent subfilter = new FilterControlledPermanent("{this}"); diff --git a/Mage.Sets/src/mage/cards/c/ChanneledForce.java b/Mage.Sets/src/mage/cards/c/ChanneledForce.java index 74917f9627a..39a2d151ab1 100644 --- a/Mage.Sets/src/mage/cards/c/ChanneledForce.java +++ b/Mage.Sets/src/mage/cards/c/ChanneledForce.java @@ -25,7 +25,7 @@ public final class ChanneledForce extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{U}{R}"); // As an additional cost to cast this spell, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS)); + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Target player draws X cards. Channeled Force deals X damage to up to one target creature or planeswalker. this.getSpellAbility().addEffect(new ChanneledForceEffect()); diff --git a/Mage.Sets/src/mage/cards/c/CrownOfGondor.java b/Mage.Sets/src/mage/cards/c/CrownOfGondor.java index 27329de82bf..d9c7c0cb3f9 100644 --- a/Mage.Sets/src/mage/cards/c/CrownOfGondor.java +++ b/Mage.Sets/src/mage/cards/c/CrownOfGondor.java @@ -72,7 +72,7 @@ enum CrownOfGondorAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (MonarchIsSourceControllerCondition.instance.apply(game, ability)) { CardUtil.reduceCost(ability, 3); } diff --git a/Mage.Sets/src/mage/cards/d/DeepwoodDenizen.java b/Mage.Sets/src/mage/cards/d/DeepwoodDenizen.java index 5733ba08da2..a3097459e2f 100644 --- a/Mage.Sets/src/mage/cards/d/DeepwoodDenizen.java +++ b/Mage.Sets/src/mage/cards/d/DeepwoodDenizen.java @@ -62,7 +62,7 @@ enum DeepwoodDenizenAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, DeepwoodDenizenValue.instance.calculate(game, ability, null)); } } diff --git a/Mage.Sets/src/mage/cards/d/DevastatingDreams.java b/Mage.Sets/src/mage/cards/d/DevastatingDreams.java index 6847e84dfe6..7c42ed2a34f 100644 --- a/Mage.Sets/src/mage/cards/d/DevastatingDreams.java +++ b/Mage.Sets/src/mage/cards/d/DevastatingDreams.java @@ -5,6 +5,8 @@ import mage.abilities.costs.Cost; import mage.abilities.costs.VariableCostImpl; import mage.abilities.costs.VariableCostType; import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.common.DiscardXTargetCost; +import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.common.DamageAllEffect; import mage.abilities.effects.common.SacrificeAllEffect; @@ -12,6 +14,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.filter.common.FilterControlledLandPermanent; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; @@ -28,8 +31,8 @@ public final class DevastatingDreams extends CardImpl { public DevastatingDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{R}{R}"); - // As an additional cost to cast Devastating Dreams, discard X cards at random. - this.getSpellAbility().addCost(new DevastatingDreamsAdditionalCost()); + // As an additional cost to cast this spell, discard X cards at random. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true).withRandom()); // Each player sacrifices X lands. this.getSpellAbility().addEffect(new SacrificeAllEffect(GetXValue.instance, new FilterControlledLandPermanent("lands"))); diff --git a/Mage.Sets/src/mage/cards/e/EliteArcanist.java b/Mage.Sets/src/mage/cards/e/EliteArcanist.java index ff2ad860b1e..d69a5fdc689 100644 --- a/Mage.Sets/src/mage/cards/e/EliteArcanist.java +++ b/Mage.Sets/src/mage/cards/e/EliteArcanist.java @@ -1,12 +1,13 @@ package mage.cards.e; +import mage.ApprovingObject; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.CostAdjuster; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; @@ -23,7 +24,6 @@ import mage.players.Player; import mage.target.TargetCard; import java.util.UUID; -import mage.ApprovingObject; /** * @author LevelX2 @@ -44,7 +44,7 @@ public final class EliteArcanist extends CardImpl { // {X}, {T}: Copy the exiled card. You may cast the copy without paying its mana cost. X is the converted mana cost of the exiled card. Ability ability = new SimpleActivatedAbility(new EliteArcanistCopyEffect(), new ManaCostsImpl<>("{X}")); ability.addCost(new TapSourceCost()); - ability.setCostAdjuster(EliteArcanistAdjuster.instance); + ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance); this.addAbility(ability); } @@ -58,29 +58,6 @@ public final class EliteArcanist extends CardImpl { } } -enum EliteArcanistAdjuster implements CostAdjuster { - instance; - - @Override - public void adjustCosts(Ability ability, Game game) { - Permanent sourcePermanent = game.getPermanent(ability.getSourceId()); - if (sourcePermanent == null - || sourcePermanent.getImprinted() == null - || sourcePermanent.getImprinted().isEmpty()) { - return; - } - Card imprintedInstant = game.getCard(sourcePermanent.getImprinted().get(0)); - if (imprintedInstant == null) { - return; - } - int cmc = imprintedInstant.getManaValue(); - if (cmc > 0) { - ability.clearManaCostsToPay(); - ability.addManaCostsToPay(new GenericManaCost(cmc)); - } - } -} - class EliteArcanistImprintEffect extends OneShotEffect { private static final FilterCard filter = new FilterCard("instant card from your hand"); diff --git a/Mage.Sets/src/mage/cards/e/EsquireOfTheKing.java b/Mage.Sets/src/mage/cards/e/EsquireOfTheKing.java index 6f1ecf66002..86d81519f8d 100644 --- a/Mage.Sets/src/mage/cards/e/EsquireOfTheKing.java +++ b/Mage.Sets/src/mage/cards/e/EsquireOfTheKing.java @@ -64,7 +64,7 @@ enum EsquireOfTheKingAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_CREATURE_LEGENDARY, ability, game, 1)) { CardUtil.reduceCost(ability, 2); } diff --git a/Mage.Sets/src/mage/cards/e/EtheriumPteramander.java b/Mage.Sets/src/mage/cards/e/EtheriumPteramander.java index 2b29f88b3bd..681cdb1cb70 100644 --- a/Mage.Sets/src/mage/cards/e/EtheriumPteramander.java +++ b/Mage.Sets/src/mage/cards/e/EtheriumPteramander.java @@ -80,8 +80,7 @@ enum EtheriumPteramanderAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { - int count = artifactCount.calculate(game, ability, null); - CardUtil.reduceCost(ability, count); + public void reduceCost(Ability ability, Game game) { + CardUtil.reduceCost(ability, artifactCount.calculate(game, ability, null)); } } diff --git a/Mage.Sets/src/mage/cards/f/Fireball.java b/Mage.Sets/src/mage/cards/f/Fireball.java index acb030d570e..b55b90de1b9 100644 --- a/Mage.Sets/src/mage/cards/f/Fireball.java +++ b/Mage.Sets/src/mage/cards/f/Fireball.java @@ -2,11 +2,11 @@ package mage.cards.f; import mage.abilities.Ability; import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.OneShotEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.CostModificationType; import mage.constants.Outcome; import mage.game.Game; import mage.game.permanent.Permanent; @@ -24,8 +24,8 @@ public final class Fireball extends CardImpl { public Fireball(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{R}"); - // Fireball deals X damage divided evenly, rounded down, among any number of target creatures and/or players. - // Fireball costs 1 more to cast for each target beyond the first. + // This spell costs {1} more to cast for each target beyond the first. + // Fireball deals X damage divided evenly, rounded down, among any number of targets. this.getSpellAbility().addTarget(new FireballTargetCreatureOrPlayer(0, Integer.MAX_VALUE)); this.getSpellAbility().addEffect(new FireballEffect()); this.getSpellAbility().setCostAdjuster(FireballAdjuster.instance); @@ -45,10 +45,10 @@ enum FireballAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void increaseCost(Ability ability, Game game) { int numTargets = ability.getTargets().isEmpty() ? 0 : ability.getTargets().get(0).getTargets().size(); if (numTargets > 1) { - ability.addManaCostsToPay(new GenericManaCost(numTargets - 1)); + CardUtil.increaseCost(ability, numTargets - 1); } } } diff --git a/Mage.Sets/src/mage/cards/f/Firestorm.java b/Mage.Sets/src/mage/cards/f/Firestorm.java index a2f7f19f98f..2a01e592ac8 100644 --- a/Mage.Sets/src/mage/cards/f/Firestorm.java +++ b/Mage.Sets/src/mage/cards/f/Firestorm.java @@ -9,6 +9,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; @@ -25,8 +26,8 @@ public final class Firestorm extends CardImpl { public Firestorm(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}"); - // As an additional cost to cast Firestorm, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); + // As an additional cost to cast this spell, discard X cards. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Firestorm deals X damage to each of X target creatures and/or players. this.getSpellAbility().addEffect(new FirestormEffect()); diff --git a/Mage.Sets/src/mage/cards/f/FugitiveCodebreaker.java b/Mage.Sets/src/mage/cards/f/FugitiveCodebreaker.java index e21d8c6fc0d..d38e97e54aa 100644 --- a/Mage.Sets/src/mage/cards/f/FugitiveCodebreaker.java +++ b/Mage.Sets/src/mage/cards/f/FugitiveCodebreaker.java @@ -92,7 +92,7 @@ enum FugitiveCodebreakerAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, FugitiveCodebreakerDisguiseAbility.xValue.calculate(game, ability, null)); } } diff --git a/Mage.Sets/src/mage/cards/g/GhostfireBlade.java b/Mage.Sets/src/mage/cards/g/GhostfireBlade.java index ad2537bc363..57b42d9ba57 100644 --- a/Mage.Sets/src/mage/cards/g/GhostfireBlade.java +++ b/Mage.Sets/src/mage/cards/g/GhostfireBlade.java @@ -55,9 +55,9 @@ enum GhostfireBladeAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { - // checking state + public void reduceCost(Ability ability, Game game) { if (game.inCheckPlayableState()) { + // possible if (CardUtil .getAllPossibleTargets(ability, game) .stream() @@ -67,6 +67,7 @@ enum GhostfireBladeAdjuster implements CostAdjuster { return; } } else { + // real Permanent permanent = game.getPermanent(ability.getFirstTarget()); if (permanent == null || !permanent.getColor(game).isColorless()) { return; diff --git a/Mage.Sets/src/mage/cards/g/GrimGiganotosaurus.java b/Mage.Sets/src/mage/cards/g/GrimGiganotosaurus.java index f67309915c5..8d4cb8ba558 100644 --- a/Mage.Sets/src/mage/cards/g/GrimGiganotosaurus.java +++ b/Mage.Sets/src/mage/cards/g/GrimGiganotosaurus.java @@ -74,7 +74,7 @@ enum GrimGiganotosaurusAdjuster implements CostAdjuster { private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(filter); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { CardUtil.reduceCost(ability, xValue.calculate(game, ability, null)); diff --git a/Mage.Sets/src/mage/cards/h/HamletGlutton.java b/Mage.Sets/src/mage/cards/h/HamletGlutton.java index 2108c8ad0a4..703877d465b 100644 --- a/Mage.Sets/src/mage/cards/h/HamletGlutton.java +++ b/Mage.Sets/src/mage/cards/h/HamletGlutton.java @@ -61,7 +61,7 @@ enum HamletGluttonAdjuster implements CostAdjuster { private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost(); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); diff --git a/Mage.Sets/src/mage/cards/h/HelmOfObedience.java b/Mage.Sets/src/mage/cards/h/HelmOfObedience.java index a0498fd6251..469d26fcb8a 100644 --- a/Mage.Sets/src/mage/cards/h/HelmOfObedience.java +++ b/Mage.Sets/src/mage/cards/h/HelmOfObedience.java @@ -2,9 +2,8 @@ package mage.cards.h; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.costs.VariableCostType; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.VariableManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.OneShotEffect; import mage.cards.*; @@ -29,9 +28,8 @@ public final class HelmOfObedience extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}"); // {X}, {T}: Target opponent puts cards from the top of their library into their graveyard until a creature card or X cards are put into that graveyard this way, whichever comes first. If a creature card is put into that graveyard this way, sacrifice Helm of Obedience and put that card onto the battlefield under your control. X can't be 0. - VariableManaCost xCosts = new VariableManaCost(VariableCostType.NORMAL); - xCosts.setMinX(1); - Ability ability = new SimpleActivatedAbility(new HelmOfObedienceEffect(), xCosts); + Ability ability = new SimpleActivatedAbility(new HelmOfObedienceEffect(), new ManaCostsImpl<>("{X}")); + ability.setVariableCostsMinMax(1, Integer.MAX_VALUE); ability.addCost(new TapSourceCost()); ability.addTarget(new TargetOpponent()); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/h/HinataDawnCrowned.java b/Mage.Sets/src/mage/cards/h/HinataDawnCrowned.java index a685b776c34..27fa375465f 100644 --- a/Mage.Sets/src/mage/cards/h/HinataDawnCrowned.java +++ b/Mage.Sets/src/mage/cards/h/HinataDawnCrowned.java @@ -117,6 +117,10 @@ final class HinataDawnCrownedEffectUtility public static int getTargetCount(Game game, Ability abilityToModify) { if (game.inCheckPlayableState()) { + abilityToModify.getTargets().stream() + .mapToInt(a -> !a.isRequired() ? 0 : a.getMinNumberOfTargets()) + .min() + .orElse(0); Optional max = abilityToModify.getTargets().stream().map(x -> x.getMaxNumberOfTargets()).max(Integer::compare); int allPossibleSize = CardUtil.getAllPossibleTargets(abilityToModify, game).size(); return max.isPresent() ? diff --git a/Mage.Sets/src/mage/cards/h/HurkylsFinalMeditation.java b/Mage.Sets/src/mage/cards/h/HurkylsFinalMeditation.java index 4b3aa279774..3a7a896563f 100644 --- a/Mage.Sets/src/mage/cards/h/HurkylsFinalMeditation.java +++ b/Mage.Sets/src/mage/cards/h/HurkylsFinalMeditation.java @@ -49,7 +49,7 @@ enum HurkylsFinalMeditationAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void increaseCost(Ability ability, Game game) { if (!game.isActivePlayer(ability.getControllerId())) { CardUtil.increaseCost(ability, 3); } diff --git a/Mage.Sets/src/mage/cards/h/HyldasCrownOfWinter.java b/Mage.Sets/src/mage/cards/h/HyldasCrownOfWinter.java index 51d266276c8..0dc2a950b17 100644 --- a/Mage.Sets/src/mage/cards/h/HyldasCrownOfWinter.java +++ b/Mage.Sets/src/mage/cards/h/HyldasCrownOfWinter.java @@ -82,7 +82,7 @@ enum HyldasCrownOfWinterAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (ability.getControllerId().equals(game.getActivePlayerId())) { CardUtil.reduceCost(ability, 1); } diff --git a/Mage.Sets/src/mage/cards/i/IceOut.java b/Mage.Sets/src/mage/cards/i/IceOut.java index f98ed44b76e..6d4dcf0eeb3 100644 --- a/Mage.Sets/src/mage/cards/i/IceOut.java +++ b/Mage.Sets/src/mage/cards/i/IceOut.java @@ -52,7 +52,7 @@ enum IceOutAdjuster implements CostAdjuster { private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost(); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 1); diff --git a/Mage.Sets/src/mage/cards/i/InsidiousDreams.java b/Mage.Sets/src/mage/cards/i/InsidiousDreams.java index d435205d5de..6ec4080d071 100644 --- a/Mage.Sets/src/mage/cards/i/InsidiousDreams.java +++ b/Mage.Sets/src/mage/cards/i/InsidiousDreams.java @@ -25,7 +25,7 @@ public final class InsidiousDreams extends CardImpl { public InsidiousDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{B}"); - // As an additional cost to cast Insidious Dreams, discard X cards. + // As an additional cost to cast this spell, discard X cards. this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Search your library for X cards. Then shuffle your library and put those cards on top of it in any order. diff --git a/Mage.Sets/src/mage/cards/j/JohannsStopgap.java b/Mage.Sets/src/mage/cards/j/JohannsStopgap.java index 8f08fa1a1ef..fc34f2aa974 100644 --- a/Mage.Sets/src/mage/cards/j/JohannsStopgap.java +++ b/Mage.Sets/src/mage/cards/j/JohannsStopgap.java @@ -54,7 +54,7 @@ enum JohannsStopgapAdjuster implements CostAdjuster { private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost(); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); diff --git a/Mage.Sets/src/mage/cards/k/KamiOfJealousThirst.java b/Mage.Sets/src/mage/cards/k/KamiOfJealousThirst.java index fcd4bf15822..934bf79e8fb 100644 --- a/Mage.Sets/src/mage/cards/k/KamiOfJealousThirst.java +++ b/Mage.Sets/src/mage/cards/k/KamiOfJealousThirst.java @@ -60,7 +60,7 @@ enum KamiOfJealousThirstAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void increaseCost(Ability ability, Game game) { int amount = CardsDrawnThisTurnDynamicValue.instance.calculate(game, ability, null); if (amount >= 3) { CardUtil.adjustCost(ability, new ManaCostsImpl<>("{4}{B}"), false); diff --git a/Mage.Sets/src/mage/cards/k/KnollspineInvocation.java b/Mage.Sets/src/mage/cards/k/KnollspineInvocation.java index f63a809d417..32b51409d80 100644 --- a/Mage.Sets/src/mage/cards/k/KnollspineInvocation.java +++ b/Mage.Sets/src/mage/cards/k/KnollspineInvocation.java @@ -1,40 +1,43 @@ package mage.cards.k; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.CostImpl; +import mage.abilities.costs.EarlyTargetCost; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.common.DamageTargetEffect; +import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.ComparisonType; -import mage.constants.Zone; +import mage.constants.Outcome; import mage.filter.FilterCard; -import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.game.Game; +import mage.players.Player; +import mage.target.Target; import mage.target.common.TargetAnyTarget; import mage.target.common.TargetCardInHand; -import mage.util.CardUtil; import java.util.UUID; /** - * @author anonymous + * @author JayDi85 */ public final class KnollspineInvocation extends CardImpl { - private static final FilterCard filter = new FilterCard("a card with mana value X"); + protected static final FilterCard filter = new FilterCard("a card with mana value X"); public KnollspineInvocation(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}{R}"); - // {X}, Discard a card with converted mana cost X: Knollspine Invocation deals X damage to any target. + // {X}, Discard a card with mana value X: This enchantment deals X damage to any target. Ability ability = new SimpleActivatedAbility(new DamageTargetEffect(GetXValue.instance, true), new ManaCostsImpl<>("{X}")); - ability.addCost(new DiscardTargetCost(new TargetCardInHand(filter))); + ability.addCost(new KnollspineInvocationDiscardCost()); ability.addTarget(new TargetAnyTarget()); ability.setCostAdjuster(KnollspineInvocationAdjuster.instance); this.addAbility(ability); @@ -50,22 +53,103 @@ public final class KnollspineInvocation extends CardImpl { } } +class KnollspineInvocationDiscardCost extends CostImpl implements EarlyTargetCost { + + // discard card with early target selection, so {X} mana cost can be setup after choose + + public KnollspineInvocationDiscardCost() { + super(); + this.text = "Discard a card with mana value X"; + } + + public KnollspineInvocationDiscardCost(final KnollspineInvocationDiscardCost cost) { + super(cost); + } + + @Override + public KnollspineInvocationDiscardCost copy() { + return new KnollspineInvocationDiscardCost(this); + } + + @Override + public void chooseTarget(Game game, Ability source, Player controller) { + Target target = new TargetCardInHand().withChooseHint("to discard with mana value for X"); + controller.choose(Outcome.Discard, target, source, game); + addTarget(target); + } + + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + Player controller = game.getPlayer(controllerId); + return controller != null && !controller.getHand().isEmpty(); + } + + @Override + public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { + this.paid = false; + + Player controller = game.getPlayer(controllerId); + if (controller == null) { + return false; + } + + Card card = controller.getHand().get(this.getTargets().getFirstTarget(), game); + if (card == null) { + return false; + } + + this.paid = controller.discard(card, true, source, game); + + return this.paid; + } +} + enum KnollspineInvocationAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { - int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); - for (Cost cost : ability.getCosts()) { - if (!(cost instanceof DiscardTargetCost)) { - continue; - } - DiscardTargetCost discardCost = (DiscardTargetCost) cost; - discardCost.getTargets().clear(); - FilterCard adjustedFilter = new FilterCard("a card with mana value X"); - adjustedFilter.add(new ManaValuePredicate(ComparisonType.EQUAL_TO, xValue)); - discardCost.addTarget(new TargetCardInHand(adjustedFilter)); + public void prepareX(Ability ability, Game game) { + Player controller = game.getPlayer(ability.getControllerId()); + if (controller == null) { return; } + + // make sure early target used + VariableManaCost costX = ability.getManaCostsToPay().stream() + .filter(c -> c instanceof VariableManaCost) + .map(c -> (VariableManaCost) c) + .findFirst() + .orElse(null); + if (costX == null) { + throw new IllegalArgumentException("Wrong code usage: costX lost"); + } + KnollspineInvocationDiscardCost costDiscard = ability.getCosts().stream() + .filter(c -> c instanceof KnollspineInvocationDiscardCost) + .map(c -> (KnollspineInvocationDiscardCost) c) + .findFirst() + .orElse(null); + if (costDiscard == null) { + throw new IllegalArgumentException("Wrong code usage: costDiscard lost"); + } + + if (game.inCheckPlayableState()) { + // possible X + int minManaValue = controller.getHand().getCards(game).stream() + .mapToInt(MageObject::getManaValue) + .min() + .orElse(0); + int maxManaValue = controller.getHand().getCards(game).stream() + .mapToInt(MageObject::getManaValue) + .max() + .orElse(0); + ability.setVariableCostsMinMax(minManaValue, maxManaValue); + } else { + // real X + Card card = controller.getHand().get(costDiscard.getTargets().getFirstTarget(), game); + if (card == null) { + throw new IllegalStateException("Wrong code usage: card to discard lost"); + } + ability.setVariableCostsValue(card.getManaValue()); + } } } diff --git a/Mage.Sets/src/mage/cards/l/LoreseekersStone.java b/Mage.Sets/src/mage/cards/l/LoreseekersStone.java index ce1a04b8533..f059d4cbfc3 100644 --- a/Mage.Sets/src/mage/cards/l/LoreseekersStone.java +++ b/Mage.Sets/src/mage/cards/l/LoreseekersStone.java @@ -48,7 +48,7 @@ enum LoreseekersStoneAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void increaseCost(Ability ability, Game game) { Player player = game.getPlayer(ability.getControllerId()); if (player != null) { CardUtil.increaseCost(ability, player.getHand().size()); diff --git a/Mage.Sets/src/mage/cards/m/MariposaMilitaryBase.java b/Mage.Sets/src/mage/cards/m/MariposaMilitaryBase.java index a655b6d8db5..43b5ffb7483 100644 --- a/Mage.Sets/src/mage/cards/m/MariposaMilitaryBase.java +++ b/Mage.Sets/src/mage/cards/m/MariposaMilitaryBase.java @@ -64,7 +64,7 @@ enum MariposaMilitaryBaseAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, SourceControllerCountersCount.RAD.calculate(game, ability, null)); } } diff --git a/Mage.Sets/src/mage/cards/m/MirrorOfGaladriel.java b/Mage.Sets/src/mage/cards/m/MirrorOfGaladriel.java index d9c02ce3b65..da685a95e24 100644 --- a/Mage.Sets/src/mage/cards/m/MirrorOfGaladriel.java +++ b/Mage.Sets/src/mage/cards/m/MirrorOfGaladriel.java @@ -66,7 +66,7 @@ enum MirrorOfGaladrielAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int value = game.getBattlefield().count( StaticFilters.FILTER_CONTROLLED_CREATURE_LEGENDARY, ability.getControllerId(), ability, game diff --git a/Mage.Sets/src/mage/cards/m/MobilizedDistrict.java b/Mage.Sets/src/mage/cards/m/MobilizedDistrict.java index e6de269bb56..bff7732236f 100644 --- a/Mage.Sets/src/mage/cards/m/MobilizedDistrict.java +++ b/Mage.Sets/src/mage/cards/m/MobilizedDistrict.java @@ -74,7 +74,7 @@ enum MobilizedDistrictAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { int count = cardsCount.calculate(game, ability, null); diff --git a/Mage.Sets/src/mage/cards/n/NahirisWrath.java b/Mage.Sets/src/mage/cards/n/NahirisWrath.java index 015faadbce0..b7bb3ad30f7 100644 --- a/Mage.Sets/src/mage/cards/n/NahirisWrath.java +++ b/Mage.Sets/src/mage/cards/n/NahirisWrath.java @@ -8,6 +8,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.target.common.TargetCreatureOrPlaneswalker; import mage.target.targetadjustment.XTargetsCountAdjuster; @@ -21,8 +22,8 @@ public final class NahirisWrath extends CardImpl { public NahirisWrath(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{R}"); - // As an additional cost to cast Nahiri's Wrath, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); + // As an additional cost to cast this spell, discard X cards. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Nahiri's Wrath deals damage equal to the total converted mana cost of the discarded cards to each of up to X target creatures and/or planeswalkers. Effect effect = new DamageTargetEffect(DiscardCostCardManaValue.instance); diff --git a/Mage.Sets/src/mage/cards/n/NecropolisFiend.java b/Mage.Sets/src/mage/cards/n/NecropolisFiend.java index 2f491e5ac22..930fb06c2b2 100644 --- a/Mage.Sets/src/mage/cards/n/NecropolisFiend.java +++ b/Mage.Sets/src/mage/cards/n/NecropolisFiend.java @@ -6,11 +6,9 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.VariableCost; import mage.abilities.costs.common.ExileFromGraveCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.dynamicvalue.common.SignInversionDynamicValue; @@ -86,16 +84,12 @@ enum NecropolisFiendCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareX(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller == null) { return; } - for (VariableCost variableCost : ability.getManaCostsToPay().getVariableCosts()) { - if (variableCost instanceof VariableManaCost) { - ((VariableManaCost) variableCost).setMaxX(controller.getGraveyard().size()); - } - } + ability.setVariableCostsMinMax(0, controller.getGraveyard().size()); } } diff --git a/Mage.Sets/src/mage/cards/n/NemesisOfMortals.java b/Mage.Sets/src/mage/cards/n/NemesisOfMortals.java index b238a6855e5..e6960031d07 100644 --- a/Mage.Sets/src/mage/cards/n/NemesisOfMortals.java +++ b/Mage.Sets/src/mage/cards/n/NemesisOfMortals.java @@ -63,7 +63,7 @@ enum NemesisOfMortalsAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { CardUtil.reduceCost(ability, controller.getGraveyard().count(StaticFilters.FILTER_CARD_CREATURE, game)); diff --git a/Mage.Sets/src/mage/cards/n/NostalgicDreams.java b/Mage.Sets/src/mage/cards/n/NostalgicDreams.java index 9bff99fef2d..bb453db207e 100644 --- a/Mage.Sets/src/mage/cards/n/NostalgicDreams.java +++ b/Mage.Sets/src/mage/cards/n/NostalgicDreams.java @@ -22,8 +22,8 @@ public final class NostalgicDreams extends CardImpl { public NostalgicDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{G}{G}"); - // As an additional cost to cast Nostalgic Dreams, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); + // As an additional cost to cast this spell, discard X cards. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Return X target cards from your graveyard to your hand. Effect effect = new ReturnFromGraveyardToHandTargetEffect(); diff --git a/Mage.Sets/src/mage/cards/o/OpenTheWay.java b/Mage.Sets/src/mage/cards/o/OpenTheWay.java index 52f49155a0a..c1d7faebbc6 100644 --- a/Mage.Sets/src/mage/cards/o/OpenTheWay.java +++ b/Mage.Sets/src/mage/cards/o/OpenTheWay.java @@ -3,7 +3,6 @@ package mage.cards.o; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.InfoEffect; import mage.cards.*; @@ -28,7 +27,7 @@ public final class OpenTheWay extends CardImpl { this.addAbility(new SimpleStaticAbility( Zone.ALL, new InfoEffect("X can't be greater than the number of players in the game") ).setRuleAtTheTop(true)); - this.getSpellAbility().setCostAdjuster(OpenTheWayAdjuster.instance); + this.getSpellAbility().setCostAdjuster(OpenTheWayCostAdjuster.instance); // Reveal cards from the top of your library until you reveal X land cards. Put those land cards onto the battlefield tapped and the rest on the bottom of your library in a random order. this.getSpellAbility().addEffect(new OpenTheWayEffect()); @@ -44,14 +43,12 @@ public final class OpenTheWay extends CardImpl { } } -enum OpenTheWayAdjuster implements CostAdjuster { +enum OpenTheWayCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { - int playerCount = game.getPlayers().size(); - CardUtil.castStream(ability.getCosts().stream(), VariableManaCost.class) - .forEach(cost -> cost.setMaxX(playerCount)); + public void prepareX(Ability ability, Game game) { + ability.setVariableCostsMinMax(0, game.getState().getPlayersInRange(ability.getControllerId(), game, true).size()); } } diff --git a/Mage.Sets/src/mage/cards/o/OsgirTheReconstructor.java b/Mage.Sets/src/mage/cards/o/OsgirTheReconstructor.java index 5f2208a423b..6d72f04bcfb 100644 --- a/Mage.Sets/src/mage/cards/o/OsgirTheReconstructor.java +++ b/Mage.Sets/src/mage/cards/o/OsgirTheReconstructor.java @@ -33,7 +33,6 @@ import mage.util.CardUtil; import java.util.UUID; /** - * * @author Arketec */ public final class OsgirTheReconstructor extends CardImpl { @@ -81,15 +80,15 @@ enum OsgirTheReconstructorCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); Player controller = game.getPlayer(ability.getControllerId()); if (controller == null) { return; } - FilterCard filter = new FilterArtifactCard("an artifact card with mana value "+xValue+" from your graveyard"); + FilterCard filter = new FilterArtifactCard("an artifact card with mana value " + xValue + " from your graveyard"); filter.add(new ManaValuePredicate(ComparisonType.EQUAL_TO, xValue)); - for (Cost cost: ability.getCosts()) { + for (Cost cost : ability.getCosts()) { if (cost instanceof ExileFromGraveCost) { cost.getTargets().set(0, new TargetCardInYourGraveyard(filter)); } @@ -104,7 +103,7 @@ class OsgirTheReconstructorCreateArtifactTokensEffect extends OneShotEffect { this.staticText = "Create two tokens that are copies of the exiled card."; } - private OsgirTheReconstructorCreateArtifactTokensEffect(final OsgirTheReconstructorCreateArtifactTokensEffect effect) { + private OsgirTheReconstructorCreateArtifactTokensEffect(final OsgirTheReconstructorCreateArtifactTokensEffect effect) { super(effect); } @@ -127,11 +126,11 @@ class OsgirTheReconstructorCreateArtifactTokensEffect extends OneShotEffect { effect.setTargetPointer(new FixedTarget(card.getId(), game.getState().getZoneChangeCounter(card.getId()))); effect.apply(game, source); - return true; + return true; } @Override - public OsgirTheReconstructorCreateArtifactTokensEffect copy() { + public OsgirTheReconstructorCreateArtifactTokensEffect copy() { return new OsgirTheReconstructorCreateArtifactTokensEffect(this); } } diff --git a/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java b/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java index e0e5ac0e8b7..0e2b03b43a2 100644 --- a/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java +++ b/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java @@ -46,7 +46,7 @@ enum PhyrexianPurgeCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void increaseCost(Ability ability, Game game) { int numTargets = ability.getTargets().get(0).getTargets().size(); if (numTargets > 0) { ability.addCost(new PayLifeCost(numTargets * 3)); diff --git a/Mage.Sets/src/mage/cards/p/PlateArmor.java b/Mage.Sets/src/mage/cards/p/PlateArmor.java index 3fe86c05be0..87f413a895d 100644 --- a/Mage.Sets/src/mage/cards/p/PlateArmor.java +++ b/Mage.Sets/src/mage/cards/p/PlateArmor.java @@ -81,7 +81,7 @@ enum PlateArmorAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { int count = equipmentCount.calculate(game, ability, null); diff --git a/Mage.Sets/src/mage/cards/p/PrototypePortal.java b/Mage.Sets/src/mage/cards/p/PrototypePortal.java index 9f2e4ea31f7..d8a92eb098f 100644 --- a/Mage.Sets/src/mage/cards/p/PrototypePortal.java +++ b/Mage.Sets/src/mage/cards/p/PrototypePortal.java @@ -4,11 +4,8 @@ import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.VariableCost; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; @@ -42,10 +39,10 @@ public final class PrototypePortal extends CardImpl { .setAbilityWord(AbilityWord.IMPRINT) ); - // {X}, {tap}: Create a token that's a copy of the exiled card. X is the converted mana cost of that card. + // {X}, {T}: Create a token that's a copy of the exiled card. X is the converted mana cost of that card. Ability ability = new SimpleActivatedAbility(new PrototypePortalCreateTokenEffect(), new ManaCostsImpl<>("{X}")); ability.addCost(new TapSourceCost()); - ability.setCostAdjuster(PrototypePortalAdjuster.instance); + ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance); this.addAbility(ability); } @@ -59,31 +56,6 @@ public final class PrototypePortal extends CardImpl { } } -enum PrototypePortalAdjuster implements CostAdjuster { - instance; - - @Override - public void adjustCosts(Ability ability, Game game) { - Permanent card = game.getPermanent(ability.getSourceId()); - if (card != null) { - if (!card.getImprinted().isEmpty()) { - Card imprinted = game.getCard(card.getImprinted().get(0)); - if (imprinted != null) { - ability.clearManaCostsToPay(); - ability.addManaCostsToPay(new GenericManaCost(imprinted.getManaValue())); - } - } - } - - // no {X} anymore as we already have imprinted the card with defined manacost - for (ManaCost cost : ability.getManaCostsToPay()) { - if (cost instanceof VariableCost) { - cost.setPaid(); - } - } - } -} - class PrototypePortalEffect extends OneShotEffect { PrototypePortalEffect() { diff --git a/Mage.Sets/src/mage/cards/p/Pteramander.java b/Mage.Sets/src/mage/cards/p/Pteramander.java index c4fb5b9fe71..41a6acd784f 100644 --- a/Mage.Sets/src/mage/cards/p/Pteramander.java +++ b/Mage.Sets/src/mage/cards/p/Pteramander.java @@ -70,7 +70,7 @@ enum PteramanderAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int count = cardsCount.calculate(game, ability, null); CardUtil.reduceCost(ability, count); } diff --git a/Mage.Sets/src/mage/cards/q/QuestForTheNecropolis.java b/Mage.Sets/src/mage/cards/q/QuestForTheNecropolis.java index 3283a8631ec..8bcd9da1095 100644 --- a/Mage.Sets/src/mage/cards/q/QuestForTheNecropolis.java +++ b/Mage.Sets/src/mage/cards/q/QuestForTheNecropolis.java @@ -56,7 +56,7 @@ enum QuestForTheNecropolisAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int amount = Optional .ofNullable(ability.getSourcePermanentIfItStillExists(game)) .map(permanent -> permanent.getCounters(game).getCount(CounterType.QUEST)) diff --git a/Mage.Sets/src/mage/cards/r/RazorlashTransmogrant.java b/Mage.Sets/src/mage/cards/r/RazorlashTransmogrant.java index 22cdf9babf4..5ee5549c232 100644 --- a/Mage.Sets/src/mage/cards/r/RazorlashTransmogrant.java +++ b/Mage.Sets/src/mage/cards/r/RazorlashTransmogrant.java @@ -69,7 +69,7 @@ enum RazorlashTransmograntAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (makeMap(game, ability).values().stream().anyMatch(x -> x >= 4)) { CardUtil.reduceCost(ability, 4); } diff --git a/Mage.Sets/src/mage/cards/r/RestlessDreams.java b/Mage.Sets/src/mage/cards/r/RestlessDreams.java index 278a177431d..a81e7d2e06a 100644 --- a/Mage.Sets/src/mage/cards/r/RestlessDreams.java +++ b/Mage.Sets/src/mage/cards/r/RestlessDreams.java @@ -20,7 +20,7 @@ public final class RestlessDreams extends CardImpl { public RestlessDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{B}"); - // As an additional cost to cast Restless Dreams, discard X cards. + // As an additional cost to cast this spell, discard X cards. this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Return X target creature cards from your graveyard to your hand. diff --git a/Mage.Sets/src/mage/cards/s/SanctumOfTranquilLight.java b/Mage.Sets/src/mage/cards/s/SanctumOfTranquilLight.java index 11eb65f1810..ab18f92bf92 100644 --- a/Mage.Sets/src/mage/cards/s/SanctumOfTranquilLight.java +++ b/Mage.Sets/src/mage/cards/s/SanctumOfTranquilLight.java @@ -65,7 +65,7 @@ enum SanctumOfTranquilLightAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { CardUtil.reduceCost(ability, count.calculate(game, ability, null)); diff --git a/Mage.Sets/src/mage/cards/s/ScorchedEarth.java b/Mage.Sets/src/mage/cards/s/ScorchedEarth.java index 2eb04885c64..f9bfd97929a 100644 --- a/Mage.Sets/src/mage/cards/s/ScorchedEarth.java +++ b/Mage.Sets/src/mage/cards/s/ScorchedEarth.java @@ -1,23 +1,15 @@ - package mage.cards.s; -import mage.abilities.Ability; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster; +import mage.abilities.dynamicvalue.common.CardsInControllerHandCount; import mage.abilities.effects.Effect; import mage.abilities.effects.common.DestroyTargetEffect; -import mage.abilities.effects.common.InfoEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Zone; -import mage.filter.common.FilterLandCard; -import mage.game.Game; -import mage.target.common.TargetCardInHand; +import mage.filter.StaticFilters; import mage.target.common.TargetLandPermanent; import mage.target.targetadjustment.XTargetsCountAdjuster; -import mage.util.CardUtil; import java.util.UUID; @@ -29,10 +21,8 @@ public final class ScorchedEarth extends CardImpl { public ScorchedEarth(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{R}"); - // As an additional cost to cast Scorched Earth, discard X land cards. - Ability ability = new SimpleStaticAbility(Zone.ALL, new InfoEffect("as an additional cost to cast this spell, discard X land cards")); - ability.setRuleAtTheTop(true); - this.addAbility(ability); + // As an additional cost to cast this spell, discard X land cards. + DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_LANDS); // Destroy X target lands. Effect effect = new DestroyTargetEffect(); @@ -40,7 +30,7 @@ public final class ScorchedEarth extends CardImpl { this.getSpellAbility().addTarget(new TargetLandPermanent()); this.getSpellAbility().addEffect(effect); this.getSpellAbility().setTargetAdjuster(new XTargetsCountAdjuster()); - this.getSpellAbility().setCostAdjuster(ScorchedEarthCostAdjuster.instance); + this.getSpellAbility().addHint(CardsInControllerHandCount.LANDS.getHint()); } private ScorchedEarth(final ScorchedEarth card) { @@ -51,16 +41,4 @@ public final class ScorchedEarth extends CardImpl { public ScorchedEarth copy() { 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")))); - } - } } \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SeafloorStalker.java b/Mage.Sets/src/mage/cards/s/SeafloorStalker.java index 443d5889fb3..21dcd5a12fa 100644 --- a/Mage.Sets/src/mage/cards/s/SeafloorStalker.java +++ b/Mage.Sets/src/mage/cards/s/SeafloorStalker.java @@ -60,7 +60,7 @@ enum SeafloorStalkerAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { int count = PartyCount.instance.calculate(game, ability, null); diff --git a/Mage.Sets/src/mage/cards/s/SewerCrocodile.java b/Mage.Sets/src/mage/cards/s/SewerCrocodile.java index 71262d8589c..c7d5a2a3320 100644 --- a/Mage.Sets/src/mage/cards/s/SewerCrocodile.java +++ b/Mage.Sets/src/mage/cards/s/SewerCrocodile.java @@ -55,7 +55,7 @@ enum SewerCrocodileAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (DifferentManaValuesInGraveCondition.FIVE.apply(game, ability)) { CardUtil.reduceCost(ability, 3); } diff --git a/Mage.Sets/src/mage/cards/s/SickeningDreams.java b/Mage.Sets/src/mage/cards/s/SickeningDreams.java index 19079354d52..d8fd04a6bae 100644 --- a/Mage.Sets/src/mage/cards/s/SickeningDreams.java +++ b/Mage.Sets/src/mage/cards/s/SickeningDreams.java @@ -7,6 +7,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.filter.common.FilterCreaturePermanent; import java.util.UUID; @@ -19,8 +20,8 @@ public final class SickeningDreams extends CardImpl { public SickeningDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{B}"); - // As an additional cost to cast Sickening Dreams, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); + // As an additional cost to cast this spell, discard X cards. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Sickening Dreams deals X damage to each creature and each player. this.getSpellAbility().addEffect(new DamageEverythingEffect(GetXValue.instance, new FilterCreaturePermanent())); diff --git a/Mage.Sets/src/mage/cards/s/SkeletalScrying.java b/Mage.Sets/src/mage/cards/s/SkeletalScrying.java index f89a8dded84..9ea156bbf0b 100644 --- a/Mage.Sets/src/mage/cards/s/SkeletalScrying.java +++ b/Mage.Sets/src/mage/cards/s/SkeletalScrying.java @@ -61,7 +61,7 @@ enum SkeletalScryingAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); if (xValue > 0) { ability.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard(xValue, xValue, StaticFilters.FILTER_CARDS_FROM_YOUR_GRAVEYARD))); diff --git a/Mage.Sets/src/mage/cards/s/SoulFoundry.java b/Mage.Sets/src/mage/cards/s/SoulFoundry.java index 050a8387362..f8301c8d43a 100644 --- a/Mage.Sets/src/mage/cards/s/SoulFoundry.java +++ b/Mage.Sets/src/mage/cards/s/SoulFoundry.java @@ -3,11 +3,8 @@ package mage.cards.s; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.VariableCost; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect; @@ -45,7 +42,7 @@ public final class SoulFoundry extends CardImpl { // {X}, {T}: Create a token that's a copy of the exiled card. X is the converted mana cost of that card. Ability ability = new SimpleActivatedAbility(new SoulFoundryEffect(), new ManaCostsImpl<>("{X}")); ability.addCost(new TapSourceCost()); - ability.setCostAdjuster(SoulFoundryAdjuster.instance); + ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance); this.addAbility(ability); } @@ -59,31 +56,6 @@ public final class SoulFoundry extends CardImpl { } } -enum SoulFoundryAdjuster implements CostAdjuster { - instance; - - @Override - public void adjustCosts(Ability ability, Game game) { - Permanent sourcePermanent = game.getPermanent(ability.getSourceId()); - if (sourcePermanent != null) { - if (!sourcePermanent.getImprinted().isEmpty()) { - Card imprinted = game.getCard(sourcePermanent.getImprinted().get(0)); - if (imprinted != null) { - ability.clearManaCostsToPay(); - ability.addManaCostsToPay(new GenericManaCost(imprinted.getManaValue())); - } - } - } - - // no {X} anymore as we already have imprinted the card with defined manacost - for (ManaCost cost : ability.getManaCostsToPay()) { - if (cost instanceof VariableCost) { - cost.setPaid(); - } - } - } -} - class SoulFoundryImprintEffect extends OneShotEffect { private static final FilterCard filter = new FilterCard("creature card from your hand"); diff --git a/Mage.Sets/src/mage/cards/t/TamiyosLogbook.java b/Mage.Sets/src/mage/cards/t/TamiyosLogbook.java index ea7780126be..1e50eae2df7 100644 --- a/Mage.Sets/src/mage/cards/t/TamiyosLogbook.java +++ b/Mage.Sets/src/mage/cards/t/TamiyosLogbook.java @@ -61,7 +61,7 @@ enum TamiyosLogbookAdjuster implements CostAdjuster { ); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game); CardUtil.reduceCost(ability, count); } diff --git a/Mage.Sets/src/mage/cards/t/ThroneOfEldraine.java b/Mage.Sets/src/mage/cards/t/ThroneOfEldraine.java index 8c991ad97e2..5b503d04ec9 100644 --- a/Mage.Sets/src/mage/cards/t/ThroneOfEldraine.java +++ b/Mage.Sets/src/mage/cards/t/ThroneOfEldraine.java @@ -170,7 +170,7 @@ enum ThroneOfEldraineAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { ObjectColor color = (ObjectColor) game.getState().getValue(ability.getSourceId() + "_color"); if (color == null) { return; diff --git a/Mage.Sets/src/mage/cards/t/TowashiGuideBot.java b/Mage.Sets/src/mage/cards/t/TowashiGuideBot.java index 051da5a6ccc..15a26a67a7d 100644 --- a/Mage.Sets/src/mage/cards/t/TowashiGuideBot.java +++ b/Mage.Sets/src/mage/cards/t/TowashiGuideBot.java @@ -81,7 +81,7 @@ enum TowashiGuideBotAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, game.getBattlefield().count( filter, ability.getControllerId(), ability, game )); diff --git a/Mage.Sets/src/mage/cards/t/TurbulentDreams.java b/Mage.Sets/src/mage/cards/t/TurbulentDreams.java index eaf810befd6..db125c83c2f 100644 --- a/Mage.Sets/src/mage/cards/t/TurbulentDreams.java +++ b/Mage.Sets/src/mage/cards/t/TurbulentDreams.java @@ -20,7 +20,7 @@ public final class TurbulentDreams extends CardImpl { public TurbulentDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{U}{U}"); - // As an additional cost to cast Turbulent Dreams, discard X cards. + // As an additional cost to cast this spell, discard X cards. this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Return X target nonland permanents to their owners' hands. diff --git a/Mage.Sets/src/mage/cards/u/UrgentNecropsy.java b/Mage.Sets/src/mage/cards/u/UrgentNecropsy.java index 7358884883b..27643226474 100644 --- a/Mage.Sets/src/mage/cards/u/UrgentNecropsy.java +++ b/Mage.Sets/src/mage/cards/u/UrgentNecropsy.java @@ -61,7 +61,7 @@ enum UrgentNecropsyAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { int xValue = ability .getTargets() .stream() diff --git a/Mage.Sets/src/mage/cards/v/VengefulDreams.java b/Mage.Sets/src/mage/cards/v/VengefulDreams.java index a1d4a4c2363..47c70c73315 100644 --- a/Mage.Sets/src/mage/cards/v/VengefulDreams.java +++ b/Mage.Sets/src/mage/cards/v/VengefulDreams.java @@ -21,8 +21,8 @@ public final class VengefulDreams extends CardImpl { public VengefulDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{W}{W}"); - // As an additional cost to cast Vengeful Dreams, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); + // As an additional cost to cast this spell, discard X cards. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Exile X target attacking creatures. Effect effect = new ExileTargetEffect(); diff --git a/Mage.Sets/src/mage/cards/v/VindictiveFlamestoker.java b/Mage.Sets/src/mage/cards/v/VindictiveFlamestoker.java index e52e393a821..75827a2ec5c 100644 --- a/Mage.Sets/src/mage/cards/v/VindictiveFlamestoker.java +++ b/Mage.Sets/src/mage/cards/v/VindictiveFlamestoker.java @@ -65,7 +65,7 @@ enum VindictiveFlamestokerAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int amount = Optional .ofNullable(ability.getSourcePermanentIfItStillExists(game)) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/v/VoldarenEstate.java b/Mage.Sets/src/mage/cards/v/VoldarenEstate.java index 96c78e9276c..84e6d25d367 100644 --- a/Mage.Sets/src/mage/cards/v/VoldarenEstate.java +++ b/Mage.Sets/src/mage/cards/v/VoldarenEstate.java @@ -132,7 +132,7 @@ enum VoldarenEstateCostAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, vampireCount.calculate(game, ability, null)); } } diff --git a/Mage.Sets/src/mage/cards/v/VoodooDoll.java b/Mage.Sets/src/mage/cards/v/VoodooDoll.java index c19eec025f0..1117c54da00 100644 --- a/Mage.Sets/src/mage/cards/v/VoodooDoll.java +++ b/Mage.Sets/src/mage/cards/v/VoodooDoll.java @@ -72,7 +72,7 @@ enum VoodooDollAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { Permanent sourcePermanent = game.getPermanent(ability.getSourceId()); if (sourcePermanent != null) { int pin = sourcePermanent.getCounters(game).getCount(CounterType.PIN); diff --git a/Mage.Sets/src/mage/cards/w/WaytaTrainerProdigy.java b/Mage.Sets/src/mage/cards/w/WaytaTrainerProdigy.java index 780eaf2d97e..c7e7e20a89e 100644 --- a/Mage.Sets/src/mage/cards/w/WaytaTrainerProdigy.java +++ b/Mage.Sets/src/mage/cards/w/WaytaTrainerProdigy.java @@ -101,7 +101,7 @@ enum WaytaTrainerProdigyAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (game.inCheckPlayableState()) { int controllerTargets = 0; //number of possible targets controlled by the ability's controller for (UUID permId : CardUtil.getAllPossibleTargets(ability, game)) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java new file mode 100644 index 00000000000..7b9a1588ea6 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java @@ -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 +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java index f79679ec443..d10b3497e0f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java @@ -13,8 +13,12 @@ import org.mage.test.serverside.base.CardTestPlayerBase; */ public class BeltOfGiantStrengthTest extends CardTestPlayerBase { + /** + * Equipped creature has base power and toughness 10/10. + * Equip {10}. This ability costs {X} less to activate where X is the power of the creature it targets. + */ private static final String belt = "Belt of Giant Strength"; - private static final String gigantosauras = "Gigantosaurus"; + private static final String gigantosauras = "Gigantosaurus"; // 10/10 @Test public void testWithManaAvailable() { diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index e33796e3a0f..2238c6dffeb 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -2927,6 +2927,7 @@ public class TestPlayer implements Player { for (String choice : choices) { if (choice.startsWith("X=")) { int xValue = Integer.parseInt(choice.substring(2)); + assertXMinMaxValue(game, ability, xValue, min, max); choices.remove(choice); return xValue; } @@ -2934,7 +2935,7 @@ public class TestPlayer implements Player { } this.chooseStrictModeFailed("choice", game, getInfo(ability, game) - + "\nMessage: " + message); + + "\nMessage: " + message + prepareXMaxInfo(min, max)); return computerPlayer.announceXMana(min, max, message, game, ability); } @@ -2944,16 +2945,34 @@ public class TestPlayer implements Player { if (!choices.isEmpty()) { if (choices.get(0).startsWith("X=")) { int xValue = Integer.parseInt(choices.get(0).substring(2)); + assertXMinMaxValue(game, ability, xValue, min, max); choices.remove(0); return xValue; } } this.chooseStrictModeFailed("choice", game, getInfo(ability, game) - + "\nMessage: " + message); + + "\nMessage: " + message + prepareXMaxInfo(min, max)); return computerPlayer.announceXCost(min, max, message, game, ability, null); } + private String prepareXMaxInfo(int min, int max) { + if (min == 0 && max == Integer.MAX_VALUE) { + return " (any value)"; + } else { + return String.format(" (from %s to %s)", + min, + (max == Integer.MAX_VALUE) ? "any" : String.valueOf(max) + ); + } + } + + private void assertXMinMaxValue(Game game, Ability source, int xValue, int min, int max) { + if (xValue < min || xValue > max) { + Assert.fail("Found wrong X value = " + xValue + ", for " + CardUtil.getSourceName(game, source) + ", must" + prepareXMaxInfo(min, max)); + } + } + @Override public int getAmount(int min, int max, String message, Game game) { assertAliasSupportInChoices(false); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index c78ea5c0fd7..228e51880ab 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -4,6 +4,9 @@ import mage.MageObject; import mage.Mana; import mage.ObjectColor; import mage.abilities.Ability; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.ContinuousEffectsList; +import mage.abilities.effects.Effect; import mage.cards.Card; import mage.cards.decks.Deck; import mage.cards.decks.DeckCardLists; @@ -29,6 +32,7 @@ import mage.players.Player; import mage.server.game.GameSessionPlayer; import mage.util.CardUtil; import mage.util.ThreadUtils; +import mage.utils.StreamUtils; import mage.utils.SystemUtil; import mage.view.GameView; import org.junit.Assert; @@ -320,6 +324,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } assertAllCommandsUsed(); + + //assertNoDuplicatedEffects(); } protected TestPlayer createNewPlayer(String playerName, RangeOfInfluence rangeOfInfluence) { @@ -1702,6 +1708,57 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } } + /** + * Make sure game state do not contain any duplicated effects, e.g. all effect's durations are fine + */ + private void assertNoDuplicatedEffects() { + // to find bugs like https://github.com/magefree/mage/issues/12932 + + // TODO: simulate full end turn to end all effects and make it default? + + // some effects can generate duplicated effects by design + // example: Tamiyo, Inquisitive Student + x2 attack in TamiyoInquisitiveStudentTest.test_PlusTwo + // +2: Until your next turn, whenever a creature attacks you or a planeswalker you control, it gets -1/-0 until end of turn. + // TODO: add targets and affected objects check to unique key? + + // one effect can be used multiple times by different sources + // so use group key like: effect + ability + source + Map> 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 groupList = groups.getOrDefault(groupKey, null); + if (groupList == null) { + groupList = new ArrayList<>(); + groups.put(groupKey, groupList); + } + groupList.add(effect); + } + } + } + + // analyse + List duplicatedGroups = groups.keySet().stream() + .filter(groupKey -> groups.get(groupKey).size() > 1) + .collect(Collectors.toList()); + if (duplicatedGroups.size() > 0) { + System.out.println("Duplicated effect groups: " + duplicatedGroups.size()); + duplicatedGroups.forEach(groupKey -> { + System.out.println("group " + groupKey + ": "); + groups.get(groupKey).forEach(e -> { + System.out.println(" - " + e.getId() + " - " + e.getDuration() + " - " + e); + }); + }); + Assert.fail("Found duplicated effects: " + duplicatedGroups.size()); + } + } + public void assertActivePlayer(TestPlayer player) { Assert.assertEquals("message", currentGame.getState().getActivePlayerId(), player.getId()); } diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index 96f1a194195..e1bb0a5618b 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -126,7 +126,7 @@ public interface Ability extends Controllable, Serializable { /** * Gets all {@link ManaCosts} associated with this ability. These returned * costs should never be modified as they represent the base costs before - * any modifications. + * any modifications (only cost adjusters can change it, e.g. set min/max values) * * @return All {@link ManaCosts} that must be paid. */ @@ -151,6 +151,17 @@ public interface Ability extends Controllable, Serializable { void addManaCostsToPay(ManaCost manaCost); + /** + * Helper method to setup actual min/max limits of current X costs BEFORE player's X announcement + */ + void setVariableCostsMinMax(int min, int max); + + /** + * Helper method to replace X by direct value BEFORE player's X announcement + * If you need additional target for X then use CostAdjuster + EarlyTargetCost (example: Bargaining Table) + */ + void setVariableCostsValue(int xValue); + /** * Gets a map of the cost tags (set while casting/activating) of this ability, can be null if no tags have been set yet. * Does NOT return the source permanent's tags. @@ -356,7 +367,7 @@ public interface Ability extends Controllable, Serializable { * - for dies triggers - override and use TriggeredAbilityImpl.isInUseableZoneDiesTrigger inside + set setLeavesTheBattlefieldTrigger(true) * * @param sourceObject can be null for static continues effects checking like rules modification (example: Yixlid Jailer) - * @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder) + * @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder) */ boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event); @@ -533,11 +544,25 @@ public interface Ability extends Controllable, Serializable { void adjustTargets(Game game); + /** + * Dynamic X and cost modification, see CostAdjuster for more details on usage + */ Ability setCostAdjuster(CostAdjuster costAdjuster); - CostAdjuster getCostAdjuster(); + /** + * Prepare {X} settings for announce + */ + void adjustX(Game game); - void adjustCosts(Game game); + /** + * Prepare costs (generate due game state or announce) + */ + void adjustCostsPrepare(Game game); + + /** + * Apply additional cost modifications logic/effects + */ + void adjustCostsModify(Game game, CostModificationType costModificationType); List getHints(); diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 114e88343f5..95d13c2d59e 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -291,6 +291,8 @@ public abstract class AbilityImpl implements Ability { // fused or spliced spells contain multiple abilities (e.g. fused, left, right) // optional costs and cost modification must be applied only to the first/main ability + // TODO: need tests with X announced costs, cost modification effects, CostAdjuster, early cost target, etc + // can be bugged due multiple calls (not all code parts below use isMainPartAbility) boolean isMainPartAbility = !CardUtil.isFusedPartAbility(this, game); /* 20220908 - 601.2b @@ -326,6 +328,16 @@ public abstract class AbilityImpl implements Ability { return false; } + // 20241022 - 601.2b + // Choose targets for costs that have to be chosen early + // Not yet included in 601.2b but this is where it will be + handleChooseCostTargets(game, controller); + + // prepare dynamic costs (must be called before any x announce) + if (isMainPartAbility) { + adjustX(game); + } + // 20121001 - 601.2b // If the spell has a variable cost that will be paid as it's being cast (such as an {X} in // its mana cost; see rule 107.3), the player announces the value of that variable. @@ -402,7 +414,7 @@ public abstract class AbilityImpl implements Ability { // Note: ActivatedAbility does include SpellAbility & PlayLandAbility, but those should be able to be canceled too. boolean canCancel = this instanceof ActivatedAbility && controller.isHuman(); if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, canCancel)) { - // was canceled during targer selection + // was canceled during target selection return false; } } @@ -428,7 +440,7 @@ public abstract class AbilityImpl implements Ability { //20101001 - 601.2e if (isMainPartAbility) { - adjustCosts(game); // still needed for CostAdjuster objects (to handle some types of dynamic costs) + // adjustX already called before any announces game.getContinuousEffects().costModification(this, game); } @@ -726,6 +738,11 @@ public abstract class AbilityImpl implements Ability { ((EarlyTargetCost) cost).chooseTarget(game, this, controller); } } + for (ManaCost cost : getManaCostsToPay()) { + if (cost instanceof EarlyTargetCost && cost.getTargets().isEmpty()) { + ((EarlyTargetCost) cost).chooseTarget(game, this, controller); + } + } } /** @@ -764,8 +781,15 @@ public abstract class AbilityImpl implements Ability { if (!variableManaCost.isPaid()) { // should only happen for human players int xValue; if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) { - xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(), - "Announce the value for " + variableManaCost.getText(), game, this); + if (variableManaCost.wasAnnounced()) { + // announce by rules + xValue = variableManaCost.getAmount(); + } else { + // announce by player + xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(), + "Announce the value for " + variableManaCost.getText(), game, this); + } + int amountMana = xValue * variableManaCost.getXInstancesCount(); StringBuilder manaString = threadLocalBuilder.get(); if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) { @@ -1072,6 +1096,60 @@ public abstract class AbilityImpl implements Ability { } } + @Override + public void setVariableCostsMinMax(int min, int max) { + // modify all values (mtg rules allow only one type of X, so min/max must be shared between all X instances) + + // base cost + for (ManaCost cost : getManaCosts()) { + if (cost instanceof MinMaxVariableCost) { + MinMaxVariableCost minMaxCost = (MinMaxVariableCost) cost; + minMaxCost.setMinX(min); + minMaxCost.setMaxX(max); + } + } + + // prepared cost + for (ManaCost cost : getManaCostsToPay()) { + if (cost instanceof MinMaxVariableCost) { + MinMaxVariableCost minMaxCost = (MinMaxVariableCost) cost; + minMaxCost.setMinX(min); + minMaxCost.setMaxX(max); + } + } + } + + @Override + public void setVariableCostsValue(int xValue) { + // only mana cost supported + + // base cost + boolean foundBaseCost = false; + for (ManaCost cost : getManaCosts()) { + if (cost instanceof VariableManaCost) { + foundBaseCost = true; + ((VariableManaCost) cost).setMinX(xValue); + ((VariableManaCost) cost).setMaxX(xValue); + ((VariableManaCost) cost).setAmount(xValue, xValue, false); + } + } + + // prepared cost + boolean foundPreparedCost = false; + for (ManaCost cost : getManaCostsToPay()) { + if (cost instanceof VariableManaCost) { + foundPreparedCost = true; + ((VariableManaCost) cost).setMinX(xValue); + ((VariableManaCost) cost).setMaxX(xValue); + ((VariableManaCost) cost).setAmount(xValue, xValue, false); + } + } + + if (!foundPreparedCost || !foundBaseCost) { + throw new IllegalArgumentException("Wrong code usage: auto-announced X values allowed in mana costs only"); + } + } + @Override public void addEffect(Effect effect) { if (effect != null) { @@ -1676,17 +1754,6 @@ public abstract class AbilityImpl implements Ability { } } - /** - * Dynamic cost modification for ability.
- * Example: if it need stack related info (like real targets) then must - * check two states (game.inCheckPlayableState):
- * 1. In playable state it must check all possible use cases (e.g. allow to - * reduce on any available target and modes)
- * 2. In real cast state it must check current use case (e.g. real selected - * targets and modes) - * - * @param costAdjuster - */ @Override public AbilityImpl setCostAdjuster(CostAdjuster costAdjuster) { this.costAdjuster = costAdjuster; @@ -1694,14 +1761,23 @@ public abstract class AbilityImpl implements Ability { } @Override - public CostAdjuster getCostAdjuster() { - return costAdjuster; + public void adjustX(Game game) { + if (costAdjuster != null) { + costAdjuster.prepareX(this, game); + } } @Override - public void adjustCosts(Game game) { + public void adjustCostsPrepare(Game game) { if (costAdjuster != null) { - costAdjuster.adjustCosts(this, game); + costAdjuster.prepareCost(this, game); + } + } + + @Override + public void adjustCostsModify(Game game, CostModificationType costModificationType) { + if (costAdjuster != null) { + costAdjuster.modifyCost(this, game, costModificationType); } } diff --git a/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java index ea1d77449d0..e522779f851 100644 --- a/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java @@ -1,24 +1,86 @@ package mage.abilities.costs; import mage.abilities.Ability; +import mage.constants.CostModificationType; import mage.game.Game; import java.io.Serializable; /** - * @author TheElk801 + * Dynamic costs implementation to control {X} or other costs, can be used in spells and abilities + *

+ * 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 + *

+ * Calls order by game engine: + * - ... early cost target selection for EarlyTargetCost ... + * - prepareX + * - ... x announce ... + * - prepareCost + * - increaseCost + * - reduceCost + * - ... normal target selection and payment ... + * + * @author TheElk801, JayDi85 */ -@FunctionalInterface public interface CostAdjuster extends Serializable { /** - * Must check playable and real cast states. - * Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState): - * 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes) - * 2. In real cast state it must check current use case (e.g. real selected targets and modes) - * - * @param ability - * @param game + * Prepare {X} costs settings or define auto-announced mana values + *

+ * Usage example: + * - define auto-announced mana value {X} by ability.setVariableCostsValue + * - define possible {X} settings by ability.setVariableCostsMinMax */ - void adjustCosts(Ability ability, Game game); + default void prepareX(Ability ability, Game game) { + // do nothing + } + + /** + * Prepare any dynamic costs + *

+ * 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); + } + } } diff --git a/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java b/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java index 359188f7fdc..23e71faa203 100644 --- a/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java @@ -6,20 +6,11 @@ import mage.players.Player; /** * @author Grath - * Costs which extend this class need to have targets chosen, and those targets must be chosen during 601.2b step. + *

+ * Support 601.2b rules for ealry target choice before X announce and other actions */ -public abstract class EarlyTargetCost extends CostImpl { +public interface EarlyTargetCost { - protected EarlyTargetCost() { - super(); - } + void chooseTarget(Game game, Ability source, Player controller); - protected EarlyTargetCost(final EarlyTargetCost cost) { - super(cost); - } - - @Override - public abstract EarlyTargetCost copy(); - - public abstract void chooseTarget(Game game, Ability source, Player controller); } diff --git a/Mage/src/main/java/mage/abilities/costs/MinMaxVariableCost.java b/Mage/src/main/java/mage/abilities/costs/MinMaxVariableCost.java new file mode 100644 index 00000000000..80b8a1d12ed --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/MinMaxVariableCost.java @@ -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); +} diff --git a/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java index 8fc089b12de..ac863a0e819 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java @@ -10,11 +10,20 @@ import mage.players.Player; import mage.target.common.TargetCardInHand; /** + * Used to setup discard cost WITHOUT {X} mana cost + *

+ * If you have {X} in spell's mana cost then use DiscardXCardsCostAdjuster instead + *

+ * Example: + * - {2}{U}{R} + * - As an additional cost to cast this spell, discard X cards. + * * @author LevelX2 */ public class DiscardXTargetCost extends VariableCostImpl { protected FilterCard filter; + protected boolean isRandom = false; public DiscardXTargetCost(FilterCard filter) { this(filter, false); @@ -30,6 +39,12 @@ public class DiscardXTargetCost extends VariableCostImpl { protected DiscardXTargetCost(final DiscardXTargetCost cost) { super(cost); this.filter = cost.filter; + this.isRandom = cost.isRandom; + } + + public DiscardXTargetCost withRandom() { + this.isRandom = true; + return this; } @Override @@ -49,6 +64,6 @@ public class DiscardXTargetCost extends VariableCostImpl { @Override public Cost getFixedCostsFromAnnouncedValue(int xValue) { TargetCardInHand target = new TargetCardInHand(xValue, filter); - return new DiscardTargetCost(target); + return new DiscardTargetCost(target, this.isRandom); } } diff --git a/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java b/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java index 2b8885471c0..fff0baae6bb 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java @@ -35,10 +35,10 @@ public class PayLoyaltyCost extends CostImpl { int loyaltyCost = amount; - // apply cost modification + // apply dynamic costs and cost modification if (ability instanceof LoyaltyAbility) { LoyaltyAbility copiedAbility = ((LoyaltyAbility) ability).copy(); - copiedAbility.adjustCosts(game); + copiedAbility.adjustX(game); game.getContinuousEffects().costModification(copiedAbility, game); loyaltyCost = 0; for (Cost cost : copiedAbility.getCosts()) { diff --git a/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java b/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java index 42bca28413b..4806e359f85 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java @@ -59,10 +59,10 @@ public class PayVariableLoyaltyCost extends VariableCostImpl { int maxValue = permanent.getCounters(game).getCount(CounterType.LOYALTY); - // apply cost modification + // apply dynamic costs and cost modification if (source instanceof LoyaltyAbility) { LoyaltyAbility copiedAbility = ((LoyaltyAbility) source).copy(); - copiedAbility.adjustCosts(game); + copiedAbility.adjustX(game); game.getContinuousEffects().costModification(copiedAbility, game); for (Cost cost : copiedAbility.getCosts()) { if (cost instanceof PayVariableLoyaltyCost) { diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java index cd018459433..d168aa46850 100644 --- a/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java @@ -13,7 +13,7 @@ public enum CommanderManaValueAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, GreatestCommanderManaValue.instance.calculate(game, ability, null)); } } diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/DiscardXCardsCostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/DiscardXCardsCostAdjuster.java new file mode 100644 index 00000000000..302d4d7e354 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/DiscardXCardsCostAdjuster.java @@ -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 + *

+ * If you don't have {X} then use DiscardXTargetCost instead + *

+ * 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)); + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java index 37cf5e704a2..5037eedb47d 100644 --- a/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java @@ -13,7 +13,7 @@ public enum DomainAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, DomainValue.REGULAR.calculate(game, ability, null)); } } diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java index 742e315efcb..9d9d287eae2 100644 --- a/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java @@ -25,22 +25,29 @@ public class ExileCardsFromHandAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { - if (game.inCheckPlayableState()) { - return; - } + public void reduceCost(Ability ability, Game game) { Player player = game.getPlayer(ability.getControllerId()); if (player == null) { return; } + int cardCount = player.getHand().count(filter, game); - int toExile = cardCount > 0 ? player.getAmount( - 0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game - ) : 0; - if (toExile > 0) { - ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter))); - CardUtil.reduceCost(ability, 2 * toExile); + int reduceCount; + if (game.inCheckPlayableState()) { + // possible + reduceCount = 2 * cardCount; + } else { + // real - need to choose + // TODO: need early target cost instead dialog here + int toExile = cardCount == 0 ? 0 : player.getAmount( + 0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game + ); + reduceCount = 2 * toExile; + if (toExile > 0) { + ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter))); + } } + CardUtil.reduceCost(ability, 2 * reduceCount); } public static final void addAdjusterAndMessage(Card card, FilterCard filter) { diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/ImprintedManaValueXCostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/ImprintedManaValueXCostAdjuster.java new file mode 100644 index 00000000000..76ac9ca2a0a --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/ImprintedManaValueXCostAdjuster.java @@ -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 + *

+ * 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); + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java index cd2406ed5f3..3d4f08fc785 100644 --- a/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java @@ -29,10 +29,8 @@ public enum LegendaryCreatureCostAdjuster implements CostAdjuster { ); @Override - public void adjustCosts(Ability ability, Game game) { - int count = game.getBattlefield().count( - filter, ability.getControllerId(), ability, game - ); + public void reduceCost(Ability ability, Game game) { + int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game); if (count > 0) { CardUtil.reduceCost(ability, count); } diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java index b0d51dca448..f72b7fa0a86 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java @@ -32,7 +32,7 @@ public interface ManaCost extends Cost { ManaOptions getOptions(); /** - * Return all options for paying the mana cost (this) while taking into accoutn if the player can pay life. + * Return all options for paying the mana cost (this) while taking into account if the player can pay life. * Used to correctly highlight (or not) spells with Phyrexian mana depending on if the player can pay life costs. *

* E.g. Tezzeret's Gambit has a cost of {3}{U/P}. diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java index 1fd9bdd5f90..c557d260326 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java @@ -72,7 +72,7 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost { } @Override - public ManaOptions getOptions() { + public final ManaOptions getOptions() { return getOptions(true); } diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java index 118379ccef8..70ba8044ff5 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -439,7 +439,7 @@ public class ManaCostsImpl extends ArrayList implements M this.add(new ColoredManaCost(ColoredManaSymbol.lookup(symbol.charAt(0)))); } else // check X wasn't added before if (modifierForX == 0) { - // count X occurence + // count X occurrence for (String s : symbols) { if (s.equals("X")) { modifierForX++; diff --git a/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java b/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java index d2fa2b16f75..8a8d3e25a00 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java @@ -3,17 +3,19 @@ package mage.abilities.costs.mana; import mage.Mana; import mage.abilities.Ability; import mage.abilities.costs.Cost; -import mage.abilities.costs.VariableCost; +import mage.abilities.costs.MinMaxVariableCost; import mage.abilities.costs.VariableCostType; +import mage.abilities.mana.ManaOptions; import mage.constants.ColoredManaSymbol; import mage.filter.FilterMana; import mage.game.Game; import mage.players.ManaPool; +import mage.util.CardUtil; /** * @author BetaSteward_at_googlemail.com, JayDi85 */ -public final class VariableManaCost extends ManaCostImpl implements VariableCost { +public class VariableManaCost extends ManaCostImpl implements MinMaxVariableCost { // variable mana cost usage on 2019-06-20: // 1. as X value in spell/ability cast (announce X, set VariableManaCost as paid and add generic mana to pay instead) @@ -23,7 +25,7 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost protected int xInstancesCount; // number of {X} instances in cost like {X} or {X}{X} protected int xValue = 0; // final X value after announce and replace events protected int xPay = 0; // final/total need pay after announce and replace events (example: {X}{X}, X=3, xPay = 6) - protected boolean wasAnnounced = false; + protected boolean wasAnnounced = false; // X was announced by player or auto-defined by CostAdjuster protected FilterMana filter; // mana filter that can be used for that cost protected int minX = 0; @@ -59,6 +61,18 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost return 0; } + @Override + public ManaOptions getOptions(boolean canPayLifeCost) { + ManaOptions res = new ManaOptions(); + + // limit mana options for better performance + CardUtil.distributeValues(10, getMinX(), getMaxX()).forEach(value -> { + res.add(Mana.GenericMana(value)); + }); + + return res; + } + @Override public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) { // X mana cost always pays as generic mana @@ -86,6 +100,10 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost return this.isColorlessPaid(xPay); } + public boolean wasAnnounced() { + return this.wasAnnounced; + } + @Override public VariableManaCost getUnpaid() { return this; @@ -122,18 +140,22 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost return this.xInstancesCount; } + @Override public int getMinX() { return minX; } + @Override public void setMinX(int minX) { this.minX = minX; } + @Override public int getMaxX() { return maxX; } + @Override public void setMaxX(int maxX) { this.maxX = maxX; } diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java index ea8cde0572d..a7fe0661157 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java @@ -3,11 +3,27 @@ package mage.abilities.dynamicvalue.common; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.game.Game; import mage.players.Player; public enum CardsInControllerHandCount implements DynamicValue { - instance; + + instance(StaticFilters.FILTER_CARD_CARDS), // TODO: replace usage to ANY + ANY(StaticFilters.FILTER_CARD_CARDS), + CREATURES(StaticFilters.FILTER_CARD_CREATURES), + LANDS(StaticFilters.FILTER_CARD_LANDS); + + FilterCard filter; + ValueHint hint; + + CardsInControllerHandCount(FilterCard filter) { + this.filter = filter; + this.hint = new ValueHint(filter.getMessage() + " in your hand", this); + } @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { @@ -22,16 +38,20 @@ public enum CardsInControllerHandCount implements DynamicValue { @Override public CardsInControllerHandCount copy() { - return CardsInControllerHandCount.instance; + return this; } @Override public String getMessage() { - return "cards in your hand"; + return this.filter.getMessage() + " in your hand"; } @Override public String toString() { return "1"; } + + public Hint getHint() { + return this.hint; + } } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index 90e911e06c8..a60322ba64a 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -688,6 +688,8 @@ public class ContinuousEffects implements Serializable { * the battlefield for * {@link CostModificationEffect cost modification effects} and applies them * if necessary. + *

+ * Warning, don't forget to call ability.adjustX before any cost modifications * * @param abilityToModify * @param game @@ -695,6 +697,10 @@ public class ContinuousEffects implements Serializable { public void costModification(Ability abilityToModify, Game game) { List costEffects = getApplicableCostModificationEffects(game); + // add dynamic costs from X and other places + abilityToModify.adjustCostsPrepare(game); + + abilityToModify.adjustCostsModify(game, CostModificationType.INCREASE_COST); for (CostModificationEffect effect : costEffects) { if (effect.getModificationType() == CostModificationType.INCREASE_COST) { Set abilities = costModificationEffects.getAbility(effect.getId()); @@ -706,6 +712,7 @@ public class ContinuousEffects implements Serializable { } } + abilityToModify.adjustCostsModify(game, CostModificationType.REDUCE_COST); for (CostModificationEffect effect : costEffects) { if (effect.getModificationType() == CostModificationType.REDUCE_COST) { Set abilities = costModificationEffects.getAbility(effect.getId()); @@ -717,6 +724,7 @@ public class ContinuousEffects implements Serializable { } } + abilityToModify.adjustCostsModify(game, CostModificationType.SET_COST); for (CostModificationEffect effect : costEffects) { if (effect.getModificationType() == CostModificationType.SET_COST) { Set abilities = costModificationEffects.getAbility(effect.getId()); diff --git a/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java b/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java index e8dc09bd813..7cacad39c33 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java @@ -62,6 +62,9 @@ public class LookTargetHandChooseDiscardEffect extends OneShotEffect { } TargetCard target = new TargetCardInHand(upTo ? 0 : num, num, filter); if (controller.choose(Outcome.Discard, player.getHand(), target, source, game)) { + // TODO: must fizzle discard effect on not full choice + // - tests: affected (allow to choose and discard 1 instead 2) + // - real game: need to check player.discard(new CardsImpl(target.getTargets()), false, source, game); } return true; diff --git a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java index 94972323c83..72d5cbcbde4 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java @@ -132,6 +132,8 @@ public class SuspendAbility extends SpecialAction { this.addEffect(new SuspendExileEffect(suspend)); this.usesStack = false; if (suspend == Integer.MAX_VALUE) { + // example: Suspend X-{X}{W}{W}. X can't be 0. + // TODO: replace by costAdjuster for shared logic VariableManaCost xCosts = new VariableManaCost(VariableCostType.ALTERNATIVE); xCosts.setMinX(1); this.addCost(xCosts); diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index 4f9244234ba..34fb4333ab9 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -426,6 +426,16 @@ public class StackAbility extends StackObjectImpl implements Ability { // Do nothing } + @Override + public void setVariableCostsMinMax(int min, int max) { + ability.setVariableCostsMinMax(min, max); + } + + @Override + public void setVariableCostsValue(int xValue) { + ability.setVariableCostsValue(xValue); + } + @Override public Map getCostsTagMap() { return ability.getCostsTagMap(); @@ -778,14 +788,23 @@ public class StackAbility extends StackObjectImpl implements Ability { } @Override - public CostAdjuster getCostAdjuster() { - return costAdjuster; + public void adjustX(Game game) { + if (costAdjuster != null) { + costAdjuster.prepareX(this, game); + } } @Override - public void adjustCosts(Game game) { + public void adjustCostsPrepare(Game game) { if (costAdjuster != null) { - costAdjuster.adjustCosts(this, game); + costAdjuster.prepareCost(this, game); + } + } + + @Override + public void adjustCostsModify(Game game, CostModificationType costModificationType) { + if (costAdjuster != null) { + costAdjuster.modifyCost(this, game, costModificationType); } } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index c6c7d14039c..987b47e37f8 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -743,10 +743,14 @@ public interface Player extends MageItem, Copyable { boolean shuffleCardsToLibrary(Card card, Game game, Ability source); - // set the value for X mana spells and abilities + /** + * Set the value for X mana spells and abilities + */ int announceXMana(int min, int max, String message, Game game, Ability ability); - // set the value for non mana X costs + /** + * Set the value for non mana X costs + */ int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variableCost); // TODO: rework to use pair's list of effect + ability instead string's map diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 0132cc7810c..88e36b7c3fb 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -3679,8 +3679,11 @@ public abstract class PlayerImpl implements Player, Serializable { if (!copy.canActivate(playerId, game).canActivate()) { return false; } + + // apply dynamic costs and cost modification + copy.adjustX(game); if (availableMana != null) { - copy.adjustCosts(game); + // TODO: need research, why it look at availableMana here - can delete condition? game.getContinuousEffects().costModification(copy, game); } boolean canBeCastRegularly = true; @@ -3891,7 +3894,8 @@ public abstract class PlayerImpl implements Player, Serializable { copyAbility = ability.copy(); copyAbility.clearManaCostsToPay(); copyAbility.addManaCostsToPay(manaCosts.copy()); - copyAbility.adjustCosts(game); + // apply dynamic costs and cost modification + copyAbility.adjustX(game); game.getContinuousEffects().costModification(copyAbility, game); // reduced all cost @@ -3963,12 +3967,9 @@ public abstract class PlayerImpl implements Player, Serializable { // alternative cost reduce copyAbility = ability.copy(); copyAbility.clearManaCostsToPay(); - // TODO: IDE warning: - // Unchecked assignment: 'mage.abilities.costs.mana.ManaCosts' to - // 'java.util.Collection'. - // Reason: 'manaCosts' has raw type, so result of copy is erased copyAbility.addManaCostsToPay(manaCosts.copy()); - copyAbility.adjustCosts(game); + // apply dynamic costs and cost modification + copyAbility.adjustX(game); game.getContinuousEffects().costModification(copyAbility, game); // reduced all cost diff --git a/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java b/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java index c84fbd949d5..c8ffbea7271 100644 --- a/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java +++ b/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java @@ -25,8 +25,13 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster { @Override public void adjustTargets(Ability ability, Game game) { - Target newTarget = blueprintTarget.copy(); int count = dynamicValue.calculate(game, ability, ability.getEffects().get(0)); + ability.getTargets().clear(); + if (count <= 0) { + return; + } + + Target newTarget = blueprintTarget.copy(); newTarget.setMaxNumberOfTargets(count); Filter filter = newTarget.getFilter(); if (blueprintTarget.getMinNumberOfTargets() != 0) { @@ -35,7 +40,6 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster { } else { newTarget.withTargetName(filter.getMessage() + " (up to " + count + " targets)"); } - ability.getTargets().clear(); ability.addTarget(newTarget); } } diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index ee24d673186..4356de3d90f 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1079,6 +1079,53 @@ public final class CardUtil { .collect(Collectors.toSet()); } + public static Set 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 distributeValues(int count, int min, int max) { + List res = new ArrayList<>(); + if (count <= 0 || min > max) { + return res; + } + + if (min == max) { + res.add(min); + return res; + } + + int range = max - min + 1; + + // low possible amount + if (range <= count) { + for (int i = 0; i < range; i++) { + res.add(min + i); + } + return res; + } + + // big possible amount, so skip some values + double step = (double) (max - min) / (count - 1); + for (int i = 0; i < count; i++) { + res.add(min + (int) Math.round(i * step)); + } + // make sure first and last elements are good + res.set(0, min); + if (res.size() > 1) { + res.set(res.size() - 1, max); + } + + return res; + } + /** * For finding the spell or ability on the stack for "becomes the target" triggers. * @@ -1908,6 +1955,10 @@ public final class CardUtil { return defaultValue; } + public static int getSourceCostsTagX(Game game, Ability source, int defaultValue) { + return getSourceCostsTag(game, source, "X", defaultValue); + } + public static String addCostVerb(String text) { if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) { return text;