Merge branch 'forcepay' into 'master'

Support mandatory mana payments

Closes #2045

See merge request core-developers/forge!5972
This commit is contained in:
Michael Kamensky
2021-12-25 10:12:39 +00:00
12 changed files with 247 additions and 219 deletions

View File

@@ -264,7 +264,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
return res.isEmpty() ? null : PaymentDecision.card(res);
}
@Override
public PaymentDecision visit(CostGainLife cost) {
final List<Player> 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

View File

@@ -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<Mana> 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<Integer, SpellAbility> 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<Mana> manaSpentToPay = test ? new ArrayList<>() : sa.getPayingMana();
List<SpellAbility> 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<Integer, SpellAbility> 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<Mana> manaSpentToPay) {
final boolean hasConverge = sa.getHostCard().hasConverge();
List<ManaCostShard> 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;
}
/**
* <p>
* getManaFrom.
* </p>
*
* @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<String, Integer> xManaCostPaidByColor) {
final List<Pair<Mana, Integer>> 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<Mana> manaChoices = new ArrayList<>();
int bestWeight = Integer.MIN_VALUE;
for (Pair<Mana, Integer> 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<Pair<Mana, Integer>> selectManaToPayFor(final ManaPool manapool, final ManaCostShard shard,
final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid, Map<String, Integer> xManaCostPaidByColor) {
final List<Pair<Mana, Integer>> 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}
/**
* <p>
* getManaFrom.
* </p>
*
* @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<String, Integer> xManaCostPaidByColor) {
final List<Pair<Mana, Integer>> 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<Mana> manaChoices = new ArrayList<>();
int bestWeight = Integer.MIN_VALUE;
for (Pair<Mana, Integer> 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<Pair<Mana, Integer>> selectManaToPayFor(final ManaPool manapool, final ManaCostShard shard,
final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid, Map<String, Integer> xManaCostPaidByColor) {
final List<Pair<Mana, Integer>> 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();
}
}
}

View File

@@ -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<Mana> {
return true;
}
public static void refundMana(List<Mana> 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<Mana> manaPaid = sa.getPayingMana();
@@ -342,6 +354,47 @@ public class ManaPool extends ManaConversionMatrix implements Iterable<Mana> {
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<Mana> manaSpentToPay) {
final boolean hasConverge = sa.getHostCard().hasConverge();
List<ManaCostShard> 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<Mana> iterator() {
return floatingMana.values().iterator();

View File

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

View File

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

View File

@@ -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<Card> chosen = controller.chooseCardsForZoneChange(ZoneType.Exile, origin, sa, typeList, 0,
final List<Card> 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()) {