From 519b3988be52e1fe94eabae7977f11b04aeadfeb Mon Sep 17 00:00:00 2001 From: Alexander Novotny Date: Fri, 28 Jul 2023 22:05:21 -0400 Subject: [PATCH] game timer: Add chess-style buffer time option (#10598) * UI Changes * Add new buffer time options * Main functionality * Final implementation Also added player UI for when they are using their buffer time (timer turns green) --- .../mage/client/components/HoverButton.java | 12 +++++- .../mage/client/dialog/NewTableDialog.form | 8 ++++ .../mage/client/dialog/NewTableDialog.java | 26 +++++++++++- .../client/dialog/NewTournamentDialog.form | 14 +++++++ .../client/dialog/NewTournamentDialog.java | 33 ++++++++++++++- .../mage/client/dialog/PreferencesDialog.java | 2 + .../main/java/mage/client/game/GamePanel.java | 6 ++- .../java/mage/client/game/PlayAreaPanel.java | 3 +- .../java/mage/client/game/PlayerPanelExt.java | 13 +++++- .../java/mage/client/table/TablesPanel.java | 1 + .../java/mage/utils/timer/PriorityTimer.java | 17 +++++++- .../src/main/java/mage/view/GameView.java | 6 +++ .../src/main/java/mage/view/PlayerView.java | 6 +++ .../src/main/java/mage/view/TableView.java | 2 + .../java/mage/server/game/GameController.java | 15 ++++--- .../java/org/mage/test/player/TestPlayer.java | 10 +++++ .../java/org/mage/test/stub/PlayerStub.java | 10 +++++ .../java/mage/constants/MatchBufferTime.java | 41 +++++++++++++++++++ Mage/src/main/java/mage/game/Game.java | 4 ++ Mage/src/main/java/mage/game/GameImpl.java | 14 ++++++- .../main/java/mage/game/match/MatchImpl.java | 3 ++ .../java/mage/game/match/MatchOptions.java | 17 ++++++++ Mage/src/main/java/mage/players/Player.java | 14 +++++++ .../main/java/mage/players/PlayerImpl.java | 12 ++++++ 24 files changed, 273 insertions(+), 16 deletions(-) create mode 100644 Mage/src/main/java/mage/constants/MatchBufferTime.java diff --git a/Mage.Client/src/main/java/mage/client/components/HoverButton.java b/Mage.Client/src/main/java/mage/client/components/HoverButton.java index e1b803028c8..638fa4363a6 100644 --- a/Mage.Client/src/main/java/mage/client/components/HoverButton.java +++ b/Mage.Client/src/main/java/mage/client/components/HoverButton.java @@ -57,6 +57,7 @@ public class HoverButton extends JPanel implements MouseListener { private Command observer = null; private Command onHover = null; private Color textColor = Color.white; + private Color topTextColor = null; private final Rectangle centerTextArea = new Rectangle(5, 18, 75, 40); private Color centerTextColor = new Color(200, 210, 0, 200); private Color origCenterTextColor = new Color(200, 210, 0, 200); @@ -152,7 +153,7 @@ public class HoverButton extends JPanel implements MouseListener { topTextOffsetX = calculateOffsetForTop(g2d, topText); g2d.setColor(textBGColor); g2d.drawString(topText, topTextOffsetX + 1, 14); - g2d.setColor(textColor); + g2d.setColor(topTextColor != null ? topTextColor : textColor); g2d.drawString(topText, topTextOffsetX, 13); } if (topTextImage != null) { @@ -235,6 +236,15 @@ public class HoverButton extends JPanel implements MouseListener { this.textColor = textColor; } + /** + * Overrides textColor for the upper text if non-null. + * If null, return back to textColor. + * @param textColor + */ + public void setTopTextColor(Color textColor) { + this.topTextColor = textColor; + } + public void setOverlayImage(Image image) { this.overlayImage = image; this.overlayImageSize = new Dimension(image.getWidth(null), image.getHeight(null)); diff --git a/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.form b/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.form index 28d100eb090..4e38b6ea41c 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.form +++ b/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.form @@ -364,6 +364,14 @@ + + + + + + + + diff --git a/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java b/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java index 38153e1bf8c..27157781f65 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java @@ -8,6 +8,7 @@ import mage.client.table.TablePlayerPanel; import mage.client.util.Event; import mage.client.util.IgnoreList; import mage.client.util.Listener; +import mage.constants.MatchBufferTime; import mage.constants.MatchTimeLimit; import mage.constants.MultiplayerAttackOption; import mage.constants.RangeOfInfluence; @@ -85,6 +86,8 @@ public class NewTableDialog extends MageDialog { cbDeckType = new javax.swing.JComboBox(); lbTimeLimit = new javax.swing.JLabel(); cbTimeLimit = new javax.swing.JComboBox(); + lbBufferTime = new javax.swing.JLabel(); + cbBufferTime = new javax.swing.JComboBox(); lblGameType = new javax.swing.JLabel(); cbGameType = new javax.swing.JComboBox(); chkRollbackTurnsAllowed = new javax.swing.JCheckBox(); @@ -190,6 +193,9 @@ public class NewTableDialog extends MageDialog { lbTimeLimit.setText("Time Limit:"); lbTimeLimit.setToolTipText("The active time a player may use to finish the match. If their time runs out, the player looses the current game."); + lbBufferTime.setText("Buffer Time:"); + lbBufferTime.setToolTipText("The extra time a player gets whenever the timer starts before their normal time limit starts going down."); + lblGameType.setText("Game Type:"); cbGameType.addActionListener(new java.awt.event.ActionListener() { @@ -332,7 +338,11 @@ public class NewTableDialog extends MageDialog { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(lbTimeLimit) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(cbTimeLimit, javax.swing.GroupLayout.PREFERRED_SIZE, 102, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(cbTimeLimit, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lbBufferTime) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cbBufferTime, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(lblPassword) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) @@ -412,6 +422,8 @@ public class NewTableDialog extends MageDialog { .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(cbTimeLimit) .addComponent(lbTimeLimit) + .addComponent(cbBufferTime) + .addComponent(lbBufferTime) .addComponent(lblPassword) .addComponent(txtPassword) .addComponent(chkSpectatorsAllowed))) @@ -604,6 +616,7 @@ public class NewTableDialog extends MageDialog { } options.setDeckType((String) this.cbDeckType.getSelectedItem()); options.setMatchTimeLimit((MatchTimeLimit) this.cbTimeLimit.getSelectedItem()); + options.setMatchBufferTime((MatchBufferTime) this.cbBufferTime.getSelectedItem()); options.setAttackOption((MultiplayerAttackOption) this.cbAttackOption.getSelectedItem()); options.setSkillLevel((SkillLevel) this.cbSkillLevel.getSelectedItem()); options.setRange((RangeOfInfluence) this.cbRange.getSelectedItem()); @@ -801,6 +814,7 @@ public class NewTableDialog extends MageDialog { cbDeckType.setModel(new DefaultComboBoxModel(SessionHandler.getDeckTypes())); selectLimitedByDefault(); cbTimeLimit.setModel(new DefaultComboBoxModel(MatchTimeLimit.values())); + cbBufferTime.setModel(new DefaultComboBoxModel(MatchBufferTime.values())); cbRange.setModel(new DefaultComboBoxModel(RangeOfInfluence.values())); cbAttackOption.setModel(new DefaultComboBoxModel(MultiplayerAttackOption.values())); cbSkillLevel.setModel(new DefaultComboBoxModel(SkillLevel.values())); @@ -881,6 +895,14 @@ public class NewTableDialog extends MageDialog { break; } } + // TODO: Rethink defaults with buffer time? + int bufferTime = Integer.parseInt(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_TABLE_BUFFER_TIME + versionStr, "0")); + for (MatchBufferTime mtl : MatchBufferTime.values()) { + if (mtl.getBufferTime() == bufferTime) { + this.cbBufferTime.setSelectedItem(mtl); + break; + } + } cbDeckType.setSelectedItem(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_TABLE_DECK_TYPE + versionStr, "Limited")); String deckFile = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_TABLE_DECK_FILE + versionStr, null); if (deckFile != null) { @@ -985,6 +1007,7 @@ public class NewTableDialog extends MageDialog { private javax.swing.JComboBox cbRange; private javax.swing.JComboBox cbSkillLevel; private javax.swing.JComboBox cbTimeLimit; + private javax.swing.JComboBox cbBufferTime; private javax.swing.JCheckBox chkPlaneChase; private javax.swing.JCheckBox chkRated; private javax.swing.JCheckBox chkRollbackTurnsAllowed; @@ -996,6 +1019,7 @@ public class NewTableDialog extends MageDialog { private javax.swing.JSeparator jSeparator3; private javax.swing.JLabel lbDeckType; private javax.swing.JLabel lbTimeLimit; + private javax.swing.JLabel lbBufferTime; private javax.swing.JLabel lblAttack; private javax.swing.JLabel lblEdhPowerLevel; private javax.swing.JLabel lblFreeMulligans; diff --git a/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.form b/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.form index 01be5c944cd..91e22008877 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.form +++ b/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.form @@ -373,6 +373,20 @@ + + + + + + + + + + + + + + diff --git a/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java b/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java index 32d13e79b21..ca8d6509a01 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java @@ -18,6 +18,7 @@ import mage.client.SessionHandler; import mage.client.table.TournamentPlayerPanel; import mage.client.util.IgnoreList; import mage.client.util.gui.FastSearchUtil; +import mage.constants.MatchBufferTime; import mage.constants.MatchTimeLimit; import mage.constants.MultiplayerAttackOption; import mage.constants.RangeOfInfluence; @@ -85,6 +86,7 @@ public class NewTournamentDialog extends MageDialog { cbDeckType.setModel(new DefaultComboBoxModel(SessionHandler.getDeckTypes())); cbTimeLimit.setModel(new DefaultComboBoxModel(MatchTimeLimit.values())); + cbBufferTime.setModel(new DefaultComboBoxModel(MatchBufferTime.values())); cbSkillLevel.setModel(new DefaultComboBoxModel(SkillLevel.values())); cbDraftCube.setModel(new DefaultComboBoxModel(SessionHandler.getDraftCubes())); cbDraftTiming.setModel(new DefaultComboBoxModel(Arrays.stream(TimingOption.values()) @@ -131,6 +133,8 @@ public class NewTournamentDialog extends MageDialog { txtName = new javax.swing.JTextField(); lbTimeLimit = new javax.swing.JLabel(); cbTimeLimit = new javax.swing.JComboBox(); + lbBufferTime = new javax.swing.JLabel(); + cbBufferTime = new javax.swing.JComboBox(); lbSkillLevel = new javax.swing.JLabel(); cbSkillLevel = new javax.swing.JComboBox(); lblPassword = new javax.swing.JLabel(); @@ -246,6 +250,16 @@ public class NewTournamentDialog extends MageDialog { cbTimeLimit.setToolTipText("The time a player has for the whole match. If a player runs out of time during a game, they lose the complete match. "); + lbBufferTime.setText("Buffer Time:"); + lbBufferTime.setToolTipText( + "The time a player gets whenever their timer starts before their match time starts going down. "); + + org.jdesktop.beansbinding.Binding binding2 = org.jdesktop.beansbinding.Bindings.createAutoBinding( org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ_WRITE, cbBufferTime, org.jdesktop.beansbinding.ObjectProperty.create(), lbBufferTime, org.jdesktop.beansbinding.BeanProperty.create("labelFor")); + bindingGroup.addBinding(binding2); + + cbBufferTime.setToolTipText( + "The time a player gets whenever their timer starts before their match time starts going down. "); + lbSkillLevel.setText("Skill Level:"); lbSkillLevel.setToolTipText("The time a player has for the whole match. If a player runs out of time during a game, they lose the complete match. "); @@ -552,7 +566,11 @@ public class NewTournamentDialog extends MageDialog { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(lbTimeLimit) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(cbTimeLimit, javax.swing.GroupLayout.PREFERRED_SIZE, 101, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(cbTimeLimit, javax.swing.GroupLayout.PREFERRED_SIZE, 90, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lbBufferTime) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cbBufferTime, javax.swing.GroupLayout.PREFERRED_SIZE, 90, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(lblPassword) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) @@ -573,6 +591,8 @@ public class NewTournamentDialog extends MageDialog { .addComponent(lblName) .addComponent(lbTimeLimit) .addComponent(cbTimeLimit, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbBufferTime) + .addComponent(cbBufferTime, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(lblPassword) .addComponent(txtPassword, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(cbAllowSpectators, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) @@ -1339,6 +1359,7 @@ public class NewTournamentDialog extends MageDialog { tOptions.getMatchOptions().setBannedUsers(IgnoreList.getIgnoredUsers(serverAddress)); tOptions.getMatchOptions().setMatchTimeLimit((MatchTimeLimit) this.cbTimeLimit.getSelectedItem()); + tOptions.getMatchOptions().setMatchBufferTime((MatchBufferTime) this.cbBufferTime.getSelectedItem()); tOptions.getMatchOptions().setSkillLevel((SkillLevel) this.cbSkillLevel.getSelectedItem()); tOptions.getMatchOptions().setWinsNeeded((Integer) this.spnNumWins.getValue()); tOptions.getMatchOptions().setFreeMulligans((Integer) this.spnFreeMulligans.getValue()); @@ -1363,6 +1384,14 @@ public class NewTournamentDialog extends MageDialog { break; } } + // TODO: Rethink default match time with buffer time. + int bufferTime = Integer.parseInt(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_TOURNAMENT_BUFFER_TIME + versionStr, "0")); + for (MatchBufferTime mtl : MatchBufferTime.values()) { + if (mtl.getBufferTime() == bufferTime) { + this.cbBufferTime.setSelectedItem(mtl); + break; + } + } String skillLevelDefault = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_TABLE_SKILL_LEVEL + versionStr, "Casual"); for (SkillLevel skillLevel : SkillLevel.values()) { if (skillLevel.toString().equals(skillLevelDefault)) { @@ -1485,6 +1514,7 @@ public class NewTournamentDialog extends MageDialog { private javax.swing.JCheckBox cbPlaneChase; private javax.swing.JComboBox cbSkillLevel; private javax.swing.JComboBox cbTimeLimit; + private javax.swing.JComboBox cbBufferTime; private javax.swing.JComboBox cbTournamentType; private javax.swing.JCheckBox chkRated; private javax.swing.JCheckBox chkRollbackTurnsAllowed; @@ -1492,6 +1522,7 @@ public class NewTournamentDialog extends MageDialog { private javax.swing.JLabel lbDeckType; private javax.swing.JLabel lbSkillLevel; private javax.swing.JLabel lbTimeLimit; + private javax.swing.JLabel lbBufferTime; private javax.swing.JLabel lblConstructionTime; private javax.swing.JLabel lblDraftCube; private javax.swing.JLabel lblFreeMulligans; diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java index e500442a2aa..e028de5745c 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java @@ -196,6 +196,7 @@ public class PreferencesDialog extends javax.swing.JDialog { public static final String KEY_NEW_TABLE_PASSWORD_JOIN = "newTablePasswordJoin"; public static final String KEY_NEW_TABLE_DECK_TYPE = "newTableDeckType"; public static final String KEY_NEW_TABLE_TIME_LIMIT = "newTableTimeLimit"; + public static final String KEY_NEW_TABLE_BUFFER_TIME = "newTableBufferTime"; public static final String KEY_NEW_TABLE_GAME_TYPE = "newTableGameType"; public static final String KEY_NEW_TABLE_NUMBER_OF_WINS = "newTableNumberOfWins"; public static final String KEY_NEW_TABLE_ROLLBACK_TURNS_ALLOWED = "newTableRollbackTurnsAllowed"; @@ -218,6 +219,7 @@ public class PreferencesDialog extends javax.swing.JDialog { public static final String KEY_NEW_TOURNAMENT_NAME = "newTournamentName"; public static final String KEY_NEW_TOURNAMENT_PASSWORD = "newTournamentPassword"; public static final String KEY_NEW_TOURNAMENT_TIME_LIMIT = "newTournamentTimeLimit"; + public static final String KEY_NEW_TOURNAMENT_BUFFER_TIME = "newTournamentBufferTime"; public static final String KEY_NEW_TOURNAMENT_CONSTR_TIME = "newTournamentConstructionTime"; public static final String KEY_NEW_TOURNAMENT_TYPE = "newTournamentType"; public static final String KEY_NEW_TOURNAMENT_NUMBER_OF_FREE_MULLIGANS = "newTournamentNumberOfFreeMulligans"; 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 8cdb7213a6a..1bb24363081 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -605,7 +605,8 @@ public final class GamePanel extends javax.swing.JPanel { } PlayerView player = game.getPlayers().get(playerSeat); PlayAreaPanel playAreaPanel = new PlayAreaPanel(player, bigCard, gameId, game.getPriorityTime(), this, - new PlayAreaPanelOptions(game.isPlayer(), player.isHuman(), game.isPlayer(), game.isRollbackTurnsAllowed(), row == 0)); + new PlayAreaPanelOptions(game.isPlayer(), player.isHuman(), game.isPlayer(), + game.isRollbackTurnsAllowed(), row == 0)); players.put(player.getPlayerId(), playAreaPanel); playersWhoLeft.put(player.getPlayerId(), false); GridBagConstraints c = new GridBagConstraints(); @@ -649,7 +650,8 @@ public final class GamePanel extends javax.swing.JPanel { } player = game.getPlayers().get(playerNum); PlayAreaPanel playerPanel = new PlayAreaPanel(player, bigCard, gameId, game.getPriorityTime(), this, - new PlayAreaPanelOptions(game.isPlayer(), player.isHuman(), false, game.isRollbackTurnsAllowed(), row == 0)); + new PlayAreaPanelOptions(game.isPlayer(), player.isHuman(), false, game.isRollbackTurnsAllowed(), + row == 0)); players.put(player.getPlayerId(), playerPanel); playersWhoLeft.put(player.getPlayerId(), false); c = new GridBagConstraints(); 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 3e4f0df69dd..abf74ca67e5 100644 --- a/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java +++ b/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java @@ -57,7 +57,8 @@ public class PlayAreaPanel extends javax.swing.JPanel { * @param gamePanel * @param options */ - public PlayAreaPanel(PlayerView player, BigCard bigCard, UUID gameId, int priorityTime, GamePanel gamePanel, PlayAreaPanelOptions options) { + public PlayAreaPanel(PlayerView player, BigCard bigCard, UUID gameId, int priorityTime, GamePanel gamePanel, + PlayAreaPanelOptions options) { this.gamePanel = gamePanel; this.options = options; initComponents(); diff --git a/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java b/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java index 87bff89fac3..034d607f6e0 100644 --- a/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java +++ b/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java @@ -103,10 +103,14 @@ public class PlayerPanelExt extends javax.swing.JPanel { }); final PriorityTimer pt = timer; timer.setTaskOnTick(() -> { - int priorityTimeValue = pt.getCount(); + int priorityTimeValue = pt.getCount() + pt.getBufferCount(); String text = getPriorityTimeLeftString(priorityTimeValue); + PlayerPanelExt.this.avatar.setTopText(text); + PlayerPanelExt.this.avatar.setTopTextColor(pt.getBufferCount() > 0 ? Color.GREEN : null); PlayerPanelExt.this.timerLabel.setText(text); + PlayerPanelExt.this.timerLabel + .setForeground(pt.getBufferCount() > 0 ? Color.GREEN.darker().darker() : Color.BLACK); PlayerPanelExt.this.avatar.repaint(); }); timer.init(gameId); @@ -304,8 +308,13 @@ public class PlayerPanelExt extends javax.swing.JPanel { if (player.getPriorityTimeLeft() != Integer.MAX_VALUE) { String priorityTimeValue = getPriorityTimeLeftString(player); this.timer.setCount(player.getPriorityTimeLeft()); + this.timer.setBufferCount(player.getBufferTimeLeft()); this.avatar.setTopText(priorityTimeValue); this.timerLabel.setText(priorityTimeValue); + + this.avatar.setTopTextColor(player.getBufferTimeLeft() > 0 ? Color.GREEN : null); + this.timerLabel + .setForeground(player.getBufferTimeLeft() > 0 ? Color.GREEN.darker().darker() : Color.BLACK); } if (player.isTimerActive()) { this.timer.resume(); @@ -396,7 +405,7 @@ public class PlayerPanelExt extends javax.swing.JPanel { } private String getPriorityTimeLeftString(PlayerView player) { - int priorityTimeLeft = player.getPriorityTimeLeft(); + int priorityTimeLeft = player.getPriorityTimeLeft() + player.getBufferTimeLeft(); return getPriorityTimeLeftString(priorityTimeLeft); } diff --git a/Mage.Client/src/main/java/mage/client/table/TablesPanel.java b/Mage.Client/src/main/java/mage/client/table/TablesPanel.java index 8282e14b8e2..c1901322944 100644 --- a/Mage.Client/src/main/java/mage/client/table/TablesPanel.java +++ b/Mage.Client/src/main/java/mage/client/table/TablesPanel.java @@ -1708,6 +1708,7 @@ public class TablesPanel extends javax.swing.JPanel { options.setRange(RangeOfInfluence.ONE); options.setWinsNeeded(2); options.setMatchTimeLimit(MatchTimeLimit.NONE); + options.setMatchBufferTime(MatchBufferTime.NONE); options.setFreeMulligans(2); options.setSkillLevel(SkillLevel.CASUAL); options.setRollbackTurnsAllowed(true); diff --git a/Mage.Common/src/main/java/mage/utils/timer/PriorityTimer.java b/Mage.Common/src/main/java/mage/utils/timer/PriorityTimer.java index e49a820c813..fa628df4a61 100644 --- a/Mage.Common/src/main/java/mage/utils/timer/PriorityTimer.java +++ b/Mage.Common/src/main/java/mage/utils/timer/PriorityTimer.java @@ -18,6 +18,7 @@ public class PriorityTimer extends TimerTask { private final Action taskOnTimeout; private int count; + private int bufferCount = 0; private Action taskOnTick; private States state = States.NONE; @@ -76,6 +77,14 @@ public class PriorityTimer extends TimerTask { this.count = count; } + public int getBufferCount() { + return bufferCount; + } + + public void setBufferCount(int count) { + this.bufferCount = count; + } + public void setTaskOnTick(Action taskOnTick) { this.taskOnTick = taskOnTick; } @@ -83,7 +92,13 @@ public class PriorityTimer extends TimerTask { @Override public void run() { if (state == States.RUNNING) { - count--; + // Count down buffer time first + if (bufferCount > 0) { + bufferCount--; + } else { + count--; + } + if (taskOnTick != null) { try { taskOnTick.execute(); diff --git a/Mage.Common/src/main/java/mage/view/GameView.java b/Mage.Common/src/main/java/mage/view/GameView.java index 84a2abbf959..91529f791a5 100644 --- a/Mage.Common/src/main/java/mage/view/GameView.java +++ b/Mage.Common/src/main/java/mage/view/GameView.java @@ -42,6 +42,7 @@ public class GameView implements Serializable { private static final Logger LOGGER = Logger.getLogger(GameView.class); private final int priorityTime; + private final int bufferTime; private final List players = new ArrayList<>(); private CardsView hand; private PlayableObjectsList canPlayObjects; @@ -68,6 +69,7 @@ public class GameView implements Serializable { Player createdForPlayer = null; this.isPlayer = createdForPlayerId != null; this.priorityTime = game.getPriorityTime(); + this.bufferTime = game.getBufferTime(); for (Player player : state.getPlayers().values()) { players.add(new PlayerView(player, state, game, createdForPlayerId, watcherUserId)); if (player.getId().equals(createdForPlayerId)) { @@ -313,6 +315,10 @@ public class GameView implements Serializable { return priorityTime; } + public int getBufferTime() { + return bufferTime; + } + public UUID getActivePlayerId() { return activePlayerId; } diff --git a/Mage.Common/src/main/java/mage/view/PlayerView.java b/Mage.Common/src/main/java/mage/view/PlayerView.java index 9703f20509e..0e2b136afec 100644 --- a/Mage.Common/src/main/java/mage/view/PlayerView.java +++ b/Mage.Common/src/main/java/mage/view/PlayerView.java @@ -47,6 +47,7 @@ public class PlayerView implements Serializable { private final List attachments = new ArrayList<>(); private final int statesSavedSize; private final int priorityTimeLeft; + private final int bufferTimeLeft; private final boolean passedTurn; // F4 private final boolean passedUntilEndOfTurn; // F5 private final boolean passedUntilNextMain; // F6 @@ -74,6 +75,7 @@ public class PlayerView implements Serializable { this.isActive = (player.getId().equals(state.getActivePlayerId())); this.hasPriority = player.getId().equals(state.getPriorityPlayerId()); this.priorityTimeLeft = player.getPriorityTimeLeft(); + this.bufferTimeLeft = player.getBufferTimeLeft(); this.timerActive = (this.hasPriority && player.isGameUnderControl()) || (player.getPlayersUnderYourControl().contains(state.getPriorityPlayerId())) || player.getId().equals(game.getState().getChoosingPlayerId()); @@ -275,6 +277,10 @@ public class PlayerView implements Serializable { return priorityTimeLeft; } + public int getBufferTimeLeft() { + return bufferTimeLeft; + } + public boolean hasPriority() { return hasPriority; } diff --git a/Mage.Common/src/main/java/mage/view/TableView.java b/Mage.Common/src/main/java/mage/view/TableView.java index 32d09ee79a2..9bd2403f02a 100644 --- a/Mage.Common/src/main/java/mage/view/TableView.java +++ b/Mage.Common/src/main/java/mage/view/TableView.java @@ -98,6 +98,7 @@ public class TableView implements Serializable { if (table.getMatch().getGames().isEmpty()) { addInfo.append("Wins:").append(table.getMatch().getWinsNeeded()); addInfo.append(" Time: ").append(table.getMatch().getOptions().getMatchTimeLimit().toString()); + addInfo.append(" Buffer: ").append(table.getMatch().getOptions().getMatchBufferTime().toString()); if (table.getMatch().getFreeMulligans() > 0) { addInfo.append(" FM: ").append(table.getMatch().getFreeMulligans()); } @@ -150,6 +151,7 @@ public class TableView implements Serializable { stateText.append(" (").append(table.getTournament().getPlayers().size()).append('/').append(table.getNumberOfSeats()).append(')'); } infoText.append(" Time: ").append(table.getTournament().getOptions().getMatchOptions().getMatchTimeLimit().toString()); + infoText.append(" Buffer: ").append(table.getTournament().getOptions().getMatchOptions().getMatchBufferTime().toString()); if (table.getTournament().getOptions().getMatchOptions().getFreeMulligans() > 0) { infoText.append(" FM: ").append(table.getTournament().getOptions().getMatchOptions().getFreeMulligans()); } 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 8c1b23bfcb3..3f00ad33bae 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameController.java +++ b/Mage.Server/src/main/java/mage/server/game/GameController.java @@ -150,15 +150,18 @@ public class GameController implements GameCallback { if (playerId == null) { throw new MageException("RESUME_TIMER: playerId can't be null"); } + Player player = game.getState().getPlayer(playerId); + if (player == null) { + throw new MageException("RESUME_TIMER: player can't be null"); + } + timer = timers.get(playerId); if (timer == null) { - Player player = game.getState().getPlayer(playerId); - if (player != null) { - timer = createPlayerTimer(event.getPlayerId(), player.getPriorityTimeLeft()); - } else { - throw new MageException("RESUME_TIMER: player can't be null"); - } + timer = createPlayerTimer(event.getPlayerId(), player.getPriorityTimeLeft()); } + + player.setBufferTimeLeft(game.getBufferTime()); + timer.setBufferCount(game.getBufferTime()); timer.resume(); break; case PAUSE_TIMER: 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 472e6c968d2..5a5357206d5 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 @@ -3856,6 +3856,16 @@ public class TestPlayer implements Player { return computerPlayer.getPriorityTimeLeft(); } + @Override + public void setBufferTimeLeft(int timeLeft) { + computerPlayer.setBufferTimeLeft(timeLeft); + } + + @Override + public int getBufferTimeLeft() { + return computerPlayer.getBufferTimeLeft(); + } + @Override public boolean hasQuit() { return computerPlayer.hasQuit(); 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 1b5c9b622dc..52b0b6b62ef 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 @@ -1149,6 +1149,16 @@ public class PlayerStub implements Player { return 0; } + @Override + public void setBufferTimeLeft(int timeLeft) { + + } + + @Override + public int getBufferTimeLeft() { + return 0; + } + @Override public void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving) { diff --git a/Mage/src/main/java/mage/constants/MatchBufferTime.java b/Mage/src/main/java/mage/constants/MatchBufferTime.java new file mode 100644 index 00000000000..bfb93e2ca5e --- /dev/null +++ b/Mage/src/main/java/mage/constants/MatchBufferTime.java @@ -0,0 +1,41 @@ +package mage.constants; + +/** + * The time a player receives whenever the timer starts. This ticks down before their normal time, + * and refreshes to full every time the timer starts, creating a sort of buffer. Similar to how to + * chess clocks work. + * + * Based off of MatchTimeLimit + * + * @author alexander-novo + */ +public enum MatchBufferTime { + NONE(0, "None"), + SEC__05(5, "5 Seconds"), + SEC__10(10, "10 Seconds"), + SEC__15(15, "15 Seconds"), + SEC__20(20, "20 Seconds"), + SEC__25(25, "25 Seconds"), + SEC__30(30, "30 Seconds"); + + private final int matchSeconds; + private final String name; + + MatchBufferTime(int matchSeconds, String name) { + this.matchSeconds = matchSeconds; + this.name = name; + } + + public int getBufferTime() { + return matchSeconds; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 63e85a38dd3..464bc1eaff1 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -529,6 +529,10 @@ public interface Game extends MageItem, Serializable, Copyable { void setPriorityTime(int priorityTime); + int getBufferTime(); + + void setBufferTime(int bufferTime); + UUID getStartingPlayerId(); void setStartingPlayerId(UUID startingPlayerId); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 10c6c77b67d..e10dfb29f68 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -143,7 +143,8 @@ public abstract class GameImpl implements Game { private boolean scopeRelevant = false; // replacement effects: used to indicate that currently applied replacement effects have to check for scope relevance (614.12 13/01/18) private boolean saveGame = false; // replay code, not done - private int priorityTime; // match time limit + private int priorityTime; // Match time limit (per player). Set at the start of the match and only goes down. + private int bufferTime; // Buffer time before priority time starts going down. Buffer time is refreshed every time the timer starts. private final int startingLife; private final int startingHandSize; private final int minimumDeckSize; @@ -253,6 +254,7 @@ public abstract class GameImpl implements Game { this.scopeRelevant = game.scopeRelevant; this.saveGame = game.saveGame; this.priorityTime = game.priorityTime; + this.bufferTime = game.bufferTime; this.startingLife = game.startingLife; this.startingHandSize = game.startingHandSize; this.minimumDeckSize = game.minimumDeckSize; @@ -3645,6 +3647,16 @@ public abstract class GameImpl implements Game { this.priorityTime = priorityTime; } + @Override + public int getBufferTime() { + return bufferTime; + } + + @Override + public void setBufferTime(int bufferTime) { + this.bufferTime = bufferTime; + } + @Override public UUID getStartingPlayerId() { return startingPlayerId; diff --git a/Mage/src/main/java/mage/game/match/MatchImpl.java b/Mage/src/main/java/mage/game/match/MatchImpl.java index 535b768ccdd..1ab1ddbeaf7 100644 --- a/Mage/src/main/java/mage/game/match/MatchImpl.java +++ b/Mage/src/main/java/mage/game/match/MatchImpl.java @@ -201,9 +201,11 @@ public abstract class MatchImpl implements Match { // set the priority time left for the match if (games.isEmpty()) { // first game full time matchPlayer.getPlayer().setPriorityTimeLeft(options.getPriorityTime()); + matchPlayer.getPlayer().setBufferTimeLeft(options.getBufferTime()); } else { if (matchPlayer.getPriorityTimeLeft() > 0) { matchPlayer.getPlayer().setPriorityTimeLeft(matchPlayer.getPriorityTimeLeft()); + matchPlayer.getPlayer().setBufferTimeLeft(options.getBufferTime()); } } } else { @@ -213,6 +215,7 @@ public abstract class MatchImpl implements Match { } } game.setPriorityTime(options.getPriorityTime()); + game.setBufferTime(options.getBufferTime()); } protected void shufflePlayers() { diff --git a/Mage/src/main/java/mage/game/match/MatchOptions.java b/Mage/src/main/java/mage/game/match/MatchOptions.java index 0fd193193e3..dc0dfb0eee6 100644 --- a/Mage/src/main/java/mage/game/match/MatchOptions.java +++ b/Mage/src/main/java/mage/game/match/MatchOptions.java @@ -1,6 +1,7 @@ package mage.game.match; +import mage.constants.MatchBufferTime; import mage.constants.MatchTimeLimit; import mage.constants.MultiplayerAttackOption; import mage.constants.RangeOfInfluence; @@ -48,6 +49,7 @@ public class MatchOptions implements Serializable { * Time each player has during the game to play using his\her priority. */ protected MatchTimeLimit matchTimeLimit; // 0 = no priorityTime handling + protected MatchBufferTime matchBufferTime; // Amount of time each player gets before their normal time limit counts down. Refreshes each time the normal timer is invoked. protected MulliganType mulliganType; /*public MatchOptions(String name, String gameType) { @@ -160,6 +162,21 @@ public class MatchOptions implements Serializable { this.matchTimeLimit = matchTimeLimit; } + public int getBufferTime() { + if (matchBufferTime == null) { + return MatchBufferTime.NONE.getBufferTime(); + } + return matchBufferTime.getBufferTime(); + } + + public MatchBufferTime getMatchBufferTime() { + return this.matchBufferTime; + } + + public void setMatchBufferTime(MatchBufferTime matchBufferTime) { + this.matchBufferTime = matchBufferTime; + } + public String getPassword() { return password; } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 26fc0aab0f8..da497976b74 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -854,6 +854,20 @@ public interface Player extends MageItem, Copyable { */ int getPriorityTimeLeft(); + /** + * Set seconds left before priority time starts ticking down. + * + * @param timeLeft + */ + void setBufferTimeLeft(int timeLeft); + + /** + * Returns seconds left before priority time starts ticking down. + * + * @return + */ + int getBufferTimeLeft(); + void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving); boolean hasReachedNextTurnAfterLeaving(); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 321e04fbf87..5454d412472 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -119,6 +119,7 @@ public abstract class PlayerImpl implements Player, Serializable { protected int turns; protected int storedBookmark = -1; protected int priorityTimeLeft = Integer.MAX_VALUE; + protected int bufferTimeLeft = 0; // conceded or connection lost game protected boolean left; @@ -275,6 +276,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.justActivatedType = player.justActivatedType; this.priorityTimeLeft = player.getPriorityTimeLeft(); + this.bufferTimeLeft = player.getBufferTimeLeft(); this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); @@ -4454,6 +4456,16 @@ public abstract class PlayerImpl implements Player, Serializable { return priorityTimeLeft; } + @Override + public void setBufferTimeLeft(int timeLeft) { + bufferTimeLeft = timeLeft; + } + + @Override + public int getBufferTimeLeft() { + return bufferTimeLeft; + } + @Override public boolean hasQuit() { return quit;