diff --git a/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.form b/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.form
new file mode 100644
index 00000000000..a0252c10eb3
--- /dev/null
+++ b/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.form
@@ -0,0 +1,103 @@
+
+
+
diff --git a/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java
new file mode 100644
index 00000000000..550cf72640b
--- /dev/null
+++ b/Mage.Client/src/main/java/mage/client/dialog/PickMultiNumberDialog.java
@@ -0,0 +1,215 @@
+package mage.client.dialog;
+
+import mage.constants.ColoredManaSymbol;
+import org.mage.card.arcane.ManaSymbols;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * @author weirddan455
+ */
+public class PickMultiNumberDialog extends MageDialog {
+
+ private List labelList = null;
+ private List spinnerList = null;
+
+ public PickMultiNumberDialog() {
+ initComponents();
+ this.setModal(true);
+ }
+
+ public void showDialog(List messages, int min, int max, Map options) {
+ this.header.setText((String) options.get("header"));
+ this.header.setHorizontalAlignment(SwingConstants.CENTER);
+ this.setTitle((String) options.get("title"));
+
+ if (labelList != null) {
+ for (JLabel label : labelList) {
+ jPanel1.remove(label);
+ }
+ }
+ if (spinnerList != null) {
+ for (JSpinner spinner : spinnerList) {
+ jPanel1.remove(spinner);
+ }
+ }
+ int size = messages.size();
+ labelList = new ArrayList<>(size);
+ spinnerList = new ArrayList<>(size);
+ jPanel1.setLayout(new GridBagLayout());
+ GridBagConstraints labelC = new GridBagConstraints();
+ GridBagConstraints spinnerC = new GridBagConstraints();
+ for (int i = 0; i < size; i++) {
+ JLabel label = new JLabel();
+
+ // mana mode
+ String manaText = null;
+ String input = messages.get(i);
+ switch (input) {
+ case "W":
+ manaText = ColoredManaSymbol.W.getColorHtmlName();
+ break;
+ case "U":
+ manaText = ColoredManaSymbol.U.getColorHtmlName();
+ break;
+ case "B":
+ manaText = ColoredManaSymbol.B.getColorHtmlName();
+ break;
+ case "R":
+ manaText = ColoredManaSymbol.R.getColorHtmlName();
+ break;
+ case "G":
+ manaText = ColoredManaSymbol.G.getColorHtmlName();
+ break;
+ }
+ if (manaText != null) {
+ label.setText("" + manaText);
+ Image image = ManaSymbols.getSizedManaSymbol(input);
+ if (image != null) {
+ label.setIcon(new ImageIcon(image));
+ }
+ } else {
+ // text mode
+ label.setText("" + input);
+ }
+
+ labelC.weightx = 0.5;
+ labelC.gridx = 0;
+ labelC.gridy = i;
+ jPanel1.add(label, labelC);
+ labelList.add(label);
+
+ JSpinner spinner = new JSpinner();
+ spinner.setModel(new SpinnerNumberModel(0, 0, max, 1));
+ spinnerC.weightx = 0.5;
+ spinnerC.gridx = 1;
+ spinnerC.gridy = i;
+ spinnerC.ipadx = 20;
+ spinner.addChangeListener(e -> {
+ updateControls(min, max);
+ });
+ jPanel1.add(spinner, spinnerC);
+ spinnerList.add(spinner);
+ }
+ this.counterText.setText("0 out of 0");
+ this.counterText.setHorizontalAlignment(SwingConstants.CENTER);
+
+ updateControls(min, max);
+
+ this.pack();
+ this.makeWindowCentered();
+ this.setVisible(true);
+ }
+
+ private void updateControls(int min, int max) {
+ int totalChosenAmount = 0;
+ for (JSpinner jSpinner : spinnerList) {
+ totalChosenAmount += ((Number) jSpinner.getValue()).intValue();
+ }
+ counterText.setText(totalChosenAmount + " out of " + max);
+ chooseButton.setEnabled(totalChosenAmount >= min && totalChosenAmount <= max);
+ }
+
+ public String getMultiAmount() {
+ return spinnerList
+ .stream()
+ .map(spinner -> ((Number) spinner.getValue()).intValue())
+ .map(String::valueOf)
+ .collect(Collectors.joining(" "));
+ }
+
+ /**
+ * This method is called from within the constructor to initialize the form.
+ * WARNING: Do NOT modify this code. The content of this method is always
+ * regenerated by the Form Editor.
+ */
+ @SuppressWarnings("unchecked")
+ // //GEN-BEGIN:initComponents
+ private void initComponents() {
+
+ chooseButton = new javax.swing.JButton();
+ header = new javax.swing.JLabel();
+ counterText = new javax.swing.JLabel();
+ jScrollPane1 = new javax.swing.JScrollPane();
+ jPanel1 = new javax.swing.JPanel();
+
+ chooseButton.setText("Choose");
+ chooseButton.setEnabled(false);
+ chooseButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ chooseButtonActionPerformed(evt);
+ }
+ });
+
+ header.setText("Header");
+
+ counterText.setText("Counter");
+
+ javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
+ jPanel1.setLayout(jPanel1Layout);
+ jPanel1Layout.setHorizontalGroup(
+ jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGap(0, 413, Short.MAX_VALUE)
+ );
+ jPanel1Layout.setVerticalGroup(
+ jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGap(0, 273, Short.MAX_VALUE)
+ );
+
+ jScrollPane1.setViewportView(jPanel1);
+
+ javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
+ getContentPane().setLayout(layout);
+ layout.setHorizontalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addGap(12, 12, 12)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(header, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
+ .addComponent(counterText, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))
+ .addGroup(layout.createSequentialGroup()
+ .addGap(184, 184, 184)
+ .addComponent(chooseButton)
+ .addGap(0, 172, Short.MAX_VALUE))
+ .addGroup(layout.createSequentialGroup()
+ .addContainerGap()
+ .addComponent(jScrollPane1)))
+ .addContainerGap())
+ );
+ layout.setVerticalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
+ .addContainerGap()
+ .addComponent(header)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(counterText)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 276, Short.MAX_VALUE)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(chooseButton)
+ .addContainerGap())
+ );
+ }// //GEN-END:initComponents
+
+ private void chooseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseButtonActionPerformed
+ this.hideDialog();
+ }//GEN-LAST:event_chooseButtonActionPerformed
+
+
+ // Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.JButton chooseButton;
+ private javax.swing.JLabel counterText;
+ private javax.swing.JLabel header;
+ private javax.swing.JPanel jPanel1;
+ private javax.swing.JScrollPane jScrollPane1;
+ // End of variables declaration//GEN-END:variables
+}
diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java
index 937b2d1a816..f33661e1226 100644
--- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java
+++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java
@@ -86,6 +86,7 @@ public final class GamePanel extends javax.swing.JPanel {
GamePane gamePane;
private ReplayTask replayTask;
private final PickNumberDialog pickNumber;
+ private final PickMultiNumberDialog pickMultiNumber;
private JLayeredPane jLayeredPane;
private String chosenHandKey = "You";
private boolean smallMode = false;
@@ -134,6 +135,9 @@ public final class GamePanel extends javax.swing.JPanel {
pickNumber = new PickNumberDialog();
MageFrame.getDesktop().add(pickNumber, JLayeredPane.MODAL_LAYER);
+ pickMultiNumber = new PickMultiNumberDialog();
+ MageFrame.getDesktop().add(pickMultiNumber, JLayeredPane.MODAL_LAYER);
+
this.feedbackPanel.setConnectedChatPanel(this.userChatPanel);
// Override layout (I can't edit generated code)
@@ -238,6 +242,9 @@ public final class GamePanel extends javax.swing.JPanel {
if (pickNumber != null) {
pickNumber.removeDialog();
}
+ if (pickMultiNumber != null) {
+ pickMultiNumber.removeDialog();
+ }
for (CardInfoWindowDialog exileDialog : exiles.values()) {
exileDialog.cleanUp();
exileDialog.removeDialog();
@@ -1617,6 +1624,11 @@ public final class GamePanel extends javax.swing.JPanel {
}
}
+ public void getMultiAmount(List messages, int min, int max, Map options) {
+ pickMultiNumber.showDialog(messages, min, max, options);
+ SessionHandler.sendPlayerString(gameId, pickMultiNumber.getMultiAmount());
+ }
+
public void getChoice(Choice choice, UUID objectId) {
hideAll();
// TODO: remember last choices and search incremental for same events?
diff --git a/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java b/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java
index aeeb3fd273a..8b7e42099dd 100644
--- a/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java
+++ b/Mage.Client/src/main/java/mage/client/remote/CallbackClientImpl.java
@@ -297,6 +297,18 @@ public class CallbackClientImpl implements CallbackClient {
break;
}
+ case GAME_GET_MULTI_AMOUNT: {
+ GameClientMessage message = (GameClientMessage) callback.getData();
+
+ GamePanel panel = MageFrame.getGame(callback.getObjectId());
+ if (panel != null) {
+ appendJsonEvent("GAME_GET_MULTI_AMOUNT", callback.getObjectId(), message);
+
+ panel.getMultiAmount(message.getMessages(), message.getMin(), message.getMax(), message.getOptions());
+ }
+ break;
+ }
+
case GAME_UPDATE: {
GamePanel panel = MageFrame.getGame(callback.getObjectId());
diff --git a/Mage.Common/src/main/java/mage/interfaces/callback/ClientCallbackMethod.java b/Mage.Common/src/main/java/mage/interfaces/callback/ClientCallbackMethod.java
index 95f21ad8376..5fff7629ebd 100644
--- a/Mage.Common/src/main/java/mage/interfaces/callback/ClientCallbackMethod.java
+++ b/Mage.Common/src/main/java/mage/interfaces/callback/ClientCallbackMethod.java
@@ -44,6 +44,7 @@ public enum ClientCallbackMethod {
GAME_PLAY_MANA("gamePlayMana"),
GAME_PLAY_XMANA("gamePlayXMana"),
GAME_GET_AMOUNT("gameSelectAmount"),
+ GAME_GET_MULTI_AMOUNT("gameSelectMultiAmount"),
DRAFT_INIT("draftInit"),
DRAFT_PICK("draftPick"),
DRAFT_UPDATE("draftUpdate");
diff --git a/Mage.Common/src/main/java/mage/view/GameClientMessage.java b/Mage.Common/src/main/java/mage/view/GameClientMessage.java
index bf02f2522df..092079bf7cc 100644
--- a/Mage.Common/src/main/java/mage/view/GameClientMessage.java
+++ b/Mage.Common/src/main/java/mage/view/GameClientMessage.java
@@ -3,6 +3,7 @@
package mage.view;
import java.io.Serializable;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -42,6 +43,8 @@ public class GameClientMessage implements Serializable {
private Map options;
@Expose
private Choice choice;
+ @Expose
+ private List messages;
public GameClientMessage(GameView gameView) {
this.gameView = gameView;
@@ -93,6 +96,13 @@ public class GameClientMessage implements Serializable {
this.message = name;
}
+ public GameClientMessage(List messages, int min, int max, Map options) {
+ this.messages = messages;
+ this.min = min;
+ this.max = max;
+ this.options = options;
+ }
+
public GameClientMessage(Choice choice) {
this.choice = choice;
}
@@ -145,6 +155,10 @@ public class GameClientMessage implements Serializable {
return choice;
}
+ public List getMessages() {
+ return messages;
+ }
+
public String toJson() {
Gson gson = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
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 b8a6c69c3fe..183f47bb76f 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
@@ -2028,6 +2028,7 @@ public class ComputerPlayer extends PlayerImpl implements Player {
}
@Override
+ // TODO: add AI support with outcome and replace random with min/max
public int getAmount(int min, int max, String message, Game game) {
log.debug("getAmount");
if (message.startsWith("Assign damage to ")) {
@@ -2039,6 +2040,29 @@ public class ComputerPlayer extends PlayerImpl implements Player {
return min;
}
+ @Override
+ public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) {
+ log.debug("getMultiAmount");
+
+ int needCount = messages.size();
+ List defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max);
+ if (needCount == 0) {
+ return defaultList;
+ }
+
+ // BAD effect
+ // default list uses minimum possible values, so return it on bad effect
+ // TODO: need something for damage target and mana logic here, current version is useless but better than random
+ if (!outcome.isGood()) {
+ return defaultList;
+ }
+
+ // GOOD effect
+ // values must be stable, so AI must able to simulate it and choose correct actions
+ // fill max values as much as possible
+ return MultiAmountType.prepareMaxValues(needCount, min, max);
+ }
+
@Override
public UUID chooseAttackerOrder(List attackers, Game game) {
//TODO: improve this
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 5b85ac9c261..593f23d2e4b 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
@@ -50,6 +50,7 @@ import org.apache.log4j.Logger;
import java.awt.*;
import java.io.Serializable;
import java.util.*;
+import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Collectors;
@@ -668,7 +669,7 @@ public class HumanPlayer extends PlayerImpl {
options.put("choosable", (Serializable) choosable);
}
- // if nothing to choose then show window (user must see non selectable items and click on any of them)
+ // if nothing to choose then show dialog (user must see non selectable items and click on any of them)
if (required && choosable.isEmpty()) {
required = false;
}
@@ -743,7 +744,7 @@ public class HumanPlayer extends PlayerImpl {
options.put("choosable", (Serializable) choosable);
}
- // if nothing to choose then show window (user must see non selectable items and click on any of them)
+ // if nothing to choose then show dialog (user must see non selectable items and click on any of them)
if (required && choosable.isEmpty()) {
required = false;
}
@@ -781,6 +782,7 @@ public class HumanPlayer extends PlayerImpl {
@Override
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) {
// choose amount
+ // human can choose or un-choose MULTIPLE targets at once
if (gameInCheckPlayableState(game)) {
return true;
}
@@ -790,55 +792,106 @@ public class HumanPlayer extends PlayerImpl {
abilityControllerId = target.getAbilityController();
}
+ int amountTotal = target.getAmountTotal(game, source);
+
+ // Two steps logic:
+ // 1. Select targets
+ // 2. Distribute amount between selected targets
+
+ // 1. Select targets
while (canRespond()) {
Set possibleTargets = target.possibleTargets(source == null ? null : source.getSourceId(), abilityControllerId, game);
boolean required = target.isRequired(source != null ? source.getSourceId() : null, game);
if (possibleTargets.isEmpty()
- || target.getTargets().size() >= target.getNumberOfTargets()) {
+ || target.getSize() >= target.getNumberOfTargets()) {
+ required = false;
+ }
+
+ // selected
+ Map options = getOptions(target, null);
+ java.util.List chosen = target.getTargets();
+ options.put("chosen", (Serializable) chosen);
+ // selectable
+ java.util.List choosable = new ArrayList<>();
+ for (UUID targetId : possibleTargets) {
+ if (target.canTarget(abilityControllerId, targetId, source, game)) {
+ choosable.add(targetId);
+ }
+ }
+ if (!choosable.isEmpty()) {
+ options.put("choosable", (Serializable) choosable);
+ }
+
+ // if nothing to choose then show dialog (user must see non selectable items and click on any of them)
+ if (required && choosable.isEmpty()) {
required = false;
}
updateGameStatePriority("chooseTargetAmount", game);
prepareForResponse(game);
if (!isExecutingMacro()) {
- String selectedNames = target.getTargetedName(game);
- game.fireSelectTargetEvent(playerId, new MessageToClient(target.getMessage()
- + "
Amount remaining: " + target.getAmountRemaining()
- + (selectedNames.isEmpty() ? "" : ", selected: " + selectedNames),
- getRelatedObjectName(source, game)),
- possibleTargets,
- required,
- getOptions(target, null));
+ // target amount uses for damage only, if you see another use case then message must be changed here and on getMultiAmount call
+ String message = String.format("Select targets to distribute %d damage (selected %d)", amountTotal, target.getTargets().size());
+ game.fireSelectTargetEvent(playerId, new MessageToClient(message, getRelatedObjectName(source, game)), possibleTargets, required, options);
}
waitForResponse(game);
UUID responseId = getFixedResponseUUID(game);
if (responseId != null) {
- if (target.canTarget(abilityControllerId, responseId, source, game)) {
- UUID targetId = responseId;
- MageObject targetObject = game.getObject(targetId);
-
- boolean removeMode = target.getTargets().contains(targetId)
- && chooseUse(outcome, "What do you want to do with " + (targetObject != null ? targetObject.getLogName() : " target") + "?", "",
- "Remove from selected", "Add extra amount (remaining " + target.getAmountRemaining() + ")", source, game);
-
- if (removeMode) {
- target.remove(targetId);
- } else {
- if (target.getAmountRemaining() > 0) {
- int amountSelected = getAmount(1, target.getAmountRemaining(), "Select amount", game);
- target.addTarget(targetId, amountSelected, source, game);
- }
- }
-
- return true;
+ if (target.contains(responseId)) {
+ // unselect
+ target.remove(responseId);
+ } else if (possibleTargets.contains(responseId) && target.canTarget(abilityControllerId, responseId, source, game)) {
+ // select
+ target.addTarget(responseId, source, game);
}
} else if (!required) {
- return false;
+ break;
}
}
- return false;
+ // no targets to choose or disconnected
+ List targets = target.getTargets();
+ if (targets.isEmpty()) {
+ return false;
+ }
+
+ // 2. Distribute amount between selected targets
+
+ // prepare targets list with p/t or life stats (cause that's dialog used for damage distribute)
+ List targetNames = new ArrayList<>();
+ for (UUID targetId : targets) {
+ MageObject targetObject = game.getObject(targetId);
+ if (targetObject != null) {
+ targetNames.add(String.format("%s, P/T: %d/%d",
+ targetObject.getIdName(),
+ targetObject.getPower().getValue(),
+ targetObject.getToughness().getValue()
+ ));
+ } else {
+ Player player = game.getPlayer(targetId);
+ if (player != null) {
+ targetNames.add(String.format("%s, life: %d", player.getName(), player.getLife()));
+ } else {
+ targetNames.add("ERROR, unknown target " + targetId.toString());
+ }
+ }
+ }
+
+ // ask and assign new amount
+ List targetValues = getMultiAmount(outcome, targetNames, 1, amountTotal, MultiAmountType.DAMAGE, game);
+ for (int i = 0; i < targetValues.size(); i++) {
+ int newAmount = targetValues.get(i);
+ UUID targetId = targets.get(i);
+ if (newAmount <= 0) {
+ // remove target
+ target.remove(targetId);
+ } else {
+ // set amount
+ target.setTargetAmount(targetId, newAmount, source, game);
+ }
+ }
+ return true;
}
@Override
@@ -1880,6 +1933,52 @@ public class HumanPlayer extends PlayerImpl {
}
}
+ @Override
+ public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) {
+ int needCount = messages.size();
+ List defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max);
+ if (needCount == 0) {
+ return defaultList;
+ }
+
+ if (gameInCheckPlayableState(game)) {
+ return defaultList;
+ }
+
+ List answer = null;
+ while (canRespond()) {
+ updateGameStatePriority("getMultiAmount", game);
+ prepareForResponse(game);
+ if (!isExecutingMacro()) {
+ Map options = new HashMap<>(2);
+ options.put("title", type.getTitle());
+ options.put("header", type.getHeader());
+ game.fireGetMultiAmountEvent(playerId, messages, min, max, options);
+ }
+ waitForResponse(game);
+
+ // waiting correct values only
+ if (response.getString() != null) {
+ answer = MultiAmountType.parseAnswer(response.getString(), needCount, min, max, false);
+ if (MultiAmountType.isGoodValues(answer, needCount, min, max)) {
+ break;
+ } else {
+ // it's not normal: can be cheater or a wrong GUI checks
+ answer = null;
+ logger.error(String.format("GUI return wrong MultiAmountType values: %d %d %d - %s", needCount, min, max, response.getString()));
+ game.informPlayer(this, "Error, you must enter correct values.");
+ }
+ }
+ }
+
+ if (answer != null) {
+ return answer;
+ } else {
+ // something wrong, e.g. player disconnected
+ return defaultList;
+ }
+ }
+
@Override
public void sideboard(Match match, Deck deck) {
match.fireSideboardEvent(playerId, deck);
diff --git a/Mage.Server/src/main/java/mage/server/game/GameController.java b/Mage.Server/src/main/java/mage/server/game/GameController.java
index f6ea6a032ab..8317ea91cd1 100644
--- a/Mage.Server/src/main/java/mage/server/game/GameController.java
+++ b/Mage.Server/src/main/java/mage/server/game/GameController.java
@@ -219,6 +219,9 @@ public class GameController implements GameCallback {
case AMOUNT:
amount(event.getPlayerId(), event.getMessage(), event.getMin(), event.getMax());
break;
+ case MULTI_AMOUNT:
+ multiAmount(event.getPlayerId(), event.getMessages(), event.getMin(), event.getMax(), event.getOptions());
+ break;
case PERSONAL_MESSAGE:
informPersonal(event.getPlayerId(), event.getMessage());
break;
@@ -844,6 +847,10 @@ public class GameController implements GameCallback {
perform(playerId, playerId1 -> getGameSession(playerId1).getAmount(message, min, max));
}
+ private synchronized void multiAmount(UUID playerId, final List messages, final int min, final int max, final Map options) throws MageException {
+ perform(playerId, playerId1 -> getGameSession(playerId1).getMultiAmount(messages, min, max, options));
+ }
+
private void informOthers(UUID playerId) throws MageException {
StringBuilder message = new StringBuilder();
if (game.getStep() != null) {
diff --git a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java
index b7f3c9aae9a..0939c26eb6a 100644
--- a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java
+++ b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java
@@ -114,6 +114,13 @@ public class GameSessionPlayer extends GameSessionWatcher {
}
}
+ public void getMultiAmount(final List messages, final int min, final int max, final Map options) {
+ if (!killed) {
+ userManager.getUser(userId).ifPresent(user
+ -> user.fireCallback(new ClientCallback(ClientCallbackMethod.GAME_GET_MULTI_AMOUNT, game.getId(), new GameClientMessage(messages, min, max, options))));
+ }
+ }
+
public void endGameInfo(Table table) {
if (!killed) {
userManager.getUser(userId).ifPresent(user -> user.fireCallback(new ClientCallback(ClientCallbackMethod.END_GAME_INFO, game.getId(), getGameEndView(playerId, table))));
diff --git a/Mage.Sets/src/mage/cards/b/Boulderfall.java b/Mage.Sets/src/mage/cards/b/Boulderfall.java
index b497229dd8f..b69157265bf 100644
--- a/Mage.Sets/src/mage/cards/b/Boulderfall.java
+++ b/Mage.Sets/src/mage/cards/b/Boulderfall.java
@@ -17,7 +17,6 @@ public final class Boulderfall extends CardImpl {
public Boulderfall(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{6}{R}{R}");
-
// Boulderfall deals 5 damage divided as you choose among any number of target creatures and/or players.
this.getSpellAbility().addEffect(new DamageMultiEffect(5));
this.getSpellAbility().addTarget(new TargetAnyTargetAmount(5));
diff --git a/Mage.Sets/src/mage/cards/b/BurntOffering.java b/Mage.Sets/src/mage/cards/b/BurntOffering.java
index da088573730..d1b4389bbe8 100644
--- a/Mage.Sets/src/mage/cards/b/BurntOffering.java
+++ b/Mage.Sets/src/mage/cards/b/BurntOffering.java
@@ -1,25 +1,15 @@
package mage.cards.b;
-import java.util.LinkedHashSet;
-import java.util.Set;
import java.util.UUID;
-import mage.Mana;
-import mage.abilities.Ability;
-import mage.abilities.costs.Cost;
import mage.abilities.costs.common.SacrificeTargetCost;
-import mage.abilities.effects.Effect;
-import mage.abilities.effects.OneShotEffect;
+import mage.abilities.dynamicvalue.common.SacrificeCostConvertedMana;
+import mage.abilities.effects.mana.AddManaInAnyCombinationEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
-import mage.choices.Choice;
-import mage.choices.ChoiceImpl;
import mage.constants.CardType;
-import mage.constants.Outcome;
+import mage.constants.ColoredManaSymbol;
import static mage.filter.StaticFilters.FILTER_CONTROLLED_CREATURE_SHORT_TEXT;
-import mage.game.Game;
-import mage.game.permanent.Permanent;
-import mage.players.Player;
import mage.target.common.TargetControlledCreaturePermanent;
/**
@@ -34,7 +24,10 @@ public final class BurntOffering extends CardImpl {
//As an additional cost to cast Burnt Offering, sacrifice a creature.
this.getSpellAbility().addCost(new SacrificeTargetCost(new TargetControlledCreaturePermanent(FILTER_CONTROLLED_CREATURE_SHORT_TEXT)));
//Add an amount of {B} and/or {R} equal to the sacrificed creature's converted mana cost.
- this.getSpellAbility().addEffect(new BurntOfferingEffect());
+ SacrificeCostConvertedMana xValue = new SacrificeCostConvertedMana("creature");
+ this.getSpellAbility().addEffect(new AddManaInAnyCombinationEffect(
+ xValue, xValue, ColoredManaSymbol.B, ColoredManaSymbol.R
+ ));
}
private BurntOffering(final BurntOffering card) {
@@ -46,77 +39,3 @@ public final class BurntOffering extends CardImpl {
return new BurntOffering(this);
}
}
-
-class BurntOfferingEffect extends OneShotEffect {
-
- public BurntOfferingEffect() {
- super(Outcome.PutManaInPool);
- this.staticText = "Add X mana in any combination of {B} and/or {R},"
- + " where X is the sacrificed creature's converted mana cost";
- }
-
- public BurntOfferingEffect(final BurntOfferingEffect effect) {
- super(effect);
- }
-
- @Override
- public boolean apply(Game game, Ability source) {
- Player player = game.getPlayer(source.getControllerId());
- if (player != null) {
- Choice manaChoice = new ChoiceImpl();
- Set choices = new LinkedHashSet<>();
- choices.add("Red");
- choices.add("Black");
- manaChoice.setChoices(choices);
- manaChoice.setMessage("Select color of mana to add");
-
- int xValue = getCost(source);
-
- for (int i = 0; i < xValue; i++) {
- Mana mana = new Mana();
- if (!player.choose(Outcome.Benefit, manaChoice, game)) {
- return false;
- }
- if (manaChoice.getChoice() == null) { //Can happen if player leaves game
- return false;
- }
- switch (manaChoice.getChoice()) {
- case "Red":
- mana.increaseRed();
- break;
- case "Black":
- mana.increaseBlack();
- break;
- }
- player.getManaPool().addMana(mana, game, source);
- }
- return true;
- }
- return false;
- }
-
- @Override
- public Effect copy() {
- return new BurntOfferingEffect(this);
- }
-
- /**
- * Helper method to determine the CMC of the sacrificed creature.
- *
- * @param sourceAbility
- * @return
- */
- private int getCost(Ability sourceAbility) {
- for (Cost cost : sourceAbility.getCosts()) {
- if (cost instanceof SacrificeTargetCost) {
- SacrificeTargetCost sacrificeCost = (SacrificeTargetCost) cost;
- int totalCMC = 0;
- for (Permanent permanent : sacrificeCost.getPermanents()) {
- totalCMC += permanent.getConvertedManaCost();
- }
- return totalCMC;
- }
- }
- return 0;
- }
-}
diff --git a/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java b/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java
index a599a9cecfa..a0a72de56bb 100644
--- a/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java
+++ b/Mage.Sets/src/mage/cards/s/SelvalaHeartOfTheWilds.java
@@ -35,7 +35,6 @@ public final class SelvalaHeartOfTheWilds extends CardImpl {
}
private static final String rule = "Whenever another creature enters the battlefield, its controller may draw a card if its power is greater than each other creature's power.";
- private static final String rule2 = "Add X mana in any combination of colors, where X is the greatest power among creatures you control.";
public SelvalaHeartOfTheWilds(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}{G}");
@@ -50,8 +49,8 @@ public final class SelvalaHeartOfTheWilds extends CardImpl {
// {G}, {T}: Add X mana in any combination of colors, where X is the greatest power among creatures you control.
ManaEffect manaEffect = new AddManaInAnyCombinationEffect(
- GreatestPowerAmongControlledCreaturesValue.instance, GreatestPowerAmongControlledCreaturesValue.instance, rule2,
- ColoredManaSymbol.B, ColoredManaSymbol.U, ColoredManaSymbol.R, ColoredManaSymbol.W, ColoredManaSymbol.G);
+ GreatestPowerAmongControlledCreaturesValue.instance, GreatestPowerAmongControlledCreaturesValue.instance,
+ ColoredManaSymbol.W, ColoredManaSymbol.U, ColoredManaSymbol.B, ColoredManaSymbol.R, ColoredManaSymbol.G);
Ability ability = new SimpleManaAbility(Zone.BATTLEFIELD, manaEffect, new ManaCostsImpl("{G}"));
ability.addCost(new TapSourceCost());
this.addAbility(ability);
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dka/AltarOfTheLostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dka/AltarOfTheLostTest.java
index d16a3578172..f9f0fc11ffb 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/single/dka/AltarOfTheLostTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dka/AltarOfTheLostTest.java
@@ -3,6 +3,7 @@ package org.mage.test.cards.single.dka;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
+import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@@ -20,8 +21,11 @@ public class AltarOfTheLostTest extends CardTestPlayerBase {
// Flashback {1}{B}
addCard(Zone.GRAVEYARD, playerA, "Lingering Souls");
- setChoice(playerA, "Black");
- setChoice(playerA, "Black");
+ // Add 2 black mana (mana choice in WUBRG order)
+ setChoice(playerA, "X=0");
+ setChoice(playerA, "X=0");
+ setChoice(playerA, "X=2");
+ setChoice(playerA, TestPlayer.CHOICE_SKIP);
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Flashback {1}{B}");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
@@ -38,8 +42,10 @@ public class AltarOfTheLostTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Altar of the Lost");
addCard(Zone.HAND, playerA, "Lingering Souls");
- setChoice(playerA, "Black");
- setChoice(playerA, "Black");
+ setChoice(playerA, "X=0");
+ setChoice(playerA, "X=0");
+ setChoice(playerA, "X=2");
+ setChoice(playerA, TestPlayer.CHOICE_SKIP);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Lingering Souls");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java
new file mode 100644
index 00000000000..e2661e4c35f
--- /dev/null
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/targets/TargetMultiAmountTest.java
@@ -0,0 +1,268 @@
+package org.mage.test.cards.targets;
+
+import mage.constants.MultiAmountType;
+import mage.constants.PhaseStep;
+import mage.constants.Zone;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mage.test.player.TestPlayer;
+import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author JayDi85
+ */
+
+public class TargetMultiAmountTest extends CardTestPlayerBaseWithAIHelps {
+
+ @Test
+ public void test_DefaultValues() {
+ // default values must be assigned from first to last by minimum values
+ assertDefaultValues("", 0, 0, 0);
+ //
+ assertDefaultValues("0", 1, 0, 0);
+ assertDefaultValues("0 0", 2, 0, 0);
+ assertDefaultValues("0 0 0", 3, 0, 0);
+ //
+ assertDefaultValues("1", 1, 1, 1);
+ assertDefaultValues("1 0", 2, 1, 1);
+ assertDefaultValues("1 0 0", 3, 1, 1);
+ //
+ assertDefaultValues("1", 1, 1, 2);
+ assertDefaultValues("1 0", 2, 1, 2);
+ assertDefaultValues("1 0 0", 3, 1, 2);
+ //
+ assertDefaultValues("2", 1, 2, 2);
+ assertDefaultValues("2 0", 2, 2, 2);
+ assertDefaultValues("2 0 0", 3, 2, 2);
+ //
+ assertDefaultValues("2", 1, 2, 10);
+ assertDefaultValues("2 0", 2, 2, 10);
+ assertDefaultValues("2 0 0", 3, 2, 10);
+ //
+ // performance test
+ assertDefaultValues("2 0 0", 3, 2, Integer.MAX_VALUE);
+ }
+
+ private void assertDefaultValues(String need, int count, int min, int max) {
+ List defaultValues = MultiAmountType.prepareDefaltValues(count, min, max);
+ String current = defaultValues
+ .stream()
+ .map(String::valueOf)
+ .collect(Collectors.joining(" "));
+ Assert.assertEquals("default values", need, current);
+ Assert.assertTrue("default values must be good", MultiAmountType.isGoodValues(defaultValues, count, min, max));
+ }
+
+ @Test
+ public void test_MaxValues() {
+ // max possible values must be assigned from first to last by max possible values
+ assertMaxValues("", 0, 0, 0);
+ //
+ assertMaxValues("0", 1, 0, 0);
+ assertMaxValues("0 0", 2, 0, 0);
+ assertMaxValues("0 0 0", 3, 0, 0);
+ //
+ assertMaxValues("1", 1, 1, 1);
+ assertMaxValues("1 0", 2, 1, 1);
+ assertMaxValues("1 0 0", 3, 1, 1);
+ //
+ assertMaxValues("2", 1, 1, 2);
+ assertMaxValues("1 1", 2, 1, 2);
+ assertMaxValues("1 1 0", 3, 1, 2);
+ //
+ assertMaxValues("2", 1, 2, 2);
+ assertMaxValues("1 1", 2, 2, 2);
+ assertMaxValues("1 1 0", 3, 2, 2);
+ //
+ assertMaxValues("10", 1, 2, 10);
+ assertMaxValues("5 5", 2, 2, 10);
+ assertMaxValues("4 3 3", 3, 2, 10);
+ //
+ assertMaxValues("1 1 1 1 1 0 0 0 0 0", 10, 2, 5);
+ //
+ // performance test
+ assertMaxValues(String.valueOf(Integer.MAX_VALUE), 1, 2, Integer.MAX_VALUE);
+ int part = Integer.MAX_VALUE / 3;
+ String need = String.format("%d %d %d", part + 1, part, part);
+ assertMaxValues(need, 3, 2, Integer.MAX_VALUE);
+ }
+
+ private void assertMaxValues(String need, int count, int min, int max) {
+ List maxValues = MultiAmountType.prepareMaxValues(count, min, max);
+ String current = maxValues
+ .stream()
+ .map(String::valueOf)
+ .collect(Collectors.joining(" "));
+ Assert.assertEquals("max values", need, current);
+ Assert.assertTrue("max values must be good", MultiAmountType.isGoodValues(maxValues, count, min, max));
+ }
+
+ @Test
+ public void test_GoodValues() {
+ // good values are checking in test_DefaultValues, it's an additional
+ List list = MultiAmountType.prepareDefaltValues(3, 0, 0);
+
+ // count (0, 0, 0)
+ Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 0, 0, 0));
+ Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 1, 0, 0));
+ Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 2, 0, 0));
+ Assert.assertTrue("count", MultiAmountType.isGoodValues(list, 3, 0, 0));
+ Assert.assertFalse("count", MultiAmountType.isGoodValues(list, 4, 0, 0));
+
+ // min (0, 1, 1)
+ list.set(0, 0);
+ list.set(1, 1);
+ list.set(2, 1);
+ Assert.assertTrue("min", MultiAmountType.isGoodValues(list, 3, 0, 100));
+ Assert.assertTrue("min", MultiAmountType.isGoodValues(list, 3, 1, 100));
+ Assert.assertTrue("min", MultiAmountType.isGoodValues(list, 3, 2, 100));
+ Assert.assertFalse("min", MultiAmountType.isGoodValues(list, 3, 3, 100));
+ Assert.assertFalse("min", MultiAmountType.isGoodValues(list, 3, 4, 100));
+
+ // max (0, 1, 1)
+ list.set(0, 0);
+ list.set(1, 1);
+ list.set(2, 1);
+ Assert.assertFalse("max", MultiAmountType.isGoodValues(list, 3, 0, 0));
+ Assert.assertFalse("max", MultiAmountType.isGoodValues(list, 3, 0, 1));
+ Assert.assertTrue("max", MultiAmountType.isGoodValues(list, 3, 0, 2));
+ Assert.assertTrue("max", MultiAmountType.isGoodValues(list, 3, 0, 3));
+ Assert.assertTrue("max", MultiAmountType.isGoodValues(list, 3, 0, 4));
+ }
+
+ @Test
+ public void test_Parse() {
+ // parse must use correct values on good data or default values on broken data
+
+ // simple parse without data check
+ assertParse("", 3, 1, 3, "", false);
+ assertParse("1", 3, 1, 3, "1", false);
+ assertParse("0 0 0", 3, 1, 3, "0 0 0", false);
+ assertParse("1 0 3", 3, 1, 3, "1 0 3", false);
+ assertParse("0 5 0 6", 3, 1, 3, "1,text 5 4. 6", false);
+
+ // parse with data check - good data
+ assertParse("1 0 2", 3, 0, 3, "1 0 2", true);
+
+ // parse with data check - broken data (must return defalt - 1 0 0)
+ assertParse("1 0 0", 3, 1, 3, "", true);
+ assertParse("1 0 0", 3, 1, 3, "1", true);
+ assertParse("1 0 0", 3, 1, 3, "0 0 0", true);
+ assertParse("1 0 0", 3, 1, 3, "1 0 3", true);
+ assertParse("1 0 0", 3, 1, 3, "1,text 4.", true);
+ }
+
+ private void assertParse(String need, int count, int min, int max, String answerToParse, Boolean returnDefaultOnError) {
+ List parsedValues = MultiAmountType.parseAnswer(answerToParse, count, min, max, returnDefaultOnError);
+ String current = parsedValues
+ .stream()
+ .map(String::valueOf)
+ .collect(Collectors.joining(" "));
+ Assert.assertEquals("parsed values", need, current);
+ if (returnDefaultOnError) {
+ Assert.assertTrue("parsed values must be good", MultiAmountType.isGoodValues(parsedValues, count, min, max));
+ }
+ }
+
+ @Test
+ public void test_Mana_Manamorphose_Manual() {
+ removeAllCardsFromHand(playerA);
+
+ // Add two mana in any combination of colors.
+ // Draw a card.
+ addCard(Zone.HAND, playerA, "Manamorphose", 2); // {1}{R/G}
+ addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 * 2);
+
+ // cast and select {B}{B}
+ // one type of choices: wubrg order
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Manamorphose");
+ setChoiceAmount(playerA, 0); // W
+ setChoiceAmount(playerA, 0); // U
+ setChoiceAmount(playerA, 2); // B
+ setChoice(playerA, TestPlayer.CHOICE_SKIP); // skip RG
+ waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
+ checkManaPool("after first cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "B", 2);
+
+ // cast and select {R}{G}
+ // another type of choices
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Manamorphose");
+ setChoiceAmount(playerA, 0, 0, 0, 1, 1);
+ waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
+ checkManaPool("after second cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 1);
+ checkManaPool("after second cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "G", 1);
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+ assertAllCommandsUsed();
+ }
+
+ @Test
+ public void test_Mana_Manamorphose_AI() {
+ removeAllCardsFromHand(playerA);
+
+ // Add two mana in any combination of colors.
+ // Draw a card.
+ addCard(Zone.HAND, playerA, "Manamorphose", 1); // {1}{R/G}
+ addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
+
+ // cast, but AI must select first manas (WU)
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Manamorphose");
+ aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA);
+ waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
+ checkManaPool("after ai cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "W", 1);
+ checkManaPool("after ai cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "U", 1);
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+ assertAllCommandsUsed();
+ }
+
+ @Test
+ public void test_Damage_Boulderfall_Manual() {
+ // Boulderfall deals 5 damage divided as you choose among any number of target creatures and/or players.
+ addCard(Zone.HAND, playerA, "Boulderfall", 1); // {6}{R}{R}
+ addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8);
+ //
+ addCard(Zone.BATTLEFIELD, playerA, "Kitesail Corsair@bear", 3); // 2/1
+
+ // distribute 4x + 1x damage (kill two creatures)
+ castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Boulderfall");
+ addTargetAmount(playerA, "@bear.1", 4);
+ addTargetAmount(playerA, "@bear.2", 1);
+ waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
+ checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "@bear.1", 0);
+ checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "@bear.2", 0);
+ checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "@bear.3", 1);
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+ assertAllCommandsUsed();
+ }
+
+ @Test
+ public void test_Damage_Boulderfall_AI() {
+ // AI don't use multi amount dialogs like human (it's just one target amount choose/simulation)
+
+ // Boulderfall deals 5 damage divided as you choose among any number of target creatures and/or players.
+ addCard(Zone.HAND, playerA, "Boulderfall", 1); // {6}{R}{R}
+ addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8);
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Kitesail Corsair", 6); // 2/1
+
+ // play card and distribute damage by game simulations for best score (kills 5x creatures)
+ aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA);
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.END_TURN);
+ execute();
+ assertAllCommandsUsed();
+
+ assertGraveyardCount(playerB, "Kitesail Corsair", 5);
+ }
+}
\ No newline at end of file
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 c04aad5af6d..8cf09ff0cee 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
@@ -2667,6 +2667,51 @@ public class TestPlayer implements Player {
return computerPlayer.getAmount(min, max, message, game);
}
+ @Override
+ public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) {
+ assertAliasSupportInChoices(false);
+
+ int needCount = messages.size();
+ List defaultList = MultiAmountType.prepareDefaltValues(needCount, min, max);
+ if (needCount == 0) {
+ return defaultList;
+ }
+
+ List answer = new ArrayList<>(defaultList);
+ if (!choices.isEmpty()) {
+ // must fill all possible choices or skip it
+ for (int i = 0; i < messages.size(); i++) {
+ if (!choices.isEmpty()) {
+ // normal choice
+ if (choices.get(0).startsWith("X=")) {
+ answer.set(i, Integer.parseInt(choices.get(0).substring(2)));
+ choices.remove(0);
+ continue;
+ }
+ // skip
+ if (choices.get(0).equals(CHOICE_SKIP)) {
+ choices.remove(0);
+ break;
+ }
+ }
+ Assert.fail(String.format("Missing choice in multi amount: %s (pos %d - %s)", type.getHeader(), i + 1, messages.get(i)));
+ }
+
+ // extra check
+ if (!MultiAmountType.isGoodValues(answer, needCount, min, max)) {
+ Assert.fail("Wrong choices in multi amount: " + answer
+ .stream()
+ .map(String::valueOf)
+ .collect(Collectors.joining(",")));
+ }
+
+ return answer;
+ }
+
+ this.chooseStrictModeFailed("choice", game, "Multi amount: " + type.getHeader());
+ return computerPlayer.getMultiAmount(outcome, messages, min, max, type, game);
+ }
+
@Override
public void addAbility(Ability ability) {
computerPlayer.addAbility(ability);
@@ -3873,7 +3918,7 @@ public class TestPlayer implements Player {
Assert.assertNotEquals("chooseTargetAmount needs non zero amount remaining", 0, target.getAmountRemaining());
- assertAliasSupportInTargets(false);
+ assertAliasSupportInTargets(true);
if (!targets.isEmpty()) {
// skip targets
@@ -3894,6 +3939,8 @@ public class TestPlayer implements Player {
String targetName = choiceSettings[0];
int targetAmount = Integer.parseInt(choiceSettings[1].substring("X=".length()));
+ checkTargetDefinitionMarksSupport(target, targetName, "=");
+
// player target support
if (targetName.startsWith("targetPlayer=")) {
targetName = targetName.substring(targetName.indexOf("targetPlayer=") + "targetPlayer=".length());
@@ -3905,10 +3952,21 @@ public class TestPlayer implements Player {
if (target.getAmountRemaining() > 0) {
for (UUID possibleTarget : target.possibleTargets(source.getSourceId(), source.getControllerId(), game)) {
+ boolean foundTarget = false;
+
+ // permanent
MageObject objectPermanent = game.getObject(possibleTarget);
+ if (objectPermanent != null && hasObjectTargetNameOrAlias(objectPermanent, targetName)) {
+ foundTarget = true;
+ }
+
+ // player
Player objectPlayer = game.getPlayer(possibleTarget);
- String objectName = objectPermanent != null ? objectPermanent.getName() : objectPlayer.getName();
- if (objectName.equals(targetName)) {
+ if (!foundTarget && objectPlayer != null && objectPlayer.getName().equals(targetName)) {
+ foundTarget = true;
+ }
+
+ if (foundTarget) {
if (!target.getTargets().contains(possibleTarget) && target.canTarget(possibleTarget, source, game)) {
// can select
target.addTarget(possibleTarget, targetAmount, source, game);
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 4f448d6a714..e737c809dd8 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
@@ -1908,6 +1908,20 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
}
}
+ /**
+ * Setup amount choices.
+ *
+ * Multi amount choices uses WUBRG order (so use 1,2,3,4,5 values list)
+ *
+ * @param player
+ * @param amountList
+ */
+ public void setChoiceAmount(TestPlayer player, int... amountList) {
+ for (int amount : amountList) {
+ setChoice(player, "X=" + amount);
+ }
+ }
+
/**
* Set the modes for modal spells
*
diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java
index 4ddfb7f8245..dc7f44f0d7e 100644
--- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java
+++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java
@@ -974,6 +974,11 @@ public class PlayerStub implements Player {
return 0;
}
+ @Override
+ public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) {
+ return null;
+ }
+
@Override
public void sideboard(Match match, Deck deck) {
diff --git a/Mage/src/main/java/mage/abilities/effects/mana/AddConditionalManaOfAnyColorEffect.java b/Mage/src/main/java/mage/abilities/effects/mana/AddConditionalManaOfAnyColorEffect.java
index 6e2bb565063..82060231a4a 100644
--- a/Mage/src/main/java/mage/abilities/effects/mana/AddConditionalManaOfAnyColorEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/mana/AddConditionalManaOfAnyColorEffect.java
@@ -9,6 +9,7 @@ import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.mana.builder.ConditionalManaBuilder;
import mage.choices.ChoiceColor;
+import mage.constants.MultiAmountType;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
@@ -87,29 +88,26 @@ public class AddConditionalManaOfAnyColorEffect extends ManaEffect {
if (game != null) {
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
- ConditionalMana mana = null;
int value = amount.calculate(game, source, this);
- ChoiceColor choice = new ChoiceColor(true);
- for (int i = 0; i < value; i++) {
- if (choice.getChoice() == null) {
+ if (value > 0) {
+ if (oneChoice || value == 1) {
+ ChoiceColor choice = new ChoiceColor(true);
controller.choose(outcome, choice, game);
- }
- if (choice.getChoice() == null) {
- return null;
- }
- if (oneChoice) {
- mana = new ConditionalMana(manaBuilder.setMana(choice.getMana(value), source, game).build());
- break;
- } else {
- if (mana == null) {
- mana = new ConditionalMana(manaBuilder.setMana(choice.getMana(1), source, game).build());
- } else {
- mana.add(choice.getMana(1));
+ if (choice.getChoice() == null) {
+ return null;
}
- choice.clearChoice();
+ return new ConditionalMana(manaBuilder.setMana(choice.getMana(value), source, game).build());
}
+ List manaStrings = new ArrayList<>(5);
+ manaStrings.add("W");
+ manaStrings.add("U");
+ manaStrings.add("B");
+ manaStrings.add("R");
+ manaStrings.add("G");
+ List choices = controller.getMultiAmount(this.outcome, manaStrings, 0, value, MultiAmountType.MANA, game);
+ Mana mana = new Mana(choices.get(0), choices.get(1), choices.get(2), choices.get(3), choices.get(4), 0, 0, 0);
+ return new ConditionalMana(manaBuilder.setMana(mana, source, game).build());
}
- return mana;
}
}
return new Mana();
diff --git a/Mage/src/main/java/mage/abilities/effects/mana/AddManaInAnyCombinationEffect.java b/Mage/src/main/java/mage/abilities/effects/mana/AddManaInAnyCombinationEffect.java
index d98ecfaec07..30490589f2c 100644
--- a/Mage/src/main/java/mage/abilities/effects/mana/AddManaInAnyCombinationEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/mana/AddManaInAnyCombinationEffect.java
@@ -1,11 +1,5 @@
package mage.abilities.effects.mana;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
@@ -13,10 +7,14 @@ import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.mana.ManaOptions;
import mage.constants.ColoredManaSymbol;
import mage.constants.ManaType;
+import mage.constants.MultiAmountType;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
+import java.util.*;
+import java.util.stream.Collectors;
+
/**
* @author LevelX2
*/
@@ -27,7 +25,13 @@ public class AddManaInAnyCombinationEffect extends ManaEffect {
private final DynamicValue netAmount;
public AddManaInAnyCombinationEffect(int amount) {
- this(StaticValue.get(amount), StaticValue.get(amount), ColoredManaSymbol.B, ColoredManaSymbol.U, ColoredManaSymbol.R, ColoredManaSymbol.W, ColoredManaSymbol.G);
+ this(StaticValue.get(amount), StaticValue.get(amount),
+ ColoredManaSymbol.W,
+ ColoredManaSymbol.U,
+ ColoredManaSymbol.B,
+ ColoredManaSymbol.R,
+ ColoredManaSymbol.G
+ );
}
public AddManaInAnyCombinationEffect(int amount, ColoredManaSymbol... coloredManaSymbols) {
@@ -106,26 +110,20 @@ public class AddManaInAnyCombinationEffect extends ManaEffect {
public Mana produceMana(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player != null) {
+ int size = manaSymbols.size();
Mana mana = new Mana();
- int amountOfManaLeft = amount.calculate(game, source, this);
- int maxAmount = amountOfManaLeft;
-
- while (amountOfManaLeft > 0 && player.canRespond()) {
- for (ColoredManaSymbol coloredManaSymbol : manaSymbols) {
- int number = player.getAmount(0, amountOfManaLeft, "Distribute mana by color (" + mana.count()
- + " of " + maxAmount + " done). How many " + coloredManaSymbol.getColorHtmlName() + " mana to add (enter 0 to pass to next color)?", game);
- if (number > 0) {
- for (int i = 0; i < number; i++) {
- mana.add(new Mana(coloredManaSymbol));
- }
- amountOfManaLeft -= number;
- }
- if (amountOfManaLeft == 0) {
- break;
- }
+ List manaStrings = new ArrayList<>(size);
+ for (ColoredManaSymbol coloredManaSymbol : manaSymbols) {
+ manaStrings.add(coloredManaSymbol.toString());
+ }
+ List manaList = player.getMultiAmount(this.outcome, manaStrings, 0, amount.calculate(game, source, this), MultiAmountType.MANA, game);
+ for (int i = 0; i < size; i++) {
+ ColoredManaSymbol coloredManaSymbol = manaSymbols.get(i);
+ int amount = manaList.get(i);
+ for (int j = 0; j < amount; j++) {
+ mana.add(new Mana(coloredManaSymbol));
}
}
-
return mana;
}
return null;
@@ -134,7 +132,7 @@ public class AddManaInAnyCombinationEffect extends ManaEffect {
@Override
public Set getProducableManaTypes(Game game, Ability source) {
Set manaTypes = new HashSet<>();
- for(ColoredManaSymbol coloredManaSymbol: manaSymbols) {
+ for (ColoredManaSymbol coloredManaSymbol : manaSymbols) {
if (coloredManaSymbol.equals(ColoredManaSymbol.B)) {
manaTypes.add(ManaType.BLACK);
}
@@ -156,7 +154,8 @@ public class AddManaInAnyCombinationEffect extends ManaEffect {
private String setText() {
StringBuilder sb = new StringBuilder("Add ");
- sb.append(CardUtil.numberToText(amount.toString()));
+ String amountString = CardUtil.numberToText(amount.toString());
+ sb.append(amountString);
sb.append(" mana in any combination of ");
if (manaSymbols.size() == 5) {
sb.append("colors");
@@ -170,6 +169,10 @@ public class AddManaInAnyCombinationEffect extends ManaEffect {
sb.append('{').append(coloredManaSymbol.toString()).append('}');
}
}
+ if (amountString.equals("X")) {
+ sb.append(", where X is ");
+ sb.append(amount.getMessage());
+ }
return sb.toString();
}
}
diff --git a/Mage/src/main/java/mage/abilities/effects/mana/DynamicManaEffect.java b/Mage/src/main/java/mage/abilities/effects/mana/DynamicManaEffect.java
index e0d80d1d8d8..d14c147c51e 100644
--- a/Mage/src/main/java/mage/abilities/effects/mana/DynamicManaEffect.java
+++ b/Mage/src/main/java/mage/abilities/effects/mana/DynamicManaEffect.java
@@ -5,7 +5,7 @@ import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.choices.ChoiceColor;
-import mage.constants.Outcome;
+import mage.constants.MultiAmountType;
import mage.game.Game;
import mage.players.Player;
@@ -143,18 +143,23 @@ public class DynamicManaEffect extends ManaEffect {
computedMana.setColorless(count);
} else if (baseMana.getAny() > 0) {
Player controller = game.getPlayer(source.getControllerId());
- if (controller != null) {
- ChoiceColor choiceColor = new ChoiceColor(true);
- for (int i = 0; i < count; i++) {
- if (!choiceColor.isChosen()) {
- if (!controller.choose(Outcome.Benefit, choiceColor, game)) {
- return computedMana;
- }
- }
- choiceColor.increaseMana(computedMana);
- if (!oneChoice) {
- choiceColor.clearChoice();
+ if (controller != null && count > 0) {
+ if (oneChoice || count == 1) {
+ ChoiceColor choice = new ChoiceColor(true);
+ controller.choose(outcome, choice, game);
+ if (choice.getChoice() == null) {
+ return computedMana;
}
+ computedMana.add(choice.getMana(count));
+ } else {
+ List manaStrings = new ArrayList<>(5);
+ manaStrings.add("W");
+ manaStrings.add("U");
+ manaStrings.add("B");
+ manaStrings.add("R");
+ manaStrings.add("G");
+ List choices = controller.getMultiAmount(this.outcome, manaStrings, 0, count, MultiAmountType.MANA, game);
+ computedMana.add(new Mana(choices.get(0), choices.get(1), choices.get(2), choices.get(3), choices.get(4), 0, 0, 0));
}
}
} else {
diff --git a/Mage/src/main/java/mage/choices/Choices.java b/Mage/src/main/java/mage/choices/Choices.java
index 9bb5c0910db..c934575142f 100644
--- a/Mage/src/main/java/mage/choices/Choices.java
+++ b/Mage/src/main/java/mage/choices/Choices.java
@@ -56,6 +56,9 @@ public class Choices extends ArrayList {
public boolean choose(Game game, Ability source) {
if (this.size() > 0) {
Player player = game.getPlayer(source.getControllerId());
+ if (player == null) {
+ return false;
+ }
while (!isChosen()) {
Choice choice = this.getUnchosen().get(0);
if (!player.choose(outcome, choice, game)) {
diff --git a/Mage/src/main/java/mage/constants/MultiAmountType.java b/Mage/src/main/java/mage/constants/MultiAmountType.java
new file mode 100644
index 00000000000..feed33809d5
--- /dev/null
+++ b/Mage/src/main/java/mage/constants/MultiAmountType.java
@@ -0,0 +1,108 @@
+package mage.constants;
+
+import com.google.common.collect.Iterables;
+import mage.util.CardUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.IntStream;
+
+public enum MultiAmountType {
+
+ MANA("Add mana", "Distribute mana among colors"),
+ DAMAGE("Assign damage", "Assign damage among targets");
+
+ private final String title;
+ private final String header;
+
+ MultiAmountType(String title, String header) {
+ this.title = title;
+ this.header = header;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getHeader() {
+ return header;
+ }
+
+ public static List prepareDefaltValues(int count, int min, int max) {
+ // default values must be assigned from first to last by minimum values
+ List res = new ArrayList<>();
+ if (count == 0) {
+ return res;
+ }
+
+ // fill list
+ IntStream.range(0, count).forEach(i -> res.add(0));
+
+ // fill values
+ if (min > 0) {
+ res.set(0, min);
+ }
+
+ return res;
+ }
+
+ public static List prepareMaxValues(int count, int min, int max) {
+ // fill max values as much as possible
+ List res = new ArrayList<>();
+ if (count == 0) {
+ return res;
+ }
+
+ // fill list
+ int startingValue = max / count;
+ IntStream.range(0, count).forEach(i -> res.add(startingValue));
+
+ // fill values
+ // from first to last until complete
+ List resIndexes = new ArrayList<>(res.size());
+ IntStream.range(0, res.size()).forEach(resIndexes::add);
+ // infinite iterator (no needs with starting values use, but can be used later for different logic)
+ Iterator resIterator = Iterables.cycle(resIndexes).iterator();
+ int valueInc = 1;
+ int valueTotal = startingValue * count;
+ while (valueTotal < max) {
+ int currentIndex = resIterator.next();
+ int newValue = CardUtil.overflowInc(res.get(currentIndex), valueInc);
+ res.set(currentIndex, newValue);
+ valueTotal += valueInc;
+ }
+
+ return res;
+ }
+
+ public static boolean isGoodValues(List values, int count, int min, int max) {
+ if (values.size() != count) {
+ return false;
+ }
+
+ int currentSum = values.stream().mapToInt(i -> i).sum();
+ return currentSum >= min && currentSum <= max;
+ }
+
+ public static List parseAnswer(String answerToParse, int count, int min, int max, boolean returnDefaultOnError) {
+ List res = new ArrayList<>();
+
+ // parse
+ String normalValue = answerToParse.trim();
+ if (!normalValue.isEmpty()) {
+ Arrays.stream(normalValue.split(" ")).forEach(valueStr -> {
+ res.add(CardUtil.parseIntWithDefault(valueStr, 0));
+ });
+ }
+
+ // data check
+ if (returnDefaultOnError && !isGoodValues(res, count, min, max)) {
+ // on broken data - return default
+ return prepareDefaltValues(count, min, max);
+ }
+
+ return res;
+ }
+}
diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java
index 9012a1ad3b6..b7a65102004 100644
--- a/Mage/src/main/java/mage/game/Game.java
+++ b/Mage/src/main/java/mage/game/Game.java
@@ -277,6 +277,8 @@ public interface Game extends MageItem, Serializable {
void fireGetAmountEvent(UUID playerId, String message, int min, int max);
+ void fireGetMultiAmountEvent(UUID playerId, List messages, int min, int max, Map options);
+
void fireChoosePileEvent(UUID playerId, String message, List extends Card> pile1, List extends Card> pile2);
void fireInformEvent(String message);
diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java
index 2fcfc747ab1..45d66188f02 100644
--- a/Mage/src/main/java/mage/game/GameImpl.java
+++ b/Mage/src/main/java/mage/game/GameImpl.java
@@ -2515,6 +2515,14 @@ public abstract class GameImpl implements Game, Serializable {
playerQueryEventSource.amount(playerId, message, min, max);
}
+ @Override
+ public void fireGetMultiAmountEvent(UUID playerId, List messages, int min, int max, Map options) {
+ if (simulation) {
+ return;
+ }
+ playerQueryEventSource.multiAmount(playerId, messages, min, max, options);
+ }
+
@Override
public void fireChooseChoiceEvent(UUID playerId, Choice choice) {
if (simulation) {
diff --git a/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java b/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java
index 4aaf42d0338..4b906c6811b 100644
--- a/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java
+++ b/Mage/src/main/java/mage/game/events/PlayerQueryEvent.java
@@ -35,6 +35,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
PLAY_MANA,
PLAY_X_MANA,
AMOUNT,
+ MULTI_AMOUNT,
PICK_CARD,
CONSTRUCT,
CHOOSE_PILE,
@@ -58,8 +59,11 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
private List extends Card> pile1;
private List extends Card> pile2;
private Choice choice;
+ private List messages;
- private PlayerQueryEvent(UUID playerId, String message, List extends Ability> abilities, Set choices, Set targets, Cards cards, QueryType queryType, int min, int max, boolean required, Map options) {
+ private PlayerQueryEvent(UUID playerId, String message, List extends Ability> abilities, Set choices,
+ Set targets, Cards cards, QueryType queryType, int min, int max, boolean required,
+ Map options, List messages) {
super(playerId);
this.queryType = queryType;
this.message = message;
@@ -77,6 +81,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
this.options = options;
}
this.options.put("queryType", queryType);
+ this.messages = messages;
}
private PlayerQueryEvent(UUID playerId, String message, List booster, QueryType queryType, int time) {
@@ -143,7 +148,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
}
options.put("originalId", source.getOriginalId());
}
- return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.ASK, 0, 0, false, options);
+ return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.ASK, 0, 0, false, options, null);
}
public static PlayerQueryEvent chooseAbilityEvent(UUID playerId, String message, String objectName, List extends ActivatedAbility> choices) {
@@ -152,7 +157,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
nameAsSet = new HashSet<>();
nameAsSet.add(objectName);
}
- return new PlayerQueryEvent(playerId, message, choices, nameAsSet, null, null, QueryType.CHOOSE_ABILITY, 0, 0, false, null);
+ return new PlayerQueryEvent(playerId, message, choices, nameAsSet, null, null, QueryType.CHOOSE_ABILITY, 0, 0, false, null, null);
}
public static PlayerQueryEvent choosePileEvent(UUID playerId, String message, List extends Card> pile1, List extends Card> pile2) {
@@ -168,19 +173,19 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
}
public static PlayerQueryEvent targetEvent(UUID playerId, String message, Set targets, boolean required) {
- return new PlayerQueryEvent(playerId, message, null, null, targets, null, QueryType.PICK_TARGET, 0, 0, required, null);
+ return new PlayerQueryEvent(playerId, message, null, null, targets, null, QueryType.PICK_TARGET, 0, 0, required, null, null);
}
public static PlayerQueryEvent targetEvent(UUID playerId, String message, Set targets, boolean required, Map options) {
- return new PlayerQueryEvent(playerId, message, null, null, targets, null, QueryType.PICK_TARGET, 0, 0, required, options);
+ return new PlayerQueryEvent(playerId, message, null, null, targets, null, QueryType.PICK_TARGET, 0, 0, required, options, null);
}
public static PlayerQueryEvent targetEvent(UUID playerId, String message, Cards cards, boolean required, Map options) {
- return new PlayerQueryEvent(playerId, message, null, null, null, cards, QueryType.PICK_TARGET, 0, 0, required, options);
+ return new PlayerQueryEvent(playerId, message, null, null, null, cards, QueryType.PICK_TARGET, 0, 0, required, options, null);
}
public static PlayerQueryEvent targetEvent(UUID playerId, String message, List abilities) {
- return new PlayerQueryEvent(playerId, message, abilities, null, null, null, QueryType.PICK_ABILITY, 0, 0, true, null);
+ return new PlayerQueryEvent(playerId, message, abilities, null, null, null, QueryType.PICK_ABILITY, 0, 0, true, null, null);
}
public static PlayerQueryEvent targetEvent(UUID playerId, String message, List perms, boolean required) {
@@ -188,23 +193,27 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
}
public static PlayerQueryEvent selectEvent(UUID playerId, String message) {
- return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.SELECT, 0, 0, false, null);
+ return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.SELECT, 0, 0, false, null, null);
}
public static PlayerQueryEvent selectEvent(UUID playerId, String message, Map options) {
- return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.SELECT, 0, 0, false, options);
+ return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.SELECT, 0, 0, false, options, null);
}
public static PlayerQueryEvent playManaEvent(UUID playerId, String message, Map options) {
- return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.PLAY_MANA, 0, 0, false, options);
+ return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.PLAY_MANA, 0, 0, false, options, null);
}
public static PlayerQueryEvent playXManaEvent(UUID playerId, String message) {
- return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.PLAY_X_MANA, 0, 0, false, null);
+ return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.PLAY_X_MANA, 0, 0, false, null, null);
}
public static PlayerQueryEvent amountEvent(UUID playerId, String message, int min, int max) {
- return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.AMOUNT, min, max, false, null);
+ return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.AMOUNT, min, max, false, null, null);
+ }
+
+ public static PlayerQueryEvent multiAmountEvent(UUID playerId, List messages, int min, int max, Map options) {
+ return new PlayerQueryEvent(playerId, null, null, null, null, null, QueryType.MULTI_AMOUNT, min, max, false, options, messages);
}
public static PlayerQueryEvent pickCard(UUID playerId, String message, List booster, int time) {
@@ -287,4 +296,7 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri
return choice;
}
+ public List getMessages() {
+ return messages;
+ }
}
diff --git a/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java b/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java
index 87dd6f45ec9..b2aac5ec305 100644
--- a/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java
+++ b/Mage/src/main/java/mage/game/events/PlayerQueryEventSource.java
@@ -84,6 +84,10 @@ public class PlayerQueryEventSource implements EventSource, Se
dispatcher.fireEvent(PlayerQueryEvent.amountEvent(playerId, message, min, max));
}
+ public void multiAmount(UUID playerId, List messages, int min, int max, Map options) {
+ dispatcher.fireEvent(PlayerQueryEvent.multiAmountEvent(playerId, messages, min, max, options));
+ }
+
public void chooseChoice(UUID playerId, Choice choice) {
dispatcher.fireEvent(PlayerQueryEvent.chooseChoiceEvent(playerId, choice));
}
diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java
index 2192d2bf0a2..3d9b815069d 100644
--- a/Mage/src/main/java/mage/players/Player.java
+++ b/Mage/src/main/java/mage/players/Player.java
@@ -708,6 +708,19 @@ public interface Player extends MageItem, Copyable {
int getAmount(int min, int max, String message, Game game);
+ /**
+ * Player distributes amount among multiple options
+ *
+ * @param outcome AI hint
+ * @param messages List of options to distribute amount among
+ * @param min Minimum value per option
+ * @param max Total amount to be distributed
+ * @param type MultiAmountType enum to set dialog options such as title and header
+ * @param game Game
+ * @return List of integers with size equal to messages.size(). The sum of the integers is equal to max.
+ */
+ List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game);
+
void sideboard(Match match, Deck deck);
void construct(Tournament tournament, Deck deck);
diff --git a/Mage/src/main/java/mage/players/StubPlayer.java b/Mage/src/main/java/mage/players/StubPlayer.java
index 1b86856123c..ad47e96d2a1 100644
--- a/Mage/src/main/java/mage/players/StubPlayer.java
+++ b/Mage/src/main/java/mage/players/StubPlayer.java
@@ -11,6 +11,7 @@ import mage.cards.Card;
import mage.cards.Cards;
import mage.cards.decks.Deck;
import mage.choices.Choice;
+import mage.constants.MultiAmountType;
import mage.constants.Outcome;
import mage.constants.RangeOfInfluence;
import mage.filter.FilterMana;
@@ -205,6 +206,11 @@ public class StubPlayer extends PlayerImpl implements Player {
return 0;
}
+ @Override
+ public List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game) {
+ return null;
+ }
+
@Override
public void sideboard(Match match, Deck deck) {
diff --git a/Mage/src/main/java/mage/target/Target.java b/Mage/src/main/java/mage/target/Target.java
index ae0db1ccb5f..72f5c25a8ae 100644
--- a/Mage/src/main/java/mage/target/Target.java
+++ b/Mage/src/main/java/mage/target/Target.java
@@ -147,4 +147,8 @@ public interface Target extends Serializable {
String getChooseHint();
void setEventReporting(boolean shouldReport);
+
+ int getSize();
+
+ boolean contains(UUID targetId);
}
diff --git a/Mage/src/main/java/mage/target/TargetAmount.java b/Mage/src/main/java/mage/target/TargetAmount.java
index 72012fe9d67..692cc0cb741 100644
--- a/Mage/src/main/java/mage/target/TargetAmount.java
+++ b/Mage/src/main/java/mage/target/TargetAmount.java
@@ -5,6 +5,7 @@ import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.constants.Outcome;
import mage.game.Game;
+import mage.players.Player;
import java.util.*;
import java.util.stream.Collectors;
@@ -59,7 +60,7 @@ public abstract class TargetAmount extends TargetImpl {
public void clearChosen() {
super.clearChosen();
amountWasSet = false;
- // remainingAmount = amount;
+ // remainingAmount = amount; // auto-calced on target remove
}
public void setAmountDefinition(DynamicValue amount) {
@@ -71,6 +72,9 @@ public abstract class TargetAmount extends TargetImpl {
amountWasSet = true;
}
+ public int getAmountTotal(Game game, Ability source) {
+ return amount.calculate(game, source, null);
+ }
@Override
public void addTarget(UUID id, int amount, Ability source, Game game, boolean skipEvent) {
@@ -92,17 +96,25 @@ public abstract class TargetAmount extends TargetImpl {
@Override
public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) {
+ Player player = game.getPlayer(playerId);
+ if (player == null) {
+ return false;
+ }
+
if (!amountWasSet) {
setAmount(source, game);
}
chosen = isChosen();
while (remainingAmount > 0) {
+ if (!player.canRespond()) {
+ return chosen;
+ }
if (!getTargetController(game, playerId).chooseTargetAmount(outcome, this, source, game)) {
return chosen;
}
chosen = isChosen();
}
- return chosen = true;
+ return chosen;
}
@Override
@@ -163,4 +175,12 @@ public abstract class TargetAmount extends TargetImpl {
}
}
}
+
+ public void setTargetAmount(UUID targetId, int amount, Ability source, Game game) {
+ if (!amountWasSet) {
+ setAmount(source, game);
+ }
+ remainingAmount -= (amount - this.getTargetAmount(targetId));
+ this.setTargetAmount(targetId, amount, game);
+ }
}
diff --git a/Mage/src/main/java/mage/target/TargetImpl.java b/Mage/src/main/java/mage/target/TargetImpl.java
index 053000e106a..d1d9b83d74a 100644
--- a/Mage/src/main/java/mage/target/TargetImpl.java
+++ b/Mage/src/main/java/mage/target/TargetImpl.java
@@ -278,51 +278,60 @@ public abstract class TargetImpl implements Target {
@Override
public boolean choose(Outcome outcome, UUID playerId, UUID sourceId, Game game) {
- Player player = game.getPlayer(playerId);
- if (player == null) {
+ Player targetController = getTargetController(game, playerId);
+ if (targetController == null) {
return false;
}
- while (!isChosen() && !doneChosing()) {
- if (!player.canRespond()) {
- return chosen = targets.size() >= getNumberOfTargets();
+ chosen = targets.size() >= getNumberOfTargets();
+ do {
+ if (!targetController.canRespond()) {
+ return chosen;
}
- chosen = targets.size() >= getNumberOfTargets();
- if (!player.choose(outcome, this, sourceId, game)) {
+ if (!targetController.choose(outcome, this, sourceId, game)) {
return chosen;
}
chosen = targets.size() >= getNumberOfTargets();
- }
- return chosen = true;
+ } while (!isChosen() && !doneChosing());
+ return chosen;
}
@Override
public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) {
- Player player = game.getPlayer(playerId);
- if (player == null) {
+ Player targetController = getTargetController(game, playerId);
+ if (targetController == null) {
return false;
}
List possibleTargets = new ArrayList<>(possibleTargets(source.getSourceId(), playerId, game));
- while (!isChosen() && !doneChosing()) {
- if (!player.canRespond()) {
- return chosen = targets.size() >= getNumberOfTargets();
+
+ chosen = targets.size() >= getNumberOfTargets();
+ do {
+ if (!targetController.canRespond()) {
+ return chosen;
}
- chosen = targets.size() >= getNumberOfTargets();
if (isRandom()) {
- if (!possibleTargets.isEmpty()) {
- int index = RandomUtil.nextInt(possibleTargets.size());
- this.addTarget(possibleTargets.get(index), source, game);
- possibleTargets.remove(index);
- } else {
+ if (possibleTargets.isEmpty()) {
return chosen;
}
- } else if (!getTargetController(game, playerId).chooseTarget(outcome, this, source, game)) {
+ // find valid target
+ while (!possibleTargets.isEmpty()) {
+ int index = RandomUtil.nextInt(possibleTargets.size());
+ if (this.canTarget(playerId, possibleTargets.get(index), source, game)) {
+ this.addTarget(possibleTargets.get(index), source, game);
+ possibleTargets.remove(index);
+ break;
+ } else {
+ possibleTargets.remove(index);
+ }
+ }
+ } else if (!targetController.chooseTarget(outcome, this, source, game)) {
return chosen;
}
chosen = targets.size() >= getNumberOfTargets();
- }
- return chosen = true;
+ } while (!isChosen() && !doneChosing());
+
+ return chosen;
}
@Override
@@ -574,4 +583,14 @@ public abstract class TargetImpl implements Target {
public void setEventReporting(boolean shouldReport) {
this.shouldReportEvents = shouldReport;
}
+
+ @Override
+ public int getSize() {
+ return targets.size();
+ }
+
+ @Override
+ public boolean contains(UUID targetId) {
+ return targets.containsKey(targetId);
+ }
}
diff --git a/Mage/src/main/java/mage/target/Targets.java b/Mage/src/main/java/mage/target/Targets.java
index 09d0b080aa3..2c2a0a4796c 100644
--- a/Mage/src/main/java/mage/target/Targets.java
+++ b/Mage/src/main/java/mage/target/Targets.java
@@ -4,6 +4,7 @@ import mage.abilities.Ability;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.events.GameEvent;
+import mage.players.Player;
import mage.target.targetpointer.*;
import org.apache.log4j.Logger;
@@ -65,6 +66,7 @@ public class Targets extends ArrayList {
if (!canChoose(source.getSourceId(), playerId, game)) {
return false;
}
+
//int state = game.bookmarkState();
while (!isChosen()) {
Target target = this.getUnchosen().get(0);
diff --git a/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java b/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java
index bbbd6d63a50..99577565c06 100644
--- a/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java
+++ b/Mage/src/main/java/mage/target/common/TargetCardInLibrary.java
@@ -72,21 +72,19 @@ public class TargetCardInLibrary extends TargetCard {
}
cards.sort(Comparator.comparing(MageObject::getName));
Cards cardsId = new CardsImpl();
- cards.forEach((card) -> {
- cardsId.add(card);
- });
+ cards.forEach(cardsId::add);
- while (!isChosen() && !doneChosing()) {
+ chosen = targets.size() >= getMinNumberOfTargets();
+ do {
if (!player.canRespond()) {
- return chosen = targets.size() >= minNumberOfTargets;
+ return chosen;
}
- chosen = targets.size() >= minNumberOfTargets;
if (!player.chooseTarget(outcome, cardsId, this, null, game)) {
return chosen;
}
- chosen = targets.size() >= minNumberOfTargets;
- }
- return chosen = true;
+ chosen = targets.size() >= getMinNumberOfTargets();
+ } while (!isChosen() && !doneChosing());
+ return chosen;
}
@Override
diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java
index 5a181ca9731..2060293d645 100644
--- a/Mage/src/main/java/mage/util/CardUtil.java
+++ b/Mage/src/main/java/mage/util/CardUtil.java
@@ -1271,6 +1271,16 @@ public final class CardUtil {
return res;
}
+ public static int parseIntWithDefault(String value, int defaultValue) {
+ int res;
+ try {
+ res = Integer.parseInt(value);
+ } catch(NumberFormatException ex) {
+ res = defaultValue;
+ }
+ return res;
+ }
+
/**
* Find mapping from original to copied card (e.g. map original left side with copied left side)
*