diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java index f91c0859d5b..2b6f3500335 100644 --- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java @@ -264,7 +264,6 @@ public class AiCostDecision extends CostDecisionMakerBase { return res.isEmpty() ? null : PaymentDecision.card(res); } - @Override public PaymentDecision visit(CostGainLife cost) { final List oppsThatCanGainLife = Lists.newArrayList(); @@ -282,7 +281,6 @@ public class AiCostDecision extends CostDecisionMakerBase { return PaymentDecision.players(oppsThatCanGainLife); } - @Override public PaymentDecision visit(CostMill cost) { int c = cost.getAbilityAmount(ability); @@ -681,7 +679,6 @@ public class AiCostDecision extends CostDecisionMakerBase { toRemove += removeCounter(table, prefs, CounterEnumType.LORE, c - toRemove); } - // TODO add logic to remove positive counters? if (c > toRemove && cost.counter != null) { // TODO add logic for Ooze Flux, should probably try to make a token as big as possible diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index a295b5fd928..0b8bf14c558 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -41,7 +41,6 @@ import forge.game.zone.ZoneType; import forge.util.MyRandom; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; import java.util.*; @@ -67,16 +66,6 @@ public class ComputerUtilMana { return payManaCost(cost, sa, ai, test, checkPlayable, effect); } - private static void refundMana(List manaSpent, Player ai, SpellAbility sa) { - if (sa.getHostCard() != null) { - sa.getHostCard().setCanCounter(true); - } - for (final Mana m : manaSpent) { - ai.getManaPool().addMana(m); - } - manaSpent.clear(); - } - /** * Return the number of colors used for payment for Converge */ @@ -598,7 +587,7 @@ public class ComputerUtilMana { } // get a mana of this type from floating, bail if none available - final Mana mana = getMana(ai, part, sa, cost.getSourceRestriction(), (byte) -1, cost.getXManaCostPaidByColor()); + final Mana mana = CostPayment.getMana(ai, part, sa, cost.getSourceRestriction(), (byte) -1, cost.getXManaCostPaidByColor()); if (mana != null) { if (ai.getManaPool().tryPayCostWithMana(sa, cost, mana, false)) { manaSpentToPay.add(0, mana); @@ -609,18 +598,16 @@ public class ComputerUtilMana { if (cost.isPaid()) { // refund any mana taken from mana pool when test - refundMana(manaSpentToPay, ai, sa); - - handleOfferingsAI(sa, true, cost.isPaid()); + ManaPool.refundMana(manaSpentToPay, ai, sa); + CostPayment.handleOfferings(sa, true, cost.isPaid()); return manaSources; } // arrange all mana abilities by color produced. final ListMultimap manaAbilityMap = groupSourcesByManaColor(ai, true); if (manaAbilityMap.isEmpty()) { - refundMana(manaSpentToPay, ai, sa); - - handleOfferingsAI(sa, true, cost.isPaid()); + ManaPool.refundMana(manaSpentToPay, ai, sa); + CostPayment.handleOfferings(sa, true, cost.isPaid()); return manaSources; } @@ -666,9 +653,8 @@ public class ComputerUtilMana { Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard())); } - handleOfferingsAI(sa, true, cost.isPaid()); - - refundMana(manaSpentToPay, ai, sa); + CostPayment.handleOfferings(sa, true, cost.isPaid()); + ManaPool.refundMana(manaSpentToPay, ai, sa); return manaSources; } // getManaSourcesToPayCost() @@ -681,7 +667,7 @@ public class ComputerUtilMana { List manaSpentToPay = test ? new ArrayList<>() : sa.getPayingMana(); List paymentList = Lists.newArrayList(); - if (payManaCostFromPool(cost, sa, ai, test, manaSpentToPay)) { + if (ManaPool.payManaCostFromPool(cost, sa, ai, test, manaSpentToPay)) { return true; // paid all from floating mana } @@ -870,7 +856,7 @@ public class ComputerUtilMana { } } - handleOfferingsAI(sa, test, cost.isPaid()); + CostPayment.handleOfferings(sa, test, cost.isPaid()); // if (DEBUG_MANA_PAYMENT) { // System.err.printf("%s > [%s] payment has %s (%s +%d) for (%s) %s:%n\t%s%n%n", @@ -880,7 +866,7 @@ public class ComputerUtilMana { // The cost is still unpaid, so refund the mana and report if (!cost.isPaid()) { - refundMana(manaSpentToPay, ai, sa); + ManaPool.refundMana(manaSpentToPay, ai, sa); if (test) { resetPayment(paymentList); return false; @@ -891,7 +877,7 @@ public class ComputerUtilMana { } if (test) { - refundMana(manaSpentToPay, ai, sa); + ManaPool.refundMana(manaSpentToPay, ai, sa); resetPayment(paymentList); } @@ -929,8 +915,8 @@ public class ComputerUtilMana { final ListMultimap manaAbilityMap = groupSourcesByManaColor(ai, checkPlayable); if (manaAbilityMap.isEmpty()) { // no mana abilities, bailing out - refundMana(manaSpentToPay, ai, sa); - handleOfferingsAI(sa, test, cost.isPaid()); + ManaPool.refundMana(manaSpentToPay, ai, sa); + CostPayment.handleOfferings(sa, test, cost.isPaid()); return null; } if (DEBUG_MANA_PAYMENT) { @@ -968,156 +954,6 @@ public class ComputerUtilMana { return sourcesForShards; } - /** - * Checks if the given mana cost can be paid from floating mana. - * @param cost mana cost to pay for - * @param sa ability to pay for - * @param ai activating player - * @param test actual payment is made if this is false - * @param manaSpentToPay list of mana spent - * @return whether the floating mana is sufficient to pay the cost fully - */ - private static boolean payManaCostFromPool(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, - final boolean test, List manaSpentToPay) { - final boolean hasConverge = sa.getHostCard().hasConverge(); - List unpaidShards = cost.getUnpaidShards(); - Collections.sort(unpaidShards); // most difficult shards must come first - for (ManaCostShard part : unpaidShards) { - if (part != ManaCostShard.X) { - if (cost.isPaid()) { - continue; - } - - // get a mana of this type from floating, bail if none available - final Mana mana = getMana(ai, part, sa, cost.getSourceRestriction(), hasConverge ? cost.getColorsPaid() : -1, cost.getXManaCostPaidByColor()); - if (mana != null) { - if (ai.getManaPool().tryPayCostWithMana(sa, cost, mana, test)) { - manaSpentToPay.add(0, mana); - } - } - } - } - - if (cost.isPaid()) { - // refund any mana taken from mana pool when test - if (test) { - refundMana(manaSpentToPay, ai, sa); - } - handleOfferingsAI(sa, test, cost.isPaid()); - return true; - } - return false; - } - - /** - *

- * getManaFrom. - *

- * - * @param saBeingPaidFor - * a {@link forge.game.spellability.SpellAbility} object. - * @return a {@link forge.game.mana.Mana} object. - */ - private static Mana getMana(final Player ai, final ManaCostShard shard, final SpellAbility saBeingPaidFor, - String restriction, final byte colorsPaid, Map xManaCostPaidByColor) { - final List> weightedOptions = selectManaToPayFor(ai.getManaPool(), shard, - saBeingPaidFor, restriction, colorsPaid, xManaCostPaidByColor); - - // Exclude border case - if (weightedOptions.isEmpty()) { - return null; // There is no matching mana in the pool - } - - // select equal weight possibilities - List manaChoices = new ArrayList<>(); - int bestWeight = Integer.MIN_VALUE; - for (Pair option : weightedOptions) { - int thisWeight = option.getRight(); - Mana thisMana = option.getLeft(); - - if (thisWeight > bestWeight) { - manaChoices.clear(); - bestWeight = thisWeight; - } - - if (thisWeight == bestWeight) { - // add only distinct Mana-s - boolean haveDuplicate = false; - for (Mana m : manaChoices) { - if (m.equals(thisMana)) { - haveDuplicate = true; - break; - } - } - if (!haveDuplicate) { - manaChoices.add(thisMana); - } - } - } - - // got an only one best option? - if (manaChoices.size() == 1) { - return manaChoices.get(0); - } - - // if we are simulating mana payment for the human controller, use the first mana available (and avoid prompting the human player) - if (!(ai.getController() instanceof PlayerControllerAi)) { - return manaChoices.get(0); - } - - // Let them choose then - return ai.getController().chooseManaFromPool(manaChoices); - } - - private static List> selectManaToPayFor(final ManaPool manapool, final ManaCostShard shard, - final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid, Map xManaCostPaidByColor) { - final List> weightedOptions = new ArrayList<>(); - for (final Mana thisMana : manapool) { - if (shard == ManaCostShard.COLORED_X && !ManaCostBeingPaid.canColoredXShardBePaidByColor(MagicColor.toShortString(thisMana.getColor()), xManaCostPaidByColor)) { - continue; - } - - if (!manapool.canPayForShardWithColor(shard, thisMana.getColor())) { - continue; - } - - if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsSpellAndShardRestrictions(saBeingPaidFor, shard, thisMana.getColor())) { - continue; - } - - boolean canPay = manapool.canPayForShardWithColor(shard, thisMana.getColor()); - if (!canPay || (shard.isSnow() && !thisMana.isSnow())) { - continue; - } - - if (StringUtils.isNotBlank(restriction) && !thisMana.getSourceCard().getType().hasStringType(restriction)) { - continue; - } - - int weight = 0; - if (colorsPaid == -1) { - // prefer colorless mana to spend - weight += thisMana.isColorless() ? 5 : 0; - } else { - // get more colors for converge - weight += (thisMana.getColor() | colorsPaid) != colorsPaid ? 5 : 0; - } - - // prefer restricted mana to spend - if (thisMana.isRestricted()) { - weight += 2; - } - - // Spend non-snow mana first - if (!thisMana.isSnow()) { - weight += 1; - } - - weightedOptions.add(Pair.of(thisMana, weight)); - } - return weightedOptions; - } - private static void setExpressColorChoice(final SpellAbility sa, final Player ai, ManaCostBeingPaid cost, ManaCostShard toPay, SpellAbility saPayment) { @@ -1935,26 +1771,6 @@ public class ComputerUtilMana { return res; } - private static void handleOfferingsAI(final SpellAbility sa, boolean test, boolean costIsPaid) { - if (sa.isOffering() && sa.getSacrificedAsOffering() != null) { - final Card offering = sa.getSacrificedAsOffering(); - offering.setUsedToPay(false); - if (costIsPaid && !test) { - sa.getHostCard().getGame().getAction().sacrifice(offering, sa, false, null, null); - } - sa.resetSacrificedAsOffering(); - } - if (sa.isEmerge() && sa.getSacrificedAsEmerge() != null) { - final Card emerge = sa.getSacrificedAsEmerge(); - emerge.setUsedToPay(false); - if (costIsPaid && !test) { - sa.getHostCard().getGame().getAction().sacrifice(emerge, sa, false, null, null); - } - sa.resetSacrificedAsEmerge(); - } - } - - /** * Matches list of creatures to shards in mana cost for convoking. * @param cost cost of convoked ability diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index d6316bbf8fd..96b9f51589e 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -12,6 +12,7 @@ import org.apache.commons.lang3.StringUtils; import forge.StaticData; import forge.card.CardStateName; import forge.card.MagicColor; +import forge.card.mana.ManaAtom; import forge.game.Game; import forge.game.GameEntity; import forge.game.GameObject; @@ -153,7 +154,7 @@ public abstract class GameState { sb.append(TextUtil.concatNoSpace("humanmanapool=", humanManaPool, "\n")); } if (!computerManaPool.isEmpty()) { - sb.append(TextUtil.concatNoSpace("aimanapool=", humanManaPool, "\n")); + sb.append(TextUtil.concatNoSpace("aimanapool=", computerManaPool, "\n")); } sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n")); @@ -710,7 +711,7 @@ public abstract class GameState { private String processManaPool(ManaPool manaPool) { StringBuilder mana = new StringBuilder(); - for (final byte c : MagicColor.WUBRGC) { + for (final byte c : ManaAtom.MANATYPES) { int amount = manaPool.getAmountOfColor(c); for (int i = 0; i < amount; i++) { mana.append(MagicColor.toShortString(c)).append(" "); diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index ce59b560300..711d61877e5 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1081,6 +1081,7 @@ public class PlayerControllerAi extends PlayerController { boolean noManaCost = tgtSA.hasParam("WithoutManaCost"); if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell? Spell spell = (Spell) tgtSA; + // TODO if mandatory AI is only forced to use mana when it's already in the pool if (tgtSA.checkRestrictions(brains.getPlayer()) && (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional)) { if (noManaCost) { return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame()); diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java index e47a2e23804..27f228f3dd5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java @@ -154,6 +154,7 @@ public class DiscardAi extends SpellAbilityAi { } else if (!opp.canDiscardBy(sa, true)) { // e.g. Tamiyo, Collector of Tales continue; } + // TODO when DiscardValid is used and opponent plays with hand revealed, check if he has matching cards if (sa.usesTargeting()) { if (sa.canTarget(opp)) { sa.resetTargets(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java index 87316c9e959..18aae6692eb 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java @@ -325,6 +325,10 @@ public class PlayEffect extends SpellAbilityEffect { tgtSA = tgtSA.copyWithDefinedCost(abCost); } + if (!optional) { + tgtSA.getPayCosts().setMandatory(true); + } + if (sa.hasParam("PlayReduceCost")) { // for Kefnet only can reduce colorless cost String reduce = sa.getParam("PlayReduceCost"); diff --git a/forge-game/src/main/java/forge/game/cost/Cost.java b/forge-game/src/main/java/forge/game/cost/Cost.java index 47ba6109adf..7b194d31300 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -186,6 +186,9 @@ public class Cost implements Serializable { public final boolean isMandatory() { return this.isMandatory; } + public final void setMandatory(boolean b) { + isMandatory = b; + } public final boolean isAbility() { return this.isAbility; diff --git a/forge-game/src/main/java/forge/game/cost/CostPayment.java b/forge-game/src/main/java/forge/game/cost/CostPayment.java index 74e1836d044..ac6b5d6bf3f 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPayment.java +++ b/forge-game/src/main/java/forge/game/cost/CostPayment.java @@ -17,15 +17,25 @@ */ package forge.game.cost; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; + import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import forge.card.MagicColor; +import forge.card.mana.ManaCostShard; import forge.game.Game; import forge.game.card.Card; +import forge.game.mana.Mana; import forge.game.mana.ManaConversionMatrix; +import forge.game.mana.ManaCostBeingPaid; +import forge.game.mana.ManaPool; +import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; @@ -221,4 +231,132 @@ public class CostPayment extends ManaConversionMatrix { } return true; } + + /** + *

+ * getManaFrom. + *

+ * + * @param saBeingPaidFor + * a {@link forge.game.spellability.SpellAbility} object. + * @return a {@link forge.game.mana.Mana} object. + */ + public static Mana getMana(final Player player, final ManaCostShard shard, final SpellAbility saBeingPaidFor, + String restriction, final byte colorsPaid, Map xManaCostPaidByColor) { + final List> weightedOptions = selectManaToPayFor(player.getManaPool(), shard, + saBeingPaidFor, restriction, colorsPaid, xManaCostPaidByColor); + + // Exclude border case + if (weightedOptions.isEmpty()) { + return null; // There is no matching mana in the pool + } + + // select equal weight possibilities + List manaChoices = new ArrayList<>(); + int bestWeight = Integer.MIN_VALUE; + for (Pair option : weightedOptions) { + int thisWeight = option.getRight(); + Mana thisMana = option.getLeft(); + + if (thisWeight > bestWeight) { + manaChoices.clear(); + bestWeight = thisWeight; + } + + if (thisWeight == bestWeight) { + // add only distinct Mana-s + boolean haveDuplicate = false; + for (Mana m : manaChoices) { + if (m.equals(thisMana)) { + haveDuplicate = true; + break; + } + } + if (!haveDuplicate) { + manaChoices.add(thisMana); + } + } + } + + // got an only one best option? + if (manaChoices.size() == 1) { + return manaChoices.get(0); + } + + // if we are simulating mana payment for the human controller, use the first mana available (and avoid prompting the human player) + if (!player.getController().isAI()) { + return manaChoices.get(0); + } + + // Let them choose then + return player.getController().chooseManaFromPool(manaChoices); + } + + private static List> selectManaToPayFor(final ManaPool manapool, final ManaCostShard shard, + final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid, Map xManaCostPaidByColor) { + final List> weightedOptions = new ArrayList<>(); + for (final Mana thisMana : manapool) { + if (shard == ManaCostShard.COLORED_X && !ManaCostBeingPaid.canColoredXShardBePaidByColor(MagicColor.toShortString(thisMana.getColor()), xManaCostPaidByColor)) { + continue; + } + + if (!manapool.canPayForShardWithColor(shard, thisMana.getColor())) { + continue; + } + + if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsSpellAndShardRestrictions(saBeingPaidFor, shard, thisMana.getColor())) { + continue; + } + + boolean canPay = manapool.canPayForShardWithColor(shard, thisMana.getColor()); + if (!canPay || (shard.isSnow() && !thisMana.isSnow())) { + continue; + } + + if (StringUtils.isNotBlank(restriction) && !thisMana.getSourceCard().getType().hasStringType(restriction)) { + continue; + } + + int weight = 0; + if (colorsPaid == -1) { + // prefer colorless mana to spend + weight += thisMana.isColorless() ? 5 : 0; + } else { + // get more colors for converge + weight += (thisMana.getColor() | colorsPaid) != colorsPaid ? 5 : 0; + } + + // prefer restricted mana to spend + if (thisMana.isRestricted()) { + weight += 2; + } + + // Spend non-snow mana first + if (!thisMana.isSnow()) { + weight += 1; + } + + weightedOptions.add(Pair.of(thisMana, weight)); + } + return weightedOptions; + } + + public static void handleOfferings(final SpellAbility sa, boolean test, boolean costIsPaid) { + if (sa.isOffering() && sa.getSacrificedAsOffering() != null) { + final Card offering = sa.getSacrificedAsOffering(); + offering.setUsedToPay(false); + if (costIsPaid && !test) { + sa.getHostCard().getGame().getAction().sacrifice(offering, sa, false, null, null); + } + sa.resetSacrificedAsOffering(); + } + if (sa.isEmerge() && sa.getSacrificedAsEmerge() != null) { + final Card emerge = sa.getSacrificedAsEmerge(); + emerge.setUsedToPay(false); + if (costIsPaid && !test) { + sa.getHostCard().getGame().getAction().sacrifice(emerge, sa, false, null, null); + } + sa.resetSacrificedAsEmerge(); + } + } } diff --git a/forge-game/src/main/java/forge/game/mana/ManaPool.java b/forge-game/src/main/java/forge/game/mana/ManaPool.java index 3fe18c50722..487eb89d224 100644 --- a/forge-game/src/main/java/forge/game/mana/ManaPool.java +++ b/forge-game/src/main/java/forge/game/mana/ManaPool.java @@ -18,6 +18,7 @@ package forge.game.mana; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -33,6 +34,7 @@ import forge.card.mana.ManaCostShard; import forge.game.Game; import forge.game.GlobalRuleChange; import forge.game.ability.AbilityKey; +import forge.game.cost.CostPayment; import forge.game.event.EventValueChangeType; import forge.game.event.GameEventManaPool; import forge.game.event.GameEventZone; @@ -289,6 +291,16 @@ public class ManaPool extends ManaConversionMatrix implements Iterable { return true; } + public static void refundMana(List manaSpent, Player player, SpellAbility sa) { + if (sa.getHostCard() != null) { + sa.getHostCard().setCanCounter(true); + } + for (final Mana m : manaSpent) { + player.getManaPool().addMana(m); + } + manaSpent.clear(); + } + public final void refundManaPaid(final SpellAbility sa) { // Send all mana back to your mana pool, before accounting for it. final List manaPaid = sa.getPayingMana(); @@ -342,6 +354,47 @@ public class ManaPool extends ManaConversionMatrix implements Iterable { return shard.canBePaidWithManaOfColor((byte)0); } + /** + * Checks if the given mana cost can be paid from floating mana. + * @param cost mana cost to pay for + * @param sa ability to pay for + * @param player activating player + * @param test actual payment is made if this is false + * @param manaSpentToPay list of mana spent + * @return whether the floating mana is sufficient to pay the cost fully + */ + public static boolean payManaCostFromPool(final ManaCostBeingPaid cost, final SpellAbility sa, final Player player, + final boolean test, List manaSpentToPay) { + final boolean hasConverge = sa.getHostCard().hasConverge(); + List unpaidShards = cost.getUnpaidShards(); + Collections.sort(unpaidShards); // most difficult shards must come first + for (ManaCostShard part : unpaidShards) { + if (part != ManaCostShard.X) { + if (cost.isPaid()) { + continue; + } + + // get a mana of this type from floating, bail if none available + final Mana mana = CostPayment.getMana(player, part, sa, cost.getSourceRestriction(), hasConverge ? cost.getColorsPaid() : -1, cost.getXManaCostPaidByColor()); + if (mana != null) { + if (player.getManaPool().tryPayCostWithMana(sa, cost, mana, test)) { + manaSpentToPay.add(0, mana); + } + } + } + } + + if (cost.isPaid()) { + // refund any mana taken from mana pool when test + if (test) { + refundMana(manaSpentToPay, player, sa); + } + CostPayment.handleOfferings(sa, test, cost.isPaid()); + return true; + } + return false; + } + @Override public Iterator iterator() { return floatingMana.values().iterator(); diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java index 87d9ae1866c..3a27c05e1d6 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java @@ -44,6 +44,7 @@ public abstract class InputPayMana extends InputSyncronizedBase { protected ManaCostBeingPaid manaCost; protected final SpellAbility saPaidFor; protected boolean effect; + protected boolean mandatory = false; private final boolean wasFloatingMana; private final Queue delaySelectCards = new LinkedList<>(); @@ -406,9 +407,9 @@ public abstract class InputPayMana extends InputSyncronizedBase { protected void updateButtons() { if (supportAutoPay()) { - getController().getGui().updateButtons(getOwner(), Localizer.getInstance().getMessage("lblAuto"), Localizer.getInstance().getMessage("lblCancel"), false, true, false); + getController().getGui().updateButtons(getOwner(), Localizer.getInstance().getMessage("lblAuto"), Localizer.getInstance().getMessage("lblCancel"), false, !mandatory, false); } else { - getController().getGui().updateButtons(getOwner(), "", Localizer.getInstance().getMessage("lblCancel"), false, true, false); + getController().getGui().updateButtons(getOwner(), "", Localizer.getInstance().getMessage("lblCancel"), false, !mandatory, false); } } @@ -430,7 +431,7 @@ public abstract class InputPayMana extends InputSyncronizedBase { canPayManaCost = proc.getResult(); } if (canPayManaCost) { //enabled Auto button if mana cost can be paid - getController().getGui().updateButtons(getOwner(), Localizer.getInstance().getMessage("lblAuto"), Localizer.getInstance().getMessage("lblCancel"), true, true, true); + getController().getGui().updateButtons(getOwner(), Localizer.getInstance().getMessage("lblAuto"), Localizer.getInstance().getMessage("lblCancel"), true, !mandatory, true); } } showMessage(getMessage(), saPaidFor.getView()); @@ -447,8 +448,7 @@ public abstract class InputPayMana extends InputSyncronizedBase { if (isAlreadyPaid()) { done(); stop(); - } - else { + } else { FThreads.invokeInEdtNowOrLater(new Runnable() { @Override public void run() { diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayManaOfCostPayment.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayManaOfCostPayment.java index b18a41c02dc..e8b414817e6 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayManaOfCostPayment.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayManaOfCostPayment.java @@ -1,9 +1,14 @@ package forge.gamemodes.match.input; +import java.util.ArrayList; +import java.util.List; + import forge.card.mana.ManaAtom; import forge.card.mana.ManaCostShard; +import forge.game.mana.Mana; import forge.game.mana.ManaConversionMatrix; import forge.game.mana.ManaCostBeingPaid; +import forge.game.mana.ManaPool; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.localinstance.properties.ForgePreferences; @@ -20,13 +25,20 @@ public class InputPayManaOfCostPayment extends InputPayMana { extraMatrix = matrix; applyMatrix(); + // CR 118.3c forced cast must use pool mana + // TODO this introduces a small risk for illegal payments if the human "wastes" enough mana for abilities like Doubling Cube + if (spellAbility.getPayCosts().isMandatory()) { + List refund = new ArrayList<>(); + mandatory = ManaPool.payManaCostFromPool(new ManaCostBeingPaid(cost), spellAbility, payer, true, refund); + ManaPool.refundMana(refund, payer, spellAbility); + } + // Set Mana cost being paid for SA to be able to reference it later player.pushPaidForSA(saPaidFor); saPaidFor.setManaCostBeingPaid(manaCost); } private static final long serialVersionUID = 3467312982164195091L; - //private int phyLifeToLose = 0; private ManaConversionMatrix extraMatrix; @Override diff --git a/forge-gui/src/main/java/forge/player/HumanCostDecision.java b/forge-gui/src/main/java/forge/player/HumanCostDecision.java index 167f5f218dc..58e3e5e5919 100644 --- a/forge-gui/src/main/java/forge/player/HumanCostDecision.java +++ b/forge-gui/src/main/java/forge/player/HumanCostDecision.java @@ -48,6 +48,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { private final SpellAbility ability; private final Card source; private String orString = null; + private boolean mandatory; public HumanCostDecision(final PlayerControllerHuman controller, final Player p, final SpellAbility sa, final boolean effect, final Card source) { this(controller, p, sa, effect, source, null); @@ -56,6 +57,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { super(p, effect); this.controller = controller; ability = sa; + mandatory = sa.getPayCosts().isMandatory(); this.source = source; this.orString = orString; } @@ -84,7 +86,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { } if (discardType.equals("Hand")) { - if (!controller.confirmPayment(cost, Localizer.getInstance().getMessage("lblDoYouWantDiscardYourHand"), ability)) { + if (!mandatory && !controller.confirmPayment(cost, Localizer.getInstance().getMessage("lblDoYouWantDiscardYourHand"), ability)) { return null; } if (hand.size() > 1 && ability.getActivatingPlayer() != null) { @@ -167,7 +169,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, c, c, hand, ability); inp.setMessage(Localizer.getInstance().getMessage("lblSelectNMoreTargetTypeCardToDiscard", "%d", cost.getDescriptiveType())); - inp.setCancelAllowed(true); + inp.setCancelAllowed(!mandatory); inp.showAndWait(); if (inp.hasCancelled() || inp.getSelected().size() != c) { return null; @@ -258,7 +260,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { if (cost.from == ZoneType.Battlefield || cost.from == ZoneType.Hand) { final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, c, c, list, ability); inp.setMessage(Localizer.getInstance().getMessage("lblExileNCardsFromYourZone", "%d", cost.getFrom().getTranslatedName())); - inp.setCancelAllowed(true); + inp.setCancelAllowed(!mandatory); inp.showAndWait(); return inp.hasCancelled() ? null : PaymentDecision.card(inp.getSelected()); } @@ -379,7 +381,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { origin.add(cost.from); final CardCollection exiled = new CardCollection(); - final List chosen = controller.chooseCardsForZoneChange(ZoneType.Exile, origin, sa, typeList, 0, + final List chosen = controller.chooseCardsForZoneChange(ZoneType.Exile, origin, sa, typeList, mandatory ? nNeeded : 0, nNeeded, null, cost.toString(), null); exiled.addAll(chosen); @@ -539,7 +541,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { public PaymentDecision visit(final CostPayLife cost) { Integer c = cost.getAbilityAmount(ability); - if (ability.getPayCosts().isMandatory()) { + if (mandatory) { return PaymentDecision.number(c); } @@ -671,7 +673,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 1, 1, typeList, ability); inp.setMessage(Localizer.getInstance().getMessage("lblPutNTypeCounterOnTarget", String.valueOf(c), cost.getCounter().getName(), cost.getDescriptiveType())); - inp.setCancelAllowed(true); + inp.setCancelAllowed(!mandatory); inp.showAndWait(); if (inp.hasCancelled()) { @@ -695,7 +697,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { cost.getType().split(";"), player, source, ability); final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, c, c, validCards, ability); - inp.setCancelAllowed(true); + inp.setCancelAllowed(!mandatory); inp.setMessage(Localizer.getInstance().getMessage("lblNTypeCardsToHand", "%d", cost.getDescriptiveType())); inp.showAndWait(); if (inp.hasCancelled()) { @@ -765,7 +767,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { inp = new InputSelectCardsFromList(controller, num, num, hand, ability); inp.setMessage(Localizer.getInstance().getMessage("lblSelectNMoreTypeCardsTpReveal", "%d", cost.getDescriptiveType())); } - inp.setCancelAllowed(true); + inp.setCancelAllowed(!mandatory); inp.showAndWait(); if (inp.hasCancelled()) { return null; @@ -981,7 +983,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { if (cost.payCostFromSource()) { if (source.getController() == ability.getActivatingPlayer() && source.isInPlay()) { - return ability.getPayCosts().isMandatory() || controller.confirmPayment(cost, Localizer.getInstance().getMessage("lblSacrificeCardConfirm", CardTranslation.getTranslatedName(source.getName())), ability) ? PaymentDecision.card(source) : null; + return mandatory || controller.confirmPayment(cost, Localizer.getInstance().getMessage("lblSacrificeCardConfirm", CardTranslation.getTranslatedName(source.getName())), ability) ? PaymentDecision.card(source) : null; } return null; } @@ -1007,7 +1009,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { } final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, c, c, list, ability); inp.setMessage(Localizer.getInstance().getMessage("lblSelectATargetToSacrifice", cost.getDescriptiveType(), "%d")); - inp.setCancelAllowed(true); + inp.setCancelAllowed(!mandatory); inp.showAndWait(); if (inp.hasCancelled()) { return null; @@ -1116,7 +1118,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { } final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, c, c, typeList, ability); - inp.setCancelAllowed(true); + inp.setCancelAllowed(!mandatory); inp.setMessage(Localizer.getInstance().getMessage("lblSelectATargetToTap", cost.getDescriptiveType(), "%d")); inp.showAndWait(); if (inp.hasCancelled()) {