diff --git a/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java b/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java index 5daa90a9f7..2690ef639d 100644 --- a/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java +++ b/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java @@ -47,7 +47,9 @@ import mage.client.components.MageTextArea; import mage.client.dialog.MageDialog; import mage.client.util.audio.AudioManager; import mage.client.util.gui.ArrowBuilder; -import mage.constants.Constants; +import static mage.constants.Constants.Option.ORIGINAL_ID; +import static mage.constants.Constants.Option.SECOND_MESSAGE; +import static mage.constants.Constants.Option.SPECIAL_BUTTON; import mage.constants.PlayerAction; import mage.remote.Session; import org.apache.log4j.Logger; @@ -85,6 +87,7 @@ public class FeedbackPanel extends javax.swing.JPanel { public void init(UUID gameId) { this.gameId = gameId; session = MageFrame.getSession(); + helper.init(gameId); } public void getFeedback(FeedbackMode mode, String message, boolean special, Map options, int messageId) { @@ -95,61 +98,42 @@ public class FeedbackPanel extends javax.swing.JPanel { } this.lastMessageId = messageId; } - - this.lblMessage.setText(message); - this.helper.setMessage(message); + this.helper.setBasicMessage(message); + this.helper.setOriginalId(null); // reference to the feedback causing ability + String lblText = addAdditionalText(message, options); + this.helper.setTextArea(lblText); + this.lblMessage.setText(lblText); this.mode = mode; switch (this.mode) { case INFORM: - this.btnLeft.setVisible(false); - this.btnRight.setVisible(false); - this.helper.setState("", false, "", false); + setButtonState("", "", mode); break; case QUESTION: - this.btnLeft.setVisible(true); - this.btnLeft.setText("Yes"); - this.btnRight.setVisible(true); - this.btnRight.setText("No"); - this.helper.setState("Yes", true, "No", true); + setButtonState("Yes", "No", mode); + if (options != null && options.containsKey(ORIGINAL_ID)) { + this.helper.setOriginalId((UUID) options.get(ORIGINAL_ID)); + } break; case CONFIRM: - this.btnLeft.setVisible(true); - this.btnLeft.setText("OK"); - this.btnRight.setVisible(true); - this.btnRight.setText("Cancel"); - this.helper.setState("Ok", true, "Cancel", true); + setButtonState("OK", "Cancel", mode); break; case CANCEL: - this.btnLeft.setVisible(false); - this.btnRight.setVisible(true); - this.btnRight.setText("Cancel"); - this.helper.setState("", false, "Cancel", true); + setButtonState("", "Cancel", mode); this.helper.setUndoEnabled(false); break; case SELECT: - this.btnLeft.setVisible(false); - this.btnRight.setVisible(true); - this.btnRight.setText("Done"); - this.helper.setState("", false, "Done", true); + setButtonState("", "Done", mode); break; case END: - this.btnLeft.setVisible(false); - this.btnRight.setVisible(true); - this.btnRight.setText("Close game"); - this.helper.setState("", false, "Close game", true); + setButtonState("", "Close game", mode); ArrowBuilder.getBuilder().removeAllArrows(gameId); endWithTimeout(); break; } - if (options != null && options.containsKey(Constants.Option.SPECIAL_BUTTON)) { - String specialText = (String) options.get(Constants.Option.SPECIAL_BUTTON); - this.btnSpecial.setVisible(true); - this.btnSpecial.setText(specialText); - this.helper.setSpecial(specialText, true); + if (options != null && options.containsKey(SPECIAL_BUTTON)) { + this.setSpecial((String) options.get(SPECIAL_BUTTON), true); } else { - this.btnSpecial.setVisible(special); - this.btnSpecial.setText("Special"); - this.helper.setSpecial("Special", special); + this.setSpecial("Special", special); } requestFocusIfPossible(); @@ -162,6 +146,32 @@ public class FeedbackPanel extends javax.swing.JPanel { this.helper.setVisible(true); } + private void setButtonState(String leftText, String rightText, FeedbackMode mode) { + btnLeft.setVisible(!leftText.isEmpty()); + btnLeft.setText(leftText); + btnRight.setVisible(!rightText.isEmpty()); + btnRight.setText(rightText); + this.helper.setState(leftText, !leftText.isEmpty(), rightText, !rightText.isEmpty(), mode); + } + + private String addAdditionalText(String message, Map options) { + if (options != null && options.containsKey(SECOND_MESSAGE)) { + return message + getSmallText((String) options.get(SECOND_MESSAGE)); + } else { + return message; + } + } + + protected String getSmallText(String text) { + return "
" + text + "
"; + } + + private void setSpecial(String text, boolean visible) { + this.btnSpecial.setText(text); + this.btnSpecial.setVisible(visible); + this.helper.setSpecial(text, visible); + } + /** * Close game window by pressing OK button after 8 seconds */ diff --git a/Mage.Client/src/main/java/mage/client/game/HelperPanel.java b/Mage.Client/src/main/java/mage/client/game/HelperPanel.java index 829493e0e1..8e27613ecc 100644 --- a/Mage.Client/src/main/java/mage/client/game/HelperPanel.java +++ b/Mage.Client/src/main/java/mage/client/game/HelperPanel.java @@ -28,17 +28,34 @@ package mage.client.game; import java.awt.Color; +import java.awt.Component; import java.awt.Dimension; import java.awt.GridBagLayout; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.UUID; import javax.swing.BoxLayout; import javax.swing.JButton; +import javax.swing.JMenuItem; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import javax.swing.UIManager; +import mage.client.MageFrame; import mage.client.components.MageTextArea; +import mage.client.game.FeedbackPanel.FeedbackMode; +import static mage.client.game.FeedbackPanel.FeedbackMode.QUESTION; +import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_ID_NO; +import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_ID_YES; +import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL; +import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_TEXT_NO; +import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_TEXT_YES; +import mage.remote.Session; /** * Panel with buttons that copy the state of feedback panel. @@ -64,12 +81,34 @@ public class HelperPanel extends JPanel { private final int defaultDismissTimeout = ToolTipManager.sharedInstance().getDismissDelay(); private final Object tooltipBackground = UIManager.get("info"); + private static final String CMD_AUTO_ANSWER_ID_YES = "cmdAutoAnswerIdYes"; + private static final String CMD_AUTO_ANSWER_ID_NO = "cmdAutoAnswerIdNo"; + private static final String CMD_AUTO_ANSWER_NAME_YES = "cmdAutoAnswerNameYes"; + private static final String CMD_AUTO_ANSWER_NAME_NO = "cmdAutoAnswerNameNo"; + private static final String CMD_AUTO_ANSWER_RESET_ALL = "cmdAutoAnswerResetAll"; + + // popup menu for set automatic answers + private JPopupMenu popupMenuAskYes; + private JPopupMenu popupMenuAskNo; + + // originalId of feedback causing ability + private UUID originalId; + private String message; + + private UUID gameId; + private Session session; + public HelperPanel() { initComponents(); } - private void initComponents() { + public void init(UUID gameId) { + this.gameId = gameId; + session = MageFrame.getSession(); + } + private void initComponents() { + initPopupMenuTriggerOrder(); setBackground(new Color(0, 0, 0, 100)); //setLayout(new GridBagLayout()); setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); @@ -108,71 +147,49 @@ public class HelperPanel extends JPanel { btnUndo.setVisible(false); container.add(btnUndo); - btnLeft.addActionListener(new java.awt.event.ActionListener() { + MouseListener checkPopupAdapter = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent me) { + checkPopupMenu(me); + } + + @Override + public void mouseReleased(MouseEvent me) { + checkPopupMenu(me); + } + + }; + + btnLeft.addMouseListener(checkPopupAdapter); + btnLeft.addActionListener(new ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent evt) { if (linkLeft != null) { - { - Thread worker = new Thread() { - @Override - public void run() { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - setState("", false, "", false); - setSpecial("", false); - linkLeft.doClick(); - } - }); - } - }; - worker.start(); - } + clickButton(linkLeft); } } }); - btnRight.addActionListener(new java.awt.event.ActionListener() { + btnRight.addMouseListener(checkPopupAdapter); + btnRight.addActionListener(new ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent evt) { if (linkRight != null) { - Thread worker = new Thread() { - @Override - public void run() { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - setState("", false, "", false); - setSpecial("", false); - linkRight.doClick(); - } - }); - } - }; - worker.start(); + clickButton(linkRight); } } }); - btnSpecial.addActionListener(new java.awt.event.ActionListener() { + btnSpecial.addActionListener(new ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent evt) { if (linkSpecial != null) { { - Thread worker = new Thread() { - @Override - public void run() { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - setState("", false, "", false); - setSpecial("", false); - linkSpecial.doClick(); - } - }); - } - }; - worker.start(); +// if (evt.getActionCommand().equals("automatic")) { +// showPopupMenu(evt); +// } else { + clickButton(linkSpecial); +// } } } } @@ -200,7 +217,7 @@ public class HelperPanel extends JPanel { } }); - // sets a darker background and higher simiss time fpr tooltip in the feedback / helper panel + // sets a darker background and higher simiss time fur tooltip in the feedback / helper panel textArea.addMouseListener(new MouseAdapter() { @Override @@ -217,15 +234,50 @@ public class HelperPanel extends JPanel { }); } - public void setState(String txtLeft, boolean leftVisible, String txtRight, boolean rightVisible) { + private void checkPopupMenu(MouseEvent me) { + if (me.isPopupTrigger() + && originalId != null) { // only Yes/No requests from abilities can be automated + JButton source = (JButton) me.getSource(); + if (source.getActionCommand().startsWith(QUESTION.toString())) { + showPopupMenu(me.getComponent(), source.getActionCommand()); + me.consume(); + } + } + } + + private void clickButton(final JButton button) { + Thread worker = new Thread() { + @Override + public void run() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + setState("", false, "", false, null); + setSpecial("", false); + button.doClick(); + } + }); + } + }; + worker.start(); + } + + public void setState(String txtLeft, boolean leftVisible, String txtRight, boolean rightVisible, FeedbackMode mode) { this.btnLeft.setVisible(leftVisible); if (!txtLeft.isEmpty()) { this.btnLeft.setText(txtLeft); + if (mode != null) { + this.btnLeft.setActionCommand(mode.toString() + txtLeft); + } } this.btnRight.setVisible(rightVisible); if (!txtRight.isEmpty()) { this.btnRight.setText(txtRight); + if (mode != null) { + this.btnRight.setActionCommand(mode.toString() + txtRight); + } } + } public void setSpecial(String txtSpecial, boolean specialVisible) { @@ -251,25 +303,116 @@ public class HelperPanel extends JPanel { this.linkUndo = undo; } - public void setMessage(String message) { -// if (message.startsWith("Use alternative cost")) { -// message = "Use alternative cost?"; -// } else if (message.contains("Use ")) { -// if (message.length() < this.getWidth() / 10) { -// message = getSmallText(message); -// } else { -// message = "Use ability?" + getSmallText(message.substring(0, this.getWidth() / 10)); -// } -// } - textArea.setText(message, this.getWidth()); + public void setOriginalId(UUID originalId) { + this.originalId = originalId; } - protected String getSmallText(String text) { - return "
" + text + "
"; + public void setBasicMessage(String message) { + this.message = message; + this.textArea.setText(message, this.getWidth()); + } + + public void setTextArea(String message) { + this.textArea.setText(message, this.getWidth()); } @Override public void requestFocus() { this.btnRight.requestFocus(); } + + private void initPopupMenuTriggerOrder() { + + ActionListener actionListener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + handleAutoAnswerPopupMenuEvent(e); + } + }; + + popupMenuAskYes = new JPopupMenu(); + popupMenuAskNo = new JPopupMenu(); + + // String tooltipText = ""; + JMenuItem menuItem; + menuItem = new JMenuItem("Always Yes for the same text and ability"); + menuItem.setActionCommand(CMD_AUTO_ANSWER_ID_YES); + menuItem.addActionListener(actionListener); + menuItem.setToolTipText("If the same question from the same ability would
be asked again, it's automatically answered with Yes."); + popupMenuAskYes.add(menuItem); + + menuItem = new JMenuItem("Always No for the same text and ability"); + menuItem.setActionCommand(CMD_AUTO_ANSWER_ID_NO); + menuItem.setToolTipText("If the same question from the same ability would
be asked again, it's automatically answered with No."); + menuItem.addActionListener(actionListener); + popupMenuAskNo.add(menuItem); + + menuItem = new JMenuItem("Always Yes for the same text"); + menuItem.setActionCommand(CMD_AUTO_ANSWER_NAME_YES); + menuItem.setToolTipText("If the same question would be asked again (regardless from which source),
it's automatically answered with Yes."); + menuItem.addActionListener(actionListener); + popupMenuAskYes.add(menuItem); + + menuItem = new JMenuItem("Always No for the same text"); + menuItem.setActionCommand(CMD_AUTO_ANSWER_NAME_NO); + menuItem.setToolTipText("If the same question would be asked again (regardless from which source),
it's automatically answered with No."); + menuItem.addActionListener(actionListener); + popupMenuAskNo.add(menuItem); + + menuItem = new JMenuItem("Delete all automatic Yes/No settings"); + menuItem.setActionCommand(CMD_AUTO_ANSWER_RESET_ALL); + menuItem.addActionListener(actionListener); + popupMenuAskYes.add(menuItem); + + menuItem = new JMenuItem("Delete all automatic Yes/No settings"); + menuItem.setActionCommand(CMD_AUTO_ANSWER_RESET_ALL); + menuItem.addActionListener(actionListener); + popupMenuAskNo.add(menuItem); + } + + public void handleAutoAnswerPopupMenuEvent(ActionEvent e) { + switch (e.getActionCommand()) { + case CMD_AUTO_ANSWER_ID_YES: + session.sendPlayerAction(REQUEST_AUTO_ANSWER_ID_YES, gameId, originalId.toString() + "#" + message); + clickButton(btnLeft); + break; + case CMD_AUTO_ANSWER_ID_NO: + session.sendPlayerAction(REQUEST_AUTO_ANSWER_ID_NO, gameId, originalId.toString() + "#" + message); + clickButton(btnRight); + break; + case CMD_AUTO_ANSWER_NAME_YES: + session.sendPlayerAction(REQUEST_AUTO_ANSWER_TEXT_YES, gameId, message); + clickButton(btnLeft); + break; + case CMD_AUTO_ANSWER_NAME_NO: + session.sendPlayerAction(REQUEST_AUTO_ANSWER_TEXT_NO, gameId, message); + clickButton(btnRight); + break; + case CMD_AUTO_ANSWER_RESET_ALL: + session.sendPlayerAction(REQUEST_AUTO_ANSWER_RESET_ALL, gameId, null); + break; + } + } + + private void showPopupMenu(Component callingComponent, String actionCommand) { + // Get the location of the point 'on the screen' + Point p = callingComponent.getLocationOnScreen(); + // Show the JPopupMenu via program + // Parameter desc + // ---------------- + // this - represents current frame + // 0,0 is the co ordinate where the popup + // is shown + JPopupMenu menu; + if (actionCommand.endsWith("Yes")) { + menu = popupMenuAskYes; + } else { + menu = popupMenuAskNo; + } + menu.show(this, 0, 0); + + // Now set the location of the JPopupMenu + // This location is relative to the screen + menu.setLocation(p.x, p.y + callingComponent.getHeight()); + } } diff --git a/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java b/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java index cdf949d149..44f2f509b5 100644 --- a/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java +++ b/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java @@ -290,6 +290,18 @@ public class PlayAreaPanel extends javax.swing.JPanel { } }); + menuItem = new JMenuItem("Use requests - reset automatic answers"); + menuItem.setMnemonic(KeyEvent.VK_T); + menuItem.setToolTipText("Deletes all defined automatic answers for Yes/No usage requests."); + automaticConfirmsMenu.add(menuItem); + // Reset the replacement effcts that were auto selected for the game + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gamePanel.getSession().sendPlayerAction(PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL, gameId, null); + } + }); + JMenu handCardsMenu = new JMenu("Cards on hand"); handCardsMenu.setMnemonic(KeyEvent.VK_H); popupMenu.add(handCardsMenu); diff --git a/Mage.Common/src/mage/constants/Constants.java b/Mage.Common/src/mage/constants/Constants.java index a5b586c63a..965346f281 100644 --- a/Mage.Common/src/mage/constants/Constants.java +++ b/Mage.Common/src/mage/constants/Constants.java @@ -83,6 +83,10 @@ public final class Constants { public static final String POSSIBLE_ATTACKERS = "possibleAttackers"; public static final String SPECIAL_BUTTON = "specialButton"; + // used to control automatic answers of optional effects + public static final String ORIGINAL_ID = "originalId"; + public static final String SECOND_MESSAGE = "secondMessage"; + public static final String HINT_TEXT = "hintText"; } 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 aed2451d8b..dfec5b69e6 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 @@ -63,6 +63,7 @@ import mage.constants.ManaType; import mage.constants.Outcome; import mage.constants.PhaseStep; import mage.constants.PlayerAction; +import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL; import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL; import mage.constants.RangeOfInfluence; import mage.constants.Zone; @@ -89,6 +90,7 @@ import mage.target.common.TargetCreatureOrPlayer; import mage.target.common.TargetDefender; import mage.util.GameLog; import mage.util.ManaUtil; +import mage.util.MessageToClient; import org.apache.log4j.Logger; /** @@ -115,6 +117,9 @@ public class HumanPlayer extends PlayerImpl { protected Set triggerAutoOrderNameFirst = new HashSet<>(); protected Set triggerAutoOrderNameLast = new HashSet<>(); + protected Map requestAutoAnswerId = new HashMap<>(); + protected Map requestAutoAnswerText = new HashMap<>(); + public HumanPlayer(String name, RangeOfInfluence range, int skill) { super(name, range); replacementEffectChoice = new ChoiceImpl(true); @@ -173,10 +178,9 @@ public class HumanPlayer extends PlayerImpl { public boolean chooseMulligan(Game game) { updateGameStatePriority("chooseMulligan", game); int nextHandSize = game.mulliganDownTo(playerId); - game.fireAskPlayerEvent(playerId, new StringBuilder("Mulligan ") - .append(getHand().size() > nextHandSize ? "down to " : "for free, draw ") - .append(nextHandSize) - .append(nextHandSize == 1 ? " card?" : " cards?").toString()); + game.fireAskPlayerEvent(playerId, new MessageToClient("Mulligan " + + (getHand().size() > nextHandSize ? "down to " : "for free, draw ") + + nextHandSize + (nextHandSize == 1 ? " card?" : " cards?")), null); waitForBooleanResponse(game); if (!abort) { return response.getBoolean(); @@ -186,8 +190,19 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean chooseUse(Outcome outcome, String message, Ability source, Game game) { + if (source != null) { + Boolean answer = requestAutoAnswerId.get(source.getOriginalId() + "#" + message); + if (answer != null) { + return answer; + } else { + answer = requestAutoAnswerText.get(message); + if (answer != null) { + return answer; + } + } + } updateGameStatePriority("chooseUse", game); - game.fireAskPlayerEvent(playerId, addSecondLineWithObjectName(message, source == null ? null : source.getSourceId(), game)); + game.fireAskPlayerEvent(playerId, new MessageToClient(message, getRelatedObjectName(source, game)), source); waitForBooleanResponse(game); if (!abort) { return response.getBoolean(); @@ -195,6 +210,21 @@ public class HumanPlayer extends PlayerImpl { return false; } + private String getRelatedObjectName(Ability source, Game game) { + if (source != null) { + return getRelatedObjectName(source.getSourceId(), game); + } + return null; + } + + private String getRelatedObjectName(UUID sourceId, Game game) { + MageObject mageObject = game.getObject(sourceId); + if (mageObject != null) { + return mageObject.getLogName(); + } + return null; + } + private String addSecondLineWithObjectName(String message, UUID sourceId, Game game) { if (sourceId != null) { MageObject mageObject = game.getPermanent(sourceId); @@ -304,7 +334,7 @@ public class HumanPlayer extends PlayerImpl { List chosen = target.getTargets(); options.put("chosen", (Serializable) chosen); - game.fireSelectTargetEvent(getId(), addSecondLineWithObjectName(target.getMessage(), sourceId, game), targetIds, required, getOptions(target, options)); + game.fireSelectTargetEvent(getId(), new MessageToClient(target.getMessage(), getRelatedObjectName(sourceId, game)), targetIds, required, getOptions(target, options)); waitForResponse(game); if (response.getUUID() != null) { if (!targetIds.contains(response.getUUID())) { @@ -370,7 +400,7 @@ public class HumanPlayer extends PlayerImpl { required = false; } - game.fireSelectTargetEvent(getId(), addSecondLineWithObjectName(target.getMessage(), source == null ? null : source.getSourceId(), game), possibleTargets, required, getOptions(target, null)); + game.fireSelectTargetEvent(getId(), new MessageToClient(target.getMessage(), getRelatedObjectName(source, game)), possibleTargets, required, getOptions(target, null)); waitForResponse(game); if (response.getUUID() != null) { if (target.getTargets().contains(response.getUUID())) { @@ -438,7 +468,7 @@ public class HumanPlayer extends PlayerImpl { options.put("choosable", (Serializable) choosable); } - game.fireSelectTargetEvent(playerId, target.getMessage(), cards, required, options); + game.fireSelectTargetEvent(playerId, new MessageToClient(target.getMessage()), cards, required, options); waitForResponse(game); if (response.getUUID() != null) { if (target.canTarget(response.getUUID(), cards, game)) { @@ -492,7 +522,7 @@ public class HumanPlayer extends PlayerImpl { if (!choosable.isEmpty()) { options.put("choosable", (Serializable) choosable); } - game.fireSelectTargetEvent(playerId, addSecondLineWithObjectName(target.getMessage(), source == null ? null : source.getSourceId(), game), cards, required, options); + game.fireSelectTargetEvent(playerId, new MessageToClient(target.getMessage(), getRelatedObjectName(source, game)), cards, required, options); waitForResponse(game); if (response.getUUID() != null) { if (target.getTargets().contains(response.getUUID())) { // if already included remove it @@ -521,7 +551,7 @@ public class HumanPlayer extends PlayerImpl { public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) { updateGameStatePriority("chooseTargetAmount", game); while (!abort) { - game.fireSelectTargetEvent(playerId, addSecondLineWithObjectName(target.getMessage() + "\n Amount remaining:" + target.getAmountRemaining(), source == null ? null : source.getSourceId(), game), + game.fireSelectTargetEvent(playerId, new MessageToClient(target.getMessage() + "\n Amount remaining:" + target.getAmountRemaining(), getRelatedObjectName(source, game)), target.possibleTargets(source == null ? null : source.getSourceId(), playerId, game), target.isRequired(source), getOptions(target, null)); @@ -1043,7 +1073,8 @@ public class HumanPlayer extends PlayerImpl { protected void selectCombatGroup(UUID defenderId, UUID blockerId, Game game) { updateGameStatePriority("selectCombatGroup", game); TargetAttackingCreature target = new TargetAttackingCreature(); - game.fireSelectTargetEvent(playerId, addSecondLineWithObjectName("Select attacker to block", blockerId, game), target.possibleTargets(null, playerId, game), false, getOptions(target, null)); + game.fireSelectTargetEvent(playerId, new MessageToClient("Select attacker to block", getRelatedObjectName(blockerId, game)), + target.possibleTargets(null, playerId, game), false, getOptions(target, null)); waitForResponse(game); if (response.getBoolean() != null) { // do nothing @@ -1354,11 +1385,43 @@ public class HumanPlayer extends PlayerImpl { case TRIGGER_AUTO_ORDER_RESET_ALL: setTriggerAutoOrder(playerAction, game, data); break; + case REQUEST_AUTO_ANSWER_ID_NO: + case REQUEST_AUTO_ANSWER_ID_YES: + case REQUEST_AUTO_ANSWER_TEXT_NO: + case REQUEST_AUTO_ANSWER_TEXT_YES: + case REQUEST_AUTO_ANSWER_RESET_ALL: + setRequestAutoAnswer(playerAction, game, data); + break; default: super.sendPlayerAction(playerAction, game, data); } } + private void setRequestAutoAnswer(PlayerAction playerAction, Game game, Object data) { + if (playerAction.equals(REQUEST_AUTO_ANSWER_RESET_ALL)) { + requestAutoAnswerId.clear(); + requestAutoAnswerText.clear(); + return; + } + if (data instanceof String) { + String key = (String) data; + switch (playerAction) { + case REQUEST_AUTO_ANSWER_ID_NO: + requestAutoAnswerId.put(key, false); + break; + case REQUEST_AUTO_ANSWER_TEXT_NO: + requestAutoAnswerText.put(key, false); + break; + case REQUEST_AUTO_ANSWER_ID_YES: + requestAutoAnswerId.put(key, true); + break; + case REQUEST_AUTO_ANSWER_TEXT_YES: + requestAutoAnswerText.put(key, true); + break; + } + } + } + private void setTriggerAutoOrder(PlayerAction playerAction, Game game, Object data) { if (playerAction.equals(TRIGGER_AUTO_ORDER_RESET_ALL)) { triggerAutoOrderAbilityFirst.clear(); 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 5242a01961..eec4ea0dfe 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameController.java +++ b/Mage.Server/src/main/java/mage/server/game/GameController.java @@ -228,7 +228,7 @@ public class GameController implements GameCallback { try { switch (event.getQueryType()) { case ASK: - ask(event.getPlayerId(), event.getMessage()); + ask(event.getPlayerId(), event.getMessage(), event.getOptions()); break; case PICK_TARGET: target(event.getPlayerId(), event.getMessage(), event.getCards(), event.getPerms(), event.getTargets(), event.isRequired(), event.getOptions()); @@ -774,11 +774,11 @@ public class GameController implements GameCallback { // TODO: inform watchers about game end and who won } - private synchronized void ask(UUID playerId, final String question) throws MageException { + private synchronized void ask(UUID playerId, final String question, final Map options) throws MageException { perform(playerId, new Command() { @Override public void execute(UUID playerId) { - getGameSession(playerId).ask(question); + getGameSession(playerId).ask(question, options); } }); 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 157211555e..2f93e94376 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java +++ b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java @@ -78,11 +78,11 @@ public class GameSessionPlayer extends GameSessionWatcher { super.CleanUp(); } - public void ask(final String question) { + public void ask(final String question, final Map options) { if (!killed) { User user = UserManager.getInstance().getUser(userId); if (user != null) { - user.fireCallback(new ClientCallback("gameAsk", game.getId(), new GameClientMessage(getGameView(), question))); + user.fireCallback(new ClientCallback("gameAsk", game.getId(), new GameClientMessage(getGameView(), question, options))); } } } diff --git a/Mage.Sets/src/mage/sets/tenthedition/AngelsFeather.java b/Mage.Sets/src/mage/sets/tenthedition/AngelsFeather.java index c518a21d95..c9036a6021 100644 --- a/Mage.Sets/src/mage/sets/tenthedition/AngelsFeather.java +++ b/Mage.Sets/src/mage/sets/tenthedition/AngelsFeather.java @@ -25,20 +25,17 @@ * authors and should not be interpreted as representing official policies, either expressed * or implied, of BetaSteward_at_googlemail.com. */ - package mage.sets.tenthedition; import java.util.UUID; -import mage.abilities.TriggeredAbilityImpl; +import mage.ObjectColor; +import mage.abilities.common.SpellCastAllTriggeredAbility; import mage.abilities.effects.common.GainLifeEffect; import mage.cards.CardImpl; import mage.constants.CardType; import mage.constants.Rarity; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; -import mage.game.stack.Spell; +import mage.filter.FilterSpell; +import mage.filter.predicate.mageobject.ColorPredicate; /** * @@ -46,10 +43,18 @@ import mage.game.stack.Spell; */ public class AngelsFeather extends CardImpl { + private final static FilterSpell filter = new FilterSpell("a white spell"); + + static { + filter.add(new ColorPredicate(ObjectColor.WHITE)); + } + public AngelsFeather(UUID ownerId) { super(ownerId, 311, "Angel's Feather", Rarity.UNCOMMON, new CardType[]{CardType.ARTIFACT}, "{2}"); this.expansionSetCode = "10E"; - this.addAbility(new AngelsFeatherAbility()); + + // Whenever a player casts a white spell, you may gain 1 life. + this.addAbility(new SpellCastAllTriggeredAbility(new GainLifeEffect(1), filter, true)); } public AngelsFeather(final AngelsFeather card) { @@ -62,36 +67,3 @@ public class AngelsFeather extends CardImpl { } } - -class AngelsFeatherAbility extends TriggeredAbilityImpl { - - public AngelsFeatherAbility() { - super(Zone.BATTLEFIELD, new GainLifeEffect(1), true); - } - - public AngelsFeatherAbility(final AngelsFeatherAbility ability) { - super(ability); - } - - @Override - public AngelsFeatherAbility copy() { - return new AngelsFeatherAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == EventType.SPELL_CAST; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - Spell spell = game.getStack().getSpell(event.getTargetId()); - return spell != null && spell.getColor(game).isWhite(); - } - - @Override - public String getRule() { - return "Whenever a player casts a white spell, you may gain 1 life."; - } - -} diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/CastCreaturesTest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/CastCreaturesTest.java index d14393676f..565fc57e36 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/CastCreaturesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/CastCreaturesTest.java @@ -100,6 +100,35 @@ public class CastCreaturesTest extends CardTestPlayerBaseAI { assertPermanentCount(playerA, "Blazing Specter", 1); } + @Test + public void testSimpleCast5() { + addCard(Zone.HAND, playerA, "Plains", 2); + addCard(Zone.HAND, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Silvercoat Lion", 3); + addCard(Zone.HAND, playerA, "Soul Warden"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Plains", 1); + assertPermanentCount(playerA, "Soul Warden", 1); + } + + @Test + public void testSimpleCast6() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Plains", 2); + addCard(Zone.HAND, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, "Silvercoat Lion", 1); + addCard(Zone.HAND, playerA, "Pillarfield Ox", 1); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Plains", 1); + assertPermanentCount(playerA, "Silvercoat Lion", 1); + } + @Test public void testCast4Creature() { addCard(Zone.LIBRARY, playerA, "Swamp", 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/TargetedTriggeredTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/TargetedTriggeredTest.java new file mode 100644 index 0000000000..e0056f8638 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/TargetedTriggeredTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package org.mage.test.cards.triggers; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class TargetedTriggeredTest extends CardTestPlayerBase { + + /** + * Tests that the first spell that targets Kira, Great Glass-Spinner is + * countered. + * + */ + @Test + @Ignore + // this does currently not work in test, because the target event will be fired earlier during tests, + // so the zone change counter for the fixed target of the counterspell will not work + public void testKiraGreatGlassSpinnerFirstSpellTurn() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + addCard(Zone.BATTLEFIELD, playerB, "Kira, Great Glass-Spinner", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Kira, Great Glass-Spinner"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Lightning Bolt", 1); + + assertPermanentCount(playerB, "Kira, Great Glass-Spinner", 1); + } + +} diff --git a/Mage/src/mage/constants/PlayerAction.java b/Mage/src/mage/constants/PlayerAction.java index e572a6f2b6..746db8f13a 100644 --- a/Mage/src/mage/constants/PlayerAction.java +++ b/Mage/src/mage/constants/PlayerAction.java @@ -60,5 +60,11 @@ public enum PlayerAction { ADD_PERMISSION_TO_ROLLBACK_TURN, DENY_PERMISSON_TO_ROLLBACK_TURN, PERMISSION_REQUESTS_ALLOWED_ON, - PERMISSION_REQUESTS_ALLOWED_OFF + PERMISSION_REQUESTS_ALLOWED_OFF, + REQUEST_AUTO_ANSWER_ID_YES, + REQUEST_AUTO_ANSWER_ID_NO, + REQUEST_AUTO_ANSWER_TEXT_YES, + REQUEST_AUTO_ANSWER_TEXT_NO, + REQUEST_AUTO_ANSWER_RESET_ALL, + } diff --git a/Mage/src/mage/game/Game.java b/Mage/src/mage/game/Game.java index 0c10ca220a..e558dc02ad 100644 --- a/Mage/src/mage/game/Game.java +++ b/Mage/src/mage/game/Game.java @@ -73,6 +73,7 @@ import mage.game.turn.Turn; import mage.players.Player; import mage.players.PlayerList; import mage.players.Players; +import mage.util.MessageToClient; import mage.util.functions.ApplyToPermanent; public interface Game extends MageItem, Serializable { @@ -221,13 +222,13 @@ public interface Game extends MageItem, Serializable { void addPlayerQueryEventListener(Listener listener); - void fireAskPlayerEvent(UUID playerId, String message); + void fireAskPlayerEvent(UUID playerId, MessageToClient message, Ability source); void fireChooseChoiceEvent(UUID playerId, Choice choice); - void fireSelectTargetEvent(UUID playerId, String message, Set targets, boolean required, Map options); + void fireSelectTargetEvent(UUID playerId, MessageToClient message, Set targets, boolean required, Map options); - void fireSelectTargetEvent(UUID playerId, String message, Cards cards, boolean required, Map options); + void fireSelectTargetEvent(UUID playerId, MessageToClient message, Cards cards, boolean required, Map options); void fireSelectTargetTriggeredAbilityEvent(UUID playerId, String message, List abilities); diff --git a/Mage/src/mage/game/GameImpl.java b/Mage/src/mage/game/GameImpl.java index bddd512b7b..b8ad5577fe 100644 --- a/Mage/src/mage/game/GameImpl.java +++ b/Mage/src/mage/game/GameImpl.java @@ -118,6 +118,7 @@ import mage.target.Target; import mage.target.TargetPermanent; import mage.target.TargetPlayer; import mage.util.GameLog; +import mage.util.MessageToClient; import mage.util.functions.ApplyToPermanent; import mage.watchers.Watchers; import mage.watchers.common.BlockedAttackerWatcher; @@ -1886,11 +1887,11 @@ public abstract class GameImpl implements Game, Serializable { } @Override - public void fireAskPlayerEvent(UUID playerId, String message) { + public void fireAskPlayerEvent(UUID playerId, MessageToClient message, Ability source) { if (simulation) { return; } - playerQueryEventSource.ask(playerId, message); + playerQueryEventSource.ask(playerId, message.getMessage(), source, addMessageToOptions(message, null)); } @Override @@ -1914,19 +1915,19 @@ public abstract class GameImpl implements Game, Serializable { } @Override - public void fireSelectTargetEvent(UUID playerId, String message, Set targets, boolean required, Map options) { + public void fireSelectTargetEvent(UUID playerId, MessageToClient message, Set targets, boolean required, Map options) { if (simulation) { return; } - playerQueryEventSource.target(playerId, message, targets, required, options); + playerQueryEventSource.target(playerId, message.getMessage(), targets, required, addMessageToOptions(message, options)); } @Override - public void fireSelectTargetEvent(UUID playerId, String message, Cards cards, boolean required, Map options) { + public void fireSelectTargetEvent(UUID playerId, MessageToClient message, Cards cards, boolean required, Map options) { if (simulation) { return; } - playerQueryEventSource.target(playerId, message, cards, required, options); + playerQueryEventSource.target(playerId, message.getMessage(), cards, required, addMessageToOptions(message, options)); } /** @@ -2692,4 +2693,19 @@ public abstract class GameImpl implements Game, Serializable { return enterWithCounters.get(sourceId); } + private Map addMessageToOptions(MessageToClient message, Map options) { + if (message.getSecondMessage() != null) { + if (options == null) { + options = new HashMap<>(); + } + options.put("secondMessage", message.getSecondMessage()); + } + if (message.getHintText() != null) { + if (options == null) { + options = new HashMap<>(); + } + options.put("hintText", message.getHintText()); + } + return options; + } } diff --git a/Mage/src/mage/game/events/PlayerQueryEvent.java b/Mage/src/mage/game/events/PlayerQueryEvent.java index 6b299f2085..ea9b943451 100644 --- a/Mage/src/mage/game/events/PlayerQueryEvent.java +++ b/Mage/src/mage/game/events/PlayerQueryEvent.java @@ -149,8 +149,14 @@ public class PlayerQueryEvent extends EventObject implements ExternalEvent, Seri this.playerId = playerId; } - public static PlayerQueryEvent askEvent(UUID playerId, String message) { - return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.ASK, 0, 0, false, null); + public static PlayerQueryEvent askEvent(UUID playerId, String message, Ability source, Map options) { + if (source != null) { + if (options == null) { + options = new HashMap<>(); + } + options.put("originalId", source.getOriginalId()); + } + return new PlayerQueryEvent(playerId, message, null, null, null, null, QueryType.ASK, 0, 0, false, options); } public static PlayerQueryEvent chooseAbilityEvent(UUID playerId, String message, String objectName, List choices) { diff --git a/Mage/src/mage/game/events/PlayerQueryEventSource.java b/Mage/src/mage/game/events/PlayerQueryEventSource.java index 7620ed1657..ea5f0aed97 100644 --- a/Mage/src/mage/game/events/PlayerQueryEventSource.java +++ b/Mage/src/mage/game/events/PlayerQueryEventSource.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import mage.abilities.Ability; import mage.abilities.ActivatedAbility; import mage.abilities.TriggeredAbility; import mage.cards.Card; @@ -58,8 +59,8 @@ public class PlayerQueryEventSource implements EventSource, Se dispatcher.removeAllListener(); } - public void ask(UUID playerId, String message) { - dispatcher.fireEvent(PlayerQueryEvent.askEvent(playerId, message)); + public void ask(UUID playerId, String message, Ability source, Map options) { + dispatcher.fireEvent(PlayerQueryEvent.askEvent(playerId, message, source, options)); } public void select(UUID playerId, String message) { diff --git a/Mage/src/mage/util/MessageToClient.java b/Mage/src/mage/util/MessageToClient.java new file mode 100644 index 0000000000..fca0e28906 --- /dev/null +++ b/Mage/src/mage/util/MessageToClient.java @@ -0,0 +1,43 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package mage.util; + +/** + * + * @author LevelX2 + */ +public class MessageToClient { + + private String message; + private String secondMessage; + private String hintText; + + public MessageToClient(String message) { + this(message, null); + } + + public MessageToClient(String message, String secondMessage) { + this(message, secondMessage, null); + } + + public MessageToClient(String message, String secondMessage, String hintText) { + this.message = message; + this.secondMessage = secondMessage; + this.hintText = hintText; + } + + public String getMessage() { + return message; + } + + public String getSecondMessage() { + return secondMessage; + } + + public String getHintText() { + return hintText; + } +} diff --git a/Mage/src/mage/util/TreeNode.java b/Mage/src/mage/util/TreeNode.java index 7463b7addc..b9d4620629 100644 --- a/Mage/src/mage/util/TreeNode.java +++ b/Mage/src/mage/util/TreeNode.java @@ -1,16 +1,16 @@ /* * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. - * + * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR @@ -20,12 +20,11 @@ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied, of BetaSteward_at_googlemail.com. */ - package mage.util; import java.util.ArrayList; @@ -34,14 +33,16 @@ import java.util.List; /** * * @author BetaSteward_at_googlemail.com + * @param */ public class TreeNode { + protected T data; protected List> children; public TreeNode() { super(); - children = new ArrayList>(); + children = new ArrayList<>(); } public TreeNode(T data) { @@ -70,7 +71,7 @@ public class TreeNode { } public void addChild(T child) { - children.add(new TreeNode(child)); + children.add(new TreeNode<>(child)); } public void addChildAt(int index, TreeNode child) throws IndexOutOfBoundsException { @@ -93,7 +94,7 @@ public class TreeNode { return this.data; } - public void setData(T data) { + private void setData(T data) { this.data = data; } @@ -116,10 +117,7 @@ public class TreeNode { return false; } final TreeNode other = (TreeNode) obj; - if (this.data != other.data && (this.data == null || !this.data.equals(other.data))) { - return false; - } - return true; + return !(this.data != other.data && (this.data == null || !this.data.equals(other.data))); } }