package net.sf.colossus.gui; import java.awt.AWTEvent; import java.awt.AWTException; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.Robot; import java.awt.Toolkit; import java.awt.event.AWTEventListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import net.sf.colossus.appmain.WelcomeDialog; import net.sf.colossus.client.Client; import net.sf.colossus.client.LegionClientSide; import net.sf.colossus.common.Constants; import net.sf.colossus.common.Options; import net.sf.colossus.game.Legion; import net.sf.colossus.game.Phase; import net.sf.colossus.game.Player; import net.sf.colossus.game.PlayerColor; import net.sf.colossus.guiutil.KFrame; import net.sf.colossus.guiutil.SaveWindow; import net.sf.colossus.server.XMLSnapshotFilter; import net.sf.colossus.util.ArrayHelper; import net.sf.colossus.util.BuildInfo; import net.sf.colossus.util.HTMLColor; import net.sf.colossus.util.NullCheckPredicate; import net.sf.colossus.util.StaticResourceLoader; import net.sf.colossus.util.SystemInfo; import net.sf.colossus.variant.CreatureType; import net.sf.colossus.variant.MasterHex; import net.sf.colossus.variant.Variant; /** * Class MasterBoard implements the GUI for a Titan masterboard. * * @author David Ripton * @author Romain Dolbeau */ public final class MasterBoard extends JPanel { private static final Logger LOGGER = Logger.getLogger(MasterBoard.class .getName()); private Image offScreenBuffer; private boolean overlayChanged = false; private GUIMasterHex[][] guiHexArray = null; private Client client; private final ClientGUI gui; private KFrame masterFrame; private ShowReadme showReadme; private ShowHelpDoc showHelpDoc; private JMenu editMenu; private JMenu phaseMenu; private JPopupMenu popupMenu; private JPopupMenu popupMenuWithLegions; private Map<String, JCheckBoxMenuItem> checkboxes = new HashMap<String, JCheckBoxMenuItem>(); private JPanel[] legionFlyouts; private final MasterBoardWindowHandler mbwh; private InfoPopupHandler iph; /** Last point clicked is needed for popup menus. */ private Point lastPoint; /** * engage() has been sent to server but answer (tellEngagement()) not * received yet; mostly we have this, to be able to react properly when * user clicks on an engagement while there is still the server response * missing for the last one */ private MasterHex engagingPendingHex = null; /** * In that time while we got tellEngagement but nothing else * (bottom bar just tells engaged, but no other visible notice of what's * going on), we might be waiting for the opponent to think about * whether to flee or not. So if user is impatient and clicks a hex * with any (same, or other) engagement, inform him we are already engaged * right now and probably opponent is currently thinking about whether to * flee or not. Once we receive showConcede or showNegotiate messages, * this time window is over, after that we don't say this about the * "perhaps opponent is currently thinking whether to flee" any more. */ private boolean defenderFleePhase = false; /** * Show the message that "saving during engagement/battle will store the * last commit point" only once each game - flag that it has been shown */ private boolean saveDuringEngagementDialogMessageShown = false; /** * List of markers which are currently on the board, * for painting in z-order => the end of the list is on top. * * Now synchronized access to prevent NPEs when EDT wants to * paint a marker and asks for the legion for it, and * legion has just been removed. * I don't use a synchronizedList, because then I get into * trouble in the recreateMarkers method. */ private final LinkedHashMap<Legion, Marker> legionToMarkerMap = new LinkedHashMap<Legion, Marker>(); private final Map<Legion, Chit> recruitedChits = new HashMap<Legion, Chit>(); private final Map<MasterHex, List<Chit>> possibleRecruitChits = new HashMap<MasterHex, List<Chit>>(); /** The scrollbarspanel, needed to correct lastPoint. */ private JScrollPane scrollPane; private final Container contentPane; /** our own little bar implementation */ private BottomBar bottomBar; static Toolkit tk = Toolkit.getDefaultToolkit(); static long eventMask = AWTEvent.MOUSE_EVENT_MASK + AWTEvent.KEY_EVENT_MASK; private boolean gameOverStateReached = false; private static final String clearRecruitChits = "Clear recruit chits"; private static final String skipLegion = "Skip legion this time"; private static final String nextLegion = "Next legion"; private static final String destroyLegion = "Destroy legion"; private static final String undoLast = "Undo"; private static final String undoAll = "Undo All"; private static final String doneWithPhase = "Done"; private static final String forcedDoneWithPhase = "Forced Done"; private static final String kickPhase = "Kick phase"; private static final String takeMulligan = "Take Mulligan"; private static final String requestExtraRoll = "Request extra roll"; private static final String withdrawFromGame = "Withdraw from Game"; private static final String viewWebClient = "View Web Client"; private static final String viewFullRecruitTree = "View Full Recruit Tree"; private static final String viewHexRecruitTree = "View Hex Recruit Tree"; private static final String viewBattleMap = "View Battle Map"; private static final String viewLegions = "View Legion(s)"; private static final String chooseScreen = "Choose Screen For Info Windows"; private static final String preferences = "Preferences..."; private static final String about = "About"; private static final String viewReadme = "Show Variant Readme"; private static final String viewHelpDoc = "Options Documentation"; private static final String viewWelcome = "Show Welcome message"; private AbstractAction newGameAction; private AbstractAction loadGameAction; private AbstractAction saveGameAction; private AbstractAction saveGameAsAction; private AbstractAction suspendGameAction; private AbstractAction suspendNoSaveGameAction; private AbstractAction closeBoardAction; private AbstractAction quitGameAction; private AbstractAction cleanDisconnectAction; private AbstractAction enforcedDisconnectByServerAction; private AbstractAction tryReconnectAction; private AbstractAction checkConnectionAction; private AbstractAction checkAllConnectionsAction; private AbstractAction clearRecruitChitsAction; private AbstractAction skipLegionAction; private AbstractAction nextLegionAction; private AbstractAction destroyLegionAction; private AbstractAction undoLastAction; private AbstractAction undoAllAction; private AbstractAction doneWithPhaseAction; private AbstractAction forcedDoneWithPhaseAction; private AbstractAction kickPhaseAction; private AbstractAction takeMulliganAction; private AbstractAction requestExtraRollAction; private AbstractAction withdrawFromGameAction; private AbstractAction viewWebClientAction; private AbstractAction viewFullRecruitTreeAction; private AbstractAction viewHexRecruitTreeAction; private AbstractAction viewBattleMapAction; private AbstractAction viewLegionsAction; private AbstractAction chooseScreenAction; private AbstractAction preferencesAction; private AbstractAction aboutAction; private AbstractAction viewReadmeAction; private AbstractAction viewHelpDocAction; private AbstractAction viewWelcomeAction; private boolean playerLabelDone; private SaveWindow saveWindow; private String cachedPlayerName = "<not set yet>"; EditLegion editLegionOngoing = null; EditLegion relocateOngoing = null; private final class InfoPopupHandler extends KeyAdapter { private static final int POPUP_KEY_ALL_LEGIONS = KeyEvent.VK_SHIFT; private static final int POPUP_KEY_MY_LEGIONS = KeyEvent.VK_CONTROL; private static final int PANEL_MARGIN = 4; private static final int PANEL_PADDING = 0; private final WeakReference<Client> clientRef; private InfoPopupHandler(Client client) { super(); this.clientRef = new WeakReference<Client>(client); net.sf.colossus.util.InstanceTracker.register(this, gui .getOwningPlayer().getName()); } @Override public void keyPressed(KeyEvent e) { Client client = clientRef.get(); if (client == null) { return; } if (e.getKeyCode() == POPUP_KEY_ALL_LEGIONS) { if (legionFlyouts == null) { synchronized (legionToMarkerMap) { createLegionFlyouts(legionToMarkerMap.values()); } } } else if (e.getKeyCode() == POPUP_KEY_MY_LEGIONS) { if (legionFlyouts == null) { // copy only local players markers List<Marker> myMarkers = new ArrayList<Marker>(); synchronized (legionToMarkerMap) { for (Entry<Legion, Marker> entry : legionToMarkerMap .entrySet()) { Legion legion = entry.getKey(); if (client.isMyLegion(legion)) { myMarkers.add(entry.getValue()); } } createLegionFlyouts(myMarkers); } } } else { super.keyPressed(e); } } private void createLegionFlyouts(Collection<Marker> markers) { // copy to array so we don't get concurrent modification // exceptions when iterating Marker[] markerArray = markers.toArray(new Marker[markers.size()]); legionFlyouts = new JPanel[markers.size()]; for (int i = 0; i < markerArray.length; i++) { Marker marker = markerArray[i]; LegionClientSide legion = client.getLegion(marker.getId()); int scale = 2 * Scale.get(); boolean dubiousAsBlanks = gui.getOptions().getOption( Options.dubiousAsBlanks); boolean showMarker = gui.getOptions().getOption( Options.showMarker); final JPanel panel = new LegionInfoPanel(legion, scale, PANEL_MARGIN, PANEL_PADDING, true, gui.getEffectiveViewMode(), client.isMyLegion(legion), dubiousAsBlanks, true, showMarker); add(panel); legionFlyouts[i] = panel; panel.setLocation(marker.getLocation()); panel.setVisible(true); DragListener.makeDraggable(panel); repaint(); } } @Override public void keyReleased(KeyEvent e) { if ((e.getKeyCode() == POPUP_KEY_ALL_LEGIONS) || (e.getKeyCode() == POPUP_KEY_MY_LEGIONS)) { if (legionFlyouts != null) { for (JPanel flyout : legionFlyouts) { remove(flyout); } repaint(); legionFlyouts = null; } } else { super.keyReleased(e); } } } public MasterBoard(final Client client, ClientGUI gui) { setLayout(null); this.client = client; this.gui = gui; net.sf.colossus.util.InstanceTracker.register(this, gui.getOwningPlayerName()); String playerName = gui.getOwningPlayerName(); if (playerName == null) { playerName = "unknown"; } masterFrame = new KFrame("MasterBoard " + playerName); masterFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); contentPane = masterFrame.getContentPane(); contentPane.setLayout(new BorderLayout()); setOpaque(true); setupIcon(); setBackground(Color.black); this.mbwh = new MasterBoardWindowHandler(); masterFrame.addWindowListener(mbwh); addMouseListener(new MasterBoardMouseHandler()); addMouseMotionListener(new MasterBoardMouseMotionHandler()); this.iph = new InfoPopupHandler(client); this.addKeyListener(this.iph); tk.addAWTEventListener(new AWTEventListener() { @Override public void eventDispatched(AWTEvent e) { if (e instanceof java.awt.event.MouseEvent) { // ignore window entry/exit events // (they would also occur if window is in background, but // small part of it is visible between two other windows, // and the mouse is for a short period over that visible // part) if (((MouseEvent)e).getButton() != MouseEvent.NOBUTTON) { MasterBoard.this.gui.markThatSomethingHappened(); } } } }, eventMask); setupGUIHexes(); setupActions(); setupPopupMenus(); setupTopMenu(); scrollPane = new JScrollPane(this); contentPane.add(scrollPane, BorderLayout.CENTER); setupPlayerLabel(); saveWindow = new SaveWindow(gui.getOptions(), "MasterBoardScreen"); Point loadLocation = saveWindow.loadLocation(); if (loadLocation == null) { // Copy of code from KDialog.centerOnScreen(); Dimension d = Toolkit.getDefaultToolkit().getScreenSize(); masterFrame.setLocation(new Point(d.width / 2 - getSize().width / 2, d.height / 2 - getSize().height / 2)); } else { masterFrame.setLocation(loadLocation); } masterFrame.pack(); masterFrame.setVisible(true); // it seems Board needs to request at least once focus, // otherwise the pop-up keys (Legion FlyOuts) do not work. // YES; here we really mean this.requestFocus(), not the one which // does it only if stealFocus option is enabled. requestFocus(); } // For HotSeatMode public void setBoardActive(boolean val) { if (val) { masterFrame.setExtendedState(JFrame.NORMAL); masterFrame.repaint(); maybeRequestFocusAndToFront(); } else { masterFrame.setExtendedState(JFrame.ICONIFIED); } } public void enableSaveActions() { saveGameAction.setEnabled(true); saveGameAsAction.setEnabled(true); } /** * Inform the user that saving during an engagement will save the last * commit point, so loading it will re-set game to just before the * engagement was started. * Only if user closes the dialog, no save will be done. * * @return True if saving shall be done anyway */ private boolean saveDuringEngagementDialog() { if (saveDuringEngagementDialogMessageShown) { return true; } saveDuringEngagementDialogMessageShown = true; String[] options = new String[1]; options[0] = "OK"; int answer = JOptionPane.showOptionDialog(masterFrame, "Saving/Loading of game state while an engagement or battle " + "is ongoing is not implemented!\n" + "Saved game will store the game state " + "from the point just before the engagement started.\n\n" + "(this message will only be shown once in each game)", "Save at this phase not implemented!", JOptionPane.OK_OPTION, JOptionPane.INFORMATION_MESSAGE, null, options, options[0]); return (answer == JOptionPane.OK_OPTION); } private Constants.ConfirmVals confirmDialog(String text, int count) { String[] options = new String[3]; options[0] = "Yes"; options[1] = "No"; options[2] = "Don't ask again"; int answer; String message = "You have "; if (count == 1) message = message + "a legion "; else message = message + count + " legions "; answer = JOptionPane.showOptionDialog(this, message + text + ", are you sure you are done?", "Confirm done?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[1]); if (answer == 0) { return Constants.ConfirmVals.Yes; } if (answer == 1 || answer == -1) { return Constants.ConfirmVals.No; } return Constants.ConfirmVals.DoNotAsk; } private void setupActions() { clearRecruitChitsAction = new AbstractAction(clearRecruitChits) { public void actionPerformed(ActionEvent e) { clearRecruitedChits(); clearPossibleRecruitChits(); // TODO Only repaint needed hexes. repaint(); } }; skipLegionAction = new AbstractAction(skipLegion) { public void actionPerformed(ActionEvent e) { markLegionSkip(); } }; nextLegionAction = new AbstractAction(nextLegion) { public void actionPerformed(ActionEvent e) { jumpToNextUnhandledLegion(); } }; destroyLegionAction = new AbstractAction(destroyLegion) { public void actionPerformed(ActionEvent e) { destroyThisLegion(); } }; undoLastAction = new AbstractAction(undoLast) { public void actionPerformed(ActionEvent e) { Phase phase = client.getPhase(); if (phase == Phase.SPLIT) { gui.undoLastSplit(); alignAllLegions(); highlightTallLegions(); repaint(); } else if (phase == Phase.MOVE) { clearRecruitedChits(); clearPossibleRecruitChits(); gui.undoLastMove(); highlightUnmovedLegions(); } else if (phase == Phase.FIGHT) { LOGGER.log(Level.SEVERE, "called undoLastAction in FIGHT"); } else if (phase == Phase.MUSTER) { gui.undoLastRecruit(); highlightPossibleRecruitLegionHexes(); } else { LOGGER.log(Level.SEVERE, "Bogus phase for Undo Last"); } } }; undoAllAction = new AbstractAction(undoAll) { public void actionPerformed(ActionEvent e) { Phase phase = client.getPhase(); if (phase == Phase.SPLIT) { gui.undoAllSplits(); alignAllLegions(); highlightTallLegions(); repaint(); } else if (phase == Phase.MOVE) { gui.undoAllMoves(); highlightUnmovedLegions(); if (gui.isMyTurn()) { updateLegionsLeftToMoveText(true); } } else if (phase == Phase.FIGHT) { LOGGER.log(Level.SEVERE, "called undoAllAction in FIGHT"); } else if (phase == Phase.MUSTER) { gui.undoAllRecruits(); highlightPossibleRecruitLegionHexes(); if (gui.isMyTurn()) { updateLegionsLeftToMusterText(); } } else { LOGGER.log(Level.SEVERE, "Bogus phase for Undo All"); } } }; doneWithPhaseAction = new AbstractAction(doneWithPhase) { public void actionPerformed(ActionEvent e) { if (gameOverStateReached) { gui.askNewCloseQuitCancel(masterFrame, false); } else { // first set disabled... doneWithPhaseAction.setEnabled(false); // ... because response from server might set it // to enabled again doneWithPhase(); } focusBackToMasterboard(); } }; // will be enabled if it is player's turn doneWithPhaseAction.setEnabled(false); forcedDoneWithPhaseAction = new AbstractAction(forcedDoneWithPhase) { public void actionPerformed(ActionEvent e) { doneWithPhase(); } }; // make this always be available forcedDoneWithPhaseAction.setEnabled(true); kickPhaseAction = new AbstractAction(kickPhase) { public void actionPerformed(ActionEvent e) { gui.client.getAutoplay().switchOnInactivityAutoplay(); gui.client.getEventExecutor().retriggerEvent(); // gui.getClient().kickPhase(); } }; takeMulliganAction = new AbstractAction(takeMulligan) { public void actionPerformed(ActionEvent e) { client.mulligan(); } }; requestExtraRollAction = new AbstractAction(requestExtraRoll) { public void actionPerformed(ActionEvent e) { /* * This is needed, because it's too difficult to orchestrate * from server side to undo all moves and clear undoStack * etc. (that problem does not arise with mulligan because * there the undo can be done when sending request). * Didn't want to add yet another 'mulligan approved' message, * and worse, there might be a timing problem (undoMoves * ongoing while new roll arives). */ if (gui.isUndoStackEmpty()) { client.requestExtraRoll(); requestExtraRollAction.setEnabled(false); } else { JOptionPane.showMessageDialog(masterFrame, "To request the extra roll, you need to undo all moves " + "first (press 'A' or multiple times 'U')!", "Undo all moves first", JOptionPane.INFORMATION_MESSAGE); } } }; withdrawFromGameAction = new AbstractAction(withdrawFromGame) { public void actionPerformed(ActionEvent e) { if (!client.isAlive()) { JOptionPane.showMessageDialog(masterFrame, "You can't withdraw, " + " because you are already dead!", "You're already dead, dummy!", JOptionPane.INFORMATION_MESSAGE); return; } String[] options = new String[2]; options[0] = "Yes"; options[1] = "No"; int answer = JOptionPane.showOptionDialog(masterFrame, "Are you sure you wish to withdraw from the game?", "Confirm Withdrawal?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[1]); if (answer == JOptionPane.YES_OPTION) { client.withdrawFromGame(); } } }; viewFullRecruitTreeAction = new AbstractAction(viewFullRecruitTree) { public void actionPerformed(ActionEvent e) { Variant variant = gui.getGame().getVariant(); new ShowAllRecruits(masterFrame, gui.getOptions(), variant, gui); } }; viewWebClientAction = new AbstractAction(viewWebClient) { public void actionPerformed(ActionEvent e) { gui.showWebClient(); } }; viewHexRecruitTreeAction = new AbstractAction(viewHexRecruitTree) { public void actionPerformed(ActionEvent e) { GUIMasterHex hex = getHexContainingPoint(lastPoint); if (hex != null) { // TODO replace with actual ...getVariant() when Variant is // ready to provide the needed data Variant variant = gui.getGame().getVariant(); MasterHex hexModel = hex.getHexModel(); new ShowRecruits(masterFrame, lastPoint, hexModel, scrollPane, variant, gui); } } }; viewBattleMapAction = new AbstractAction(viewBattleMap) { public void actionPerformed(ActionEvent e) { GUIMasterHex hex = getHexContainingPoint(lastPoint); if (hex != null) { showBattleMap(hex); } } }; viewLegionsAction = new AbstractAction(viewLegions) { public void actionPerformed(ActionEvent e) { if (lastPoint == null) { LOGGER.warning("Action viewLegions called but " + "lastPoint is null."); return; } MasterBoard.this.viewLegions(lastPoint); } }; /* * After confirmation (if necessary, i.e. not gameover yet), * totally quit everything (shut down server and all windows) * so that the ViableEntityManager knows it can let the main * go to the end, ending the JVM. */ quitGameAction = new AbstractAction(Constants.quitGame) { public void actionPerformed(ActionEvent e) { boolean quitAll = false; if (gui.getGame().isGameOver()) { quitAll = true; } else { String[] options = new String[2]; options[0] = "Yes"; options[1] = "No"; int answer = JOptionPane.showOptionDialog(masterFrame, "Are you sure you wish to stop this game and quit?", "Quit Game?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[1]); if (answer == JOptionPane.YES_OPTION) { quitAll = true; } } if (quitAll) { // In startObject, set up what to do next gui.menuQuitGame(); } } }; cleanDisconnectAction = new AbstractAction(Constants.cleanDisconnect) { public void actionPerformed(ActionEvent e) { LOGGER.info("Pure disconnect by client (from File Menu)!"); client.abandonCurrentConnection(); } }; enforcedDisconnectByServerAction = new AbstractAction( Constants.enforcedDisconnectByServer) { public void actionPerformed(ActionEvent e) { LOGGER.info("Enforcing disconnect by server!"); gui.getClient().enforcedDisconnectByServer(); } }; tryReconnectAction = new AbstractAction(Constants.tryReconnect) { public void actionPerformed(ActionEvent e) { client.guiTriggeredTryReconnect(); } }; newGameAction = new AbstractAction(Constants.newGame) { public void actionPerformed(ActionEvent e) { if (!gui.getGame().isGameOver()) { String[] options = new String[2]; options[0] = "Yes"; options[1] = "No"; int answer = JOptionPane.showOptionDialog(masterFrame, "Are you sure you want to stop this game and " + "start a new one?", "New Game?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[1]); if (answer != JOptionPane.YES_OPTION) { return; } } gui.menuNewGame(); } }; loadGameAction = new AbstractAction(Constants.loadGame) { public void actionPerformed(ActionEvent e) { // No need for confirmation because the user can cancel // from the load game dialog. JFileChooser chooser = new JFileChooser( Constants.SAVE_DIR_NAME); chooser.setFileFilter(new XMLSnapshotFilter()); int returnVal = chooser.showOpenDialog(masterFrame); if (returnVal == JFileChooser.APPROVE_OPTION) { gui.menuLoadGame(chooser.getSelectedFile().getPath()); } } }; saveGameAction = new AbstractAction(Constants.saveGame) { public void actionPerformed(ActionEvent e) { boolean proceed = true; if (gui.getGame().isEngagementOngoing()) { proceed = saveDuringEngagementDialog(); } if (proceed) { gui.menuSaveGame(null); } } }; saveGameAsAction = new AbstractAction(Constants.saveGameAs) { // TODO: Need a confirmation dialog on overwrite? public void actionPerformed(ActionEvent e) { if (gui.getGame().isEngagementOngoing()) { boolean proceed = saveDuringEngagementDialog(); if (!proceed) { return; } } File savesDir = new File(Constants.SAVE_DIR_NAME); if (!savesDir.exists()) { LOGGER.log(Level.INFO, "Trying to make directory " + savesDir.toString()); if (!savesDir.mkdirs()) { LOGGER.log(Level.SEVERE, "Could not create saves directory"); JOptionPane .showMessageDialog( masterFrame, "Could not create directory " + savesDir + "\n- FileChooser dialog box will default " + "to some other (system dependent) directory!", "Creating directory " + savesDir + " failed!", JOptionPane.ERROR_MESSAGE); } } else if (!savesDir.isDirectory()) { LOGGER.log(Level.SEVERE, "Can't create directory " + savesDir.toString() + " - name exists but is not a file"); JOptionPane.showMessageDialog(masterFrame, "Can't create directory " + savesDir + " (name exists, but is not a file)\n" + "- FileChooser dialog box will default to " + "some other (system dependent) directory!", "Creating directory " + savesDir + " failed!", JOptionPane.ERROR_MESSAGE); } JFileChooser chooser = new JFileChooser(savesDir); chooser.setFileFilter(new XMLSnapshotFilter()); int returnVal = chooser.showSaveDialog(masterFrame); if (returnVal == JFileChooser.APPROVE_OPTION) { String dirname = chooser.getCurrentDirectory() .getAbsolutePath(); String basename = chooser.getSelectedFile().getName(); // Add default savegame extension. if (!basename.endsWith(Constants.XML_EXTENSION)) { basename += Constants.XML_EXTENSION; } gui.menuSaveGame(dirname + '/' + basename); } } }; suspendGameAction = new AbstractAction(Constants.suspendGame) { public void actionPerformed(ActionEvent e) { boolean proceed = true; /* if (gui.getGame().isEngagementOngoing()) { proceed = saveDuringEngagementDialog(); } */ if (proceed) { gui.menuSuspendGame(true); } } }; suspendNoSaveGameAction = new AbstractAction( Constants.suspendGameNoSave) { public void actionPerformed(ActionEvent e) { boolean proceed = true; /* if (gui.getGame().isEngagementOngoing()) { proceed = saveDuringEngagementDialog(); } */ if (proceed) { gui.menuSuspendGame(false); } } }; /* * after confirmation, close board and perhaps battle board, but * Webclient (and server, if running here), will go on. */ closeBoardAction = new AbstractAction(Constants.closeBoard) { public void actionPerformed(ActionEvent e) { boolean closeBoard = false; if (gui.getGame().isGameOver() || !client.isAlive() || client.isSpectator()) { closeBoard = true; } else { String[] options = new String[2]; options[0] = "Yes"; options[1] = "No"; int answer = JOptionPane .showOptionDialog( masterFrame, "Are you sure you wish to withdraw and close the board?", "Close Board?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[1]); if (answer == JOptionPane.YES_OPTION) { closeBoard = true; } } if (closeBoard) { gui.menuCloseBoard(); } } }; checkConnectionAction = new AbstractAction(Constants.checkConnection) { public void actionPerformed(ActionEvent e) { gui.checkServerConnection(); } }; checkAllConnectionsAction = new AbstractAction( Constants.checkAllConnections) { public void actionPerformed(ActionEvent e) { gui.checkAllConnections(); } }; chooseScreenAction = new AbstractAction(chooseScreen) { public void actionPerformed(ActionEvent e) { new ChooseScreen(getFrame(), gui); } }; preferencesAction = new AbstractAction(preferences) { public void actionPerformed(ActionEvent e) { gui.setPreferencesWindowVisible(true); } }; aboutAction = new AbstractAction(about) { public void actionPerformed(ActionEvent e) { String buildInfo = BuildInfo.getFullBuildInfoString() + "\n" + "user.home: " + System.getProperty("user.home"); String colossusHome = System.getProperty("user.home") + File.separator + ".colossus"; String logDirectory = getLogDirectory(); JOptionPane.showMessageDialog(masterFrame, "" + "Colossus Version: " + BuildInfo.getReleaseVersion() + "\n" + "Build: " + buildInfo + "\n" + "Colossus home: " + colossusHome + "\n" + "Log directory: " + logDirectory + "\n" + "Java Version: " + SystemInfo.getDisplayJavaInfo(), "About Colossus", JOptionPane.INFORMATION_MESSAGE); } }; viewReadmeAction = new AbstractAction(viewReadme) { public void actionPerformed(ActionEvent e) { if (showReadme != null) { showReadme.dispose(); } showReadme = new ShowReadme(gui.getGame().getVariant()); } }; viewHelpDocAction = new AbstractAction(viewHelpDoc) { public void actionPerformed(ActionEvent e) { if (showHelpDoc != null) { showHelpDoc.dispose(); } showHelpDoc = new ShowHelpDoc(); } }; viewWelcomeAction = new AbstractAction(viewWelcome) { public void actionPerformed(ActionEvent e) { WelcomeDialog.showWelcomeDialog(); } }; } // Derive the logDirectory from the FileHandler.pattern property // (in case someone modified the default in logging.properties file) // to show it in Help-About dialog. private String getLogDirectory() { String propName = "java.util.logging.FileHandler.pattern"; String logPattern = LogManager.getLogManager().getProperty(propName); if (logPattern == null) { return "<UNKNOWN> (system property " + " \"java.util.logging.FileHandler.pattern\" is not set?)"; } // Replace %t with Java's idea of system temp directory: if (logPattern.startsWith("%t")) { String tempDir = System.getProperty("java.io.tmpdir"); logPattern = tempDir + logPattern.substring(2); } // get only the directory part: String logDirectory = logPattern; try { File logFileWouldBe = new File(logPattern); logDirectory = logFileWouldBe.getParent(); } catch (Exception ex) { LOGGER.warning("Exception while trying to determine log " + "directory from logPattern " + logPattern + " - ignoring it."); } // initialize logPath with what we have so far... String logPath = logDirectory; // ... and try to resolve "DOCUME~1" stuff to real names on windows: try { File logDir = new File(logDirectory); logPath = logDir.getCanonicalPath(); } catch (Exception exc) { // ignore it... } return logPath; } public void doQuitGameAction() { quitGameAction.actionPerformed(null); } public void showBattleMap(GUIMasterHex hex) { new ShowBattleMap(masterFrame, gui, hex); } private void setupPopupMenus() { popupMenu = new JPopupMenu(); contentPane.add(popupMenu); JMenuItem mi = popupMenu.add(viewHexRecruitTreeAction); mi.setMnemonic(KeyEvent.VK_R); mi = popupMenu.add(viewBattleMapAction); mi.setMnemonic(KeyEvent.VK_B); popupMenuWithLegions = new JPopupMenu(); contentPane.add(popupMenu); mi = popupMenuWithLegions.add(viewHexRecruitTreeAction); mi.setMnemonic(KeyEvent.VK_R); mi = popupMenuWithLegions.add(viewBattleMapAction); mi.setMnemonic(KeyEvent.VK_B); mi = popupMenuWithLegions.add(viewLegionsAction); mi.setMnemonic(KeyEvent.VK_L); } public void viewLegions(Point point) { GUIMasterHex hex = getHexContainingPoint(point); if (hex == null) { return; } int viewMode = gui.getEffectiveViewMode(); boolean dubiousAsBlanks = gui.getOptions().getOption( Options.dubiousAsBlanks); boolean showMarker = gui.getOptions().getOption(Options.showMarker); List<Legion> legions = gui.getGame() .getLegionsByHex(hex.getHexModel()); for (Legion legion : legions) { Marker marker = legionToMarkerMap.get(legion); int chitScale = marker.getBounds().width; Point markerPoint = marker.getLocation(); // upper right corner markerPoint.setLocation(markerPoint.x + chitScale, markerPoint.y); new ShowLegion(masterFrame, (LegionClientSide)legion, markerPoint, scrollPane, 4 * Scale.get(), viewMode, client.isMyLegion(legion), dubiousAsBlanks, showMarker); } } private ItemListener itemHandler = new MasterBoardItemHandler(); private JCheckBoxMenuItem addCheckBox(JMenu menu, String name, int mnemonic) { JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(name); cbmi.setMnemonic(mnemonic); cbmi.setSelected(gui.getOptions().getOption(name)); cbmi.addItemListener(itemHandler); menu.add(cbmi); checkboxes.put(name, cbmi); return cbmi; } private void cleanCBListeners() { Iterator<String> it = checkboxes.keySet().iterator(); while (it.hasNext()) { String key = it.next(); JCheckBoxMenuItem cbmi = checkboxes.get(key); cbmi.removeItemListener(itemHandler); } checkboxes.clear(); checkboxes = null; itemHandler = null; } private void setupTopMenu() { JMenuBar menuBar = new JMenuBar(); masterFrame.setJMenuBar(menuBar); // File menu JMenu fileMenu = new JMenu("File"); fileMenu.setMnemonic(KeyEvent.VK_F); menuBar.add(fileMenu); JMenuItem mi; mi = fileMenu.add(newGameAction); mi.setMnemonic(KeyEvent.VK_N); if (!client.isRemote()) { // saving not possible during game setup phase; enable later saveGameAction.setEnabled(false); saveGameAsAction.setEnabled(false); mi = fileMenu.add(loadGameAction); mi.setMnemonic(KeyEvent.VK_L); mi = fileMenu.add(saveGameAction); mi.setMnemonic(KeyEvent.VK_S); mi = fileMenu.add(saveGameAsAction); mi.setMnemonic(KeyEvent.VK_A); } boolean showMenuItem = gui.getStartedByWebClient(); // showMenuItem = true; // for local testing if (showMenuItem) { mi = fileMenu.add(suspendGameAction); mi.setMnemonic(KeyEvent.VK_U); mi = fileMenu.add(suspendNoSaveGameAction); mi.setMnemonic(KeyEvent.VK_O); } fileMenu.addSeparator(); mi = fileMenu.add(checkConnectionAction); mi.setMnemonic(KeyEvent.VK_K); mi = fileMenu.add(checkAllConnectionsAction); mi.setMnemonic(KeyEvent.VK_X); /* Removed for r-0.13.2, since it does not work - it merely makes the * client stop processing stuff from the queue, but ClientSocketThread * keeps receiving and ack'ing lines from/to server. */ boolean _CLEAN_DISCONNECT = false; if (_CLEAN_DISCONNECT) { mi = fileMenu.add(cleanDisconnectAction); mi.setMnemonic(KeyEvent.VK_Y); } mi = fileMenu.add(tryReconnectAction); mi.setMnemonic(KeyEvent.VK_R); // only for debugging/testing boolean _DISCONNECT_BY_SERVER = true; if (!client.isRemote() && _DISCONNECT_BY_SERVER) { mi = fileMenu.add(enforcedDisconnectByServerAction); mi.setMnemonic(KeyEvent.VK_Z); } fileMenu.addSeparator(); mi = fileMenu.add(closeBoardAction); mi.setMnemonic(KeyEvent.VK_C); mi = fileMenu.add(quitGameAction); mi.setMnemonic(KeyEvent.VK_Q); // Edit menu if (gui.getOptions().getOption(Options.enableEditingMode) && !client.isRemote()) { editMenu = new JMenu("Edit"); editMenu.setMnemonic(KeyEvent.VK_E); menuBar.add(editMenu); addCheckBox(editMenu, Options.editModeActive, KeyEvent.VK_E); } // Phase menu phaseMenu = new JMenu("Phase"); phaseMenu.setMnemonic(KeyEvent.VK_P); menuBar.add(phaseMenu); mi = phaseMenu.add(clearRecruitChitsAction); mi.setMnemonic(KeyEvent.VK_C); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, 0)); mi = phaseMenu.add(skipLegionAction); mi.setMnemonic(KeyEvent.VK_S); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0)); mi = phaseMenu.add(nextLegionAction); mi.setMnemonic(KeyEvent.VK_N); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, 0)); boolean CLEMENS_TEST_2 = false; if (CLEMENS_TEST_2) { mi = phaseMenu.add(destroyLegionAction); mi.setMnemonic(KeyEvent.VK_R); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0)); } phaseMenu.addSeparator(); mi = phaseMenu.add(undoLastAction); mi.setMnemonic(KeyEvent.VK_U); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_U, 0)); mi = phaseMenu.add(undoAllAction); mi.setMnemonic(KeyEvent.VK_A); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0)); mi = phaseMenu.add(doneWithPhaseAction); mi.setMnemonic(KeyEvent.VK_D); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0)); mi = phaseMenu.add(forcedDoneWithPhaseAction); mi.setMnemonic(KeyEvent.VK_F); boolean CLEMENS_TEST = false; if (CLEMENS_TEST) { mi = phaseMenu.add(kickPhaseAction); mi.setMnemonic(KeyEvent.VK_K); } phaseMenu.addSeparator(); mi = phaseMenu.add(takeMulliganAction); mi.setMnemonic(KeyEvent.VK_M); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, 0)); mi = phaseMenu.add(requestExtraRollAction); mi.setMnemonic(KeyEvent.VK_X); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, 0)); phaseMenu.addSeparator(); if (!client.isSpectator()) { mi = phaseMenu.add(withdrawFromGameAction); mi.setMnemonic(KeyEvent.VK_W); } // Window menu: menu for the "window-related" // (satellite windows and graphic actions effecting whole "windows"), // Plus the Preferences window as last entry. JMenu windowMenu = new JMenu("Window"); windowMenu.setMnemonic(KeyEvent.VK_W); menuBar.add(windowMenu); addCheckBox(windowMenu, Options.showCaretaker, KeyEvent.VK_C); addCheckBox(windowMenu, Options.showStatusScreen, KeyEvent.VK_G); addCheckBox(windowMenu, Options.showEngagementResults, KeyEvent.VK_E); addCheckBox(windowMenu, Options.showAutoInspector, KeyEvent.VK_I); addCheckBox(windowMenu, Options.showEventViewer, KeyEvent.VK_E); addCheckBox(windowMenu, Options.showLogWindow, KeyEvent.VK_L); addCheckBox(windowMenu, Options.showConnectionLogWindow, KeyEvent.VK_O); // full recruit tree mi = windowMenu.add(viewFullRecruitTreeAction); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, 0)); mi.setMnemonic(KeyEvent.VK_R); // web client mi = windowMenu.add(viewWebClientAction); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0)); mi.setMnemonic(KeyEvent.VK_W); windowMenu.addSeparator(); // Then the "do something to a Window" actions; // and Preferences Window as last: if (GraphicsEnvironment.getLocalGraphicsEnvironment() .getScreenDevices().length > 1) { mi = windowMenu.add(chooseScreenAction); } mi = windowMenu.add(preferencesAction); mi.setMnemonic(KeyEvent.VK_P); // Then help menu JMenu helpMenu = new JMenu("Help"); helpMenu.setMnemonic(KeyEvent.VK_H); menuBar.add(helpMenu); mi = helpMenu.add(aboutAction); mi = helpMenu.add(viewReadmeAction); mi.setMnemonic(KeyEvent.VK_V); mi = helpMenu.add(viewHelpDocAction); mi = helpMenu.add(viewWelcomeAction); } /** * Find the checkbox for the given (boolean) option name; set it to the * new given value (only if different that previous value). * * @param name The option name to adjust the checkbox for * @param enable The should-be state of the checkbox */ void adjustCheckboxIfNeeded(String name, boolean enable) { JCheckBoxMenuItem cbmi = checkboxes.get(name); if (cbmi != null) { // Only set the selected state if it has changed, // to avoid infinite feedback loops. boolean previous = cbmi.isSelected(); if (enable != previous) { cbmi.setSelected(enable); } } } /** Show which player owns this board. */ void setupPlayerLabel() { if (playerLabelDone) { return; } String playerName = gui.getOwningPlayerName(); cachedPlayerName = playerName; if (bottomBar == null) { // add a bottom bar bottomBar = new BottomBar(); contentPane.add(bottomBar, BorderLayout.SOUTH); // notify masterFrame.pack(); } bottomBar.setPlayerName(playerName); PlayerColor clientColor = client.getColor(); // If we call this before player colors are chosen, just use // the defaults. if (clientColor != null) { Color color = clientColor.getBackgroundColor(); bottomBar.setPlayerColor(color); // Don't do this again. playerLabelDone = true; } } private void setupGUIHexes() { guiHexArray = new GUIMasterHex[getMasterBoard().getHorizSize()][getMasterBoard() .getVertSize()]; int scale = Scale.get(); int cx = 3 * scale; int cy = 0 * scale; for (int i = 0; i < guiHexArray.length; i++) { for (int j = 0; j < guiHexArray[0].length; j++) { if (getMasterBoard().getShow()[i][j]) { GUIMasterHex hex = new GUIMasterHex(getMasterBoard() .getPlainHexArray()[i][j]); hex.init(cx + 4 * i * scale, (int)Math.round(cy + (3 * j + ((i + getMasterBoard().getBoardParity()) & 1) * (1 + 2 * (j / 2)) + ((i + 1 + getMasterBoard() .getBoardParity()) & 1) * 2 * ((j + 1) / 2)) * GUIHex.SQRT3 * scale), scale, getMasterBoard() .isHexInverted(i, j), this); guiHexArray[i][j] = hex; } } } } /** * TODO this should probably be stored as member, possibly instead of the client class. */ private net.sf.colossus.variant.MasterBoard getMasterBoard() { return gui.getGame().getVariant().getMasterBoard(); } private void cleanGUIHexes() { for (int i = 0; i < guiHexArray.length; i++) { for (int j = 0; j < guiHexArray[0].length; j++) { if (getMasterBoard().getShow()[i][j]) { GUIMasterHex hex = guiHexArray[i][j]; hex.cleanup(); guiHexArray[i][j] = null; } } } } /** * Finishes the current phase. * * Depending on the current phase this method dispatches to * the different done methods. * * @see Client#doneWithSplits() * @see Client#doneWithMoves() * @see Client#doneWithEngagements()() * @see Client#doneWithRecruits()() */ private void doneWithPhase() { Constants.ConfirmVals answer; if (gui.getGame().isPhase(Phase.SPLIT)) { boolean beDone = true; int cnt; if ((cnt = client.findTallLegionHexes(7, false).size()) > 0 && (client.getOwningPlayer().getMarkersAvailable().size() >= 1) && gui.getOptions().getOption(Options.confirmNoSplit, true)) { answer = confirmDialog( (cnt == 1) ? "with 7 creatures that has not split" : "with 7 creatures that have not split", cnt); if (answer == Constants.ConfirmVals.DoNotAsk) { client.setPreferencesCheckBoxValue(Options.confirmNoSplit, false); beDone = true; } else if (answer == Constants.ConfirmVals.No) { beDone = false; } else { beDone = true; } } if (beDone) { client.doneWithSplits(); } else { // dispatcher disabled Done button before calling us here doneWithPhaseAction.setEnabled(true); } } else if (gui.getGame().isPhase(Phase.MOVE)) { boolean beDone = true; int mcModeInt = gui.getLegionMoveConfirmationMode(); if ((mcModeInt == Options.legionMoveConfirmationNumMove) || (mcModeInt == Options.legionMoveConfirmationNumUnvisitedMove)) { int legionStatus[] = { 0, 0, 0, 0 }; int cnt; answer = Constants.ConfirmVals.No; client.legionsNotMoved(legionStatus, true); if (mcModeInt == Options.legionMoveConfirmationNumUnvisitedMove) { cnt = legionStatus[Constants.legionStatusNotVisitedSkippedBlocked]; if (cnt > 0) { answer = confirmDialog( (cnt == 1) ? "that has not moved or been visited" : "that have not moved or been visited", cnt); } } else { cnt = legionStatus[Constants.legionStatusCount] - legionStatus[Constants.legionStatusMoved]; if (cnt > 0) { answer = confirmDialog( (cnt == 1) ? "that has not moved" : "that have not moved", cnt); } } beDone = true; if (cnt > 0) { if (answer == Constants.ConfirmVals.DoNotAsk) { client.setPreferencesRadioButtonValue( Options.legionMoveConfirmationNoMove, false); client.setPreferencesRadioButtonValue( Options.legionMoveConfirmationNoUnvisitedMove, false); client.setPreferencesRadioButtonValue( Options.legionMoveConfirmationNoConfirm, true); beDone = true; } else if (answer == Constants.ConfirmVals.No) { beDone = false; } } } if (beDone) { client.doneWithMoves(); bottomBar.setLegionsLeftToMove(0); } else { // dispatcher disabled Done button before calling us here doneWithPhaseAction.setEnabled(true); } } else if (gui.getGame().isPhase(Phase.FIGHT)) { client.doneWithEngagements(); } else if (gui.getGame().isPhase(Phase.MUSTER)) { boolean beDone = true; int cnt; if ((cnt = client.getPossibleRecruitHexes().size()) > 0 && gui.getOptions().getOption(Options.confirmNoRecruit, true)) { answer = confirmDialog((cnt == 1) ? "that has not mustered" : "that have not mustered", cnt); if (answer == Constants.ConfirmVals.DoNotAsk) { client.setPreferencesCheckBoxValue( Options.confirmNoRecruit, false); beDone = true; } else if (answer == Constants.ConfirmVals.No) { beDone = false; } else { beDone = true; } } if (beDone) { client.doneWithRecruits(); bottomBar.setLegionsLeftToMuster(0); } else { // dispatcher disabled Done button before calling us here doneWithPhaseAction.setEnabled(true); } } else { throw new IllegalStateException("Client has unknown phase value"); } } private void setupPhasePreparations(String titleText) { if (gui.isMyTurn()) { gui.resetAllLegionFlags(); setTitleInfoText(titleText); bottomBar.setPhase(titleText); } unselectAllHexes(); } public void updateSplitPendingText(String titleText) { setTitleInfoText(titleText); bottomBar.setPhase(titleText); } /** * Do the setup needed for an inactive player: * set the actions which are allowed only for active player to inactive, * and update the bottomBar info why "Done" is disabled accordingly * */ private void setupAsInactivePlayer() { undoLastAction.setEnabled(false); undoAllAction.setEnabled(false); forcedDoneWithPhaseAction.setEnabled(false); takeMulliganAction.setEnabled(false); requestExtraRollAction.setEnabled(false); String text = client.getActivePlayer() + " " + client.getPhase().getDoesWhat(); disableDoneActionActivePlayerDoes(text); setTitleInfoText(text); } void setupSplitMenu() { setupPhasePreparations("Split stacks"); if (gui.isMyTurn()) { undoLastAction.setEnabled(true); undoAllAction.setEnabled(true); forcedDoneWithPhaseAction.setEnabled(true); takeMulliganAction.setEnabled(false); requestExtraRollAction.setEnabled(false); enableDoneAction(); // If not first turn, then add Marker Count Text to the bottom display bar if (client.getTurnNumber() != 1) { bottomBar.setMarkerCount(client.getOwningPlayer() .getMarkersAvailable().size()); } highlightTallLegions(); } else { setupAsInactivePlayer(); } } void setupMoveMenu() { setupPhasePreparations("Move legions"); if (gui.isMyTurn()) { undoLastAction.setEnabled(true); undoAllAction.setEnabled(true); forcedDoneWithPhaseAction.setEnabled(true); boolean mullLeft = (gui.getOwningPlayer().getMulligansLeft() > 0); takeMulliganAction.setEnabled(mullLeft ? true : false); requestExtraRollAction.setEnabled(true); disableDoneAction("At least one legion must move"); setMovementPhase(); highlightUnmovedLegions(); updateLegionsLeftToMoveText(false); maybeRequestFocusAndToFront(); } else { setupAsInactivePlayer(); } } public void setMovementPhase() { bottomBar.setPhase("Move legions"); } void setupFightMenu() { setupPhasePreparations("Resolve engagements"); if (gui.isMyTurn()) { undoLastAction.setEnabled(false); undoAllAction.setEnabled(false); forcedDoneWithPhaseAction.setEnabled(true); takeMulliganAction.setEnabled(false); requestExtraRollAction.setEnabled(false); // if there are no engagements, we are kicked to next phase // automatically anyway. updateEngagementsLeftText(); highlightEngagements(); maybeRequestFocusAndToFront(); } else { setupAsInactivePlayer(); } } public void updateEngagementsLeftText() { int count = client.getGame().findEngagements().size(); String ongoing = ""; MasterHex engagedHex = client.getGame().getBattleSite(); if (engagedHex != null) { ongoing = "engaged on " + engagedHex.getDescription() + "; after that "; count--; } disableDoneAction(ongoing + count + " more engagement" + (count == 1 ? "" : "s") + " to resolve"); } void setupMusterMenu() { setupPhasePreparations("Muster recruits"); if (gui.isMyTurn()) { // TODO actually it's not a good idea that the ClearRecruitChits // action is also allowed in Muster phase - the chit will be // cleared from display, but not unrecruited. Might lead to // confusion. But then, if one uses that action then it's // his own fault ;-) undoLastAction.setEnabled(true); undoAllAction.setEnabled(true); forcedDoneWithPhaseAction.setEnabled(true); takeMulliganAction.setEnabled(false); requestExtraRollAction.setEnabled(false); enableDoneAction(); updateLegionsLeftToMusterText(); highlightPossibleRecruitLegionHexes(); maybeRequestFocusAndToFront(); } else { setupAsInactivePlayer(); } } /** * Highlight all hexes with legions that (still) can do recruiting */ void highlightPossibleRecruitLegionHexes() { unselectAllHexes(); selectHexes(client.getPossibleRecruitHexes()); } KFrame getFrame() { return masterFrame; } /** This is incredibly inefficient. */ void alignAllLegions() { ArrayHelper.findFirstMatch(getMasterBoard().getPlainHexArray(), new NullCheckPredicate<MasterHex>(false) { @Override public boolean matchesNonNullValue(MasterHex hex) { alignLegions(hex); return false; } }); } void alignLegions(MasterHex masterHex) { GUIMasterHex hex = getGUIHexByMasterHex(masterHex); if (hex == null) { return; } List<Legion> legions = gui.getGameClientSide().getLegionsByHex( masterHex); int numLegions = legions.size(); if (numLegions == 0) { hex.repaint(); return; } LegionClientSide legion = (LegionClientSide)legions.get(0); Marker marker = legionToMarkerMap.get(legion); if (marker == null) { hex.repaint(); return; } int chitScale = marker.getBounds().width; Point startingPoint = hex.getOffCenter(); Point point = new Point(startingPoint); if (numLegions == 1) { // Place legion in the center of the hex. int chitScale2 = chitScale / 2; point.x -= chitScale2; point.y -= chitScale2; marker.setLocation(point); } else if (numLegions == 2) { // Place legions in NW and SE corners. int chitScale4 = chitScale / 4; point.x -= 3 * chitScale4; point.y -= 3 * chitScale4; marker.setLocation(point); point = new Point(startingPoint); point.x -= chitScale4; point.y -= chitScale4; legion = (LegionClientSide)legions.get(1); marker = legionToMarkerMap.get(legion); if (marker != null) { // Second marker can be null when loading during // the engagement phase. marker.setLocation(point); } } else if (numLegions == 3) { // Place legions in NW, SE, NE corners. int chitScale4 = chitScale / 4; point.x -= 3 * chitScale4; point.y -= 3 * chitScale4; marker.setLocation(point); point = new Point(startingPoint); point.x -= chitScale4; point.y -= chitScale4; legion = (LegionClientSide)legions.get(1); marker = legionToMarkerMap.get(legion); marker.setLocation(point); point = new Point(startingPoint); point.x -= chitScale4; point.y -= chitScale; legion = (LegionClientSide)legions.get(2); marker = legionToMarkerMap.get(legion); marker.setLocation(point); } hex.repaint(); } private void alignLegions(Set<MasterHex> hexes) { for (MasterHex masterHex : hexes) { alignLegions(masterHex); } } void highlightTallLegions() { unselectAllHexes(); selectHexes(client.findTallLegionHexes()); selectHexes(client.findPendingSplitHexes(), Color.blue); selectHexes(client.findPendingUndoSplitHexes(), Color.blue); repaint(); Thread.yield(); } void highlightUnmovedLegions() { unselectAllHexes(); selectHexes(gui.getStillToMoveHexes()); selectHexes(gui.getPendingMoveHexes(), Color.blue); gui.setMover(null); repaint(); } void setPendingText(String text) { bottomBar.setPendingText(text); } // Add Marker Count Text to the bottom display bar void setMarkerCount(int markerCount) { bottomBar.setMarkerCount(markerCount); } /** Select hexes where this legion can move. */ private void highlightMoves(LegionClientSide legion) { unselectAllHexes(); Set<MasterHex> teleport = client.listTeleportMoves(legion); selectHexes(teleport, HTMLColor.purple); Set<MasterHex> normal = client.listNormalMoves(legion); selectHexes(normal, Color.white); Set<MasterHex> combo = new HashSet<MasterHex>(); combo.addAll(teleport); combo.addAll(normal); LOGGER.info("MB: addPossibleRecruitChits(LegionClientSide legion, " + "Set<MasterHex> hexes)"); if (gui.getRecruitChitMode() == Options.showRecruitChitsNumNone) { return; } addPossibleRecruitChits(legion, combo); } void highlightEngagements() { Set<MasterHex> set = gui.getGameClientSide().findEngagements(); unselectAllHexes(); selectHexes(set); } private void setupIcon() { List<String> directories = new ArrayList<String>(); directories.add(Constants.defaultDirName + StaticResourceLoader.getPathSeparator() + Constants.imagesDirName); String[] iconNames = { Constants.masterboardIconImage, Constants.masterboardIconText + "-Name-" + Constants.masterboardIconTextColor, Constants.masterboardIconSubscript + "-Subscript-" + Constants.masterboardIconTextColor }; Image image = StaticResourceLoader.getCompositeImage(iconNames, directories, 60, 60); if (image == null) { LOGGER.log(Level.WARNING, "Couldn't find Colossus icon"); } else { masterFrame.setIconImage(image); } } /** Do a brute-force search through the hex array, looking for * a match. Return the hex, or null if none is found. */ GUIMasterHex getGUIHexByMasterHex(final MasterHex masterHex) { return ArrayHelper.findFirstMatch(guiHexArray, new NullCheckPredicate<GUIMasterHex>(false) { @Override public boolean matchesNonNullValue(GUIMasterHex hex) { return hex.getHexModel().equals(masterHex); } }); } /** Return the MasterHex that contains the given point, or * null if none does. */ private GUIMasterHex getHexContainingPoint(final Point point) { return ArrayHelper.findFirstMatch(guiHexArray, new NullCheckPredicate<GUIMasterHex>(false) { @Override public boolean matchesNonNullValue(GUIMasterHex hex) { return hex.contains(point); } }); } void setMarkerForLegion(Legion legion, Marker marker) { synchronized (legionToMarkerMap) { legionToMarkerMap.remove(legion); legionToMarkerMap.put(legion, marker); } } void removeMarkerForLegion(Legion legion) { synchronized (legionToMarkerMap) { legionToMarkerMap.remove(legion); recruitedChits.remove(legion); } } /** Create new markers in response to a rescale. */ public void recreateMarkers() { Set<MasterHex> hexesNeedAligning = new HashSet<MasterHex>(); synchronized (legionToMarkerMap) { legionToMarkerMap.clear(); for (Player player : gui.getGameClientSide().getPlayers()) { for (Legion legion : player.getLegions()) { Marker marker = new Marker(legion, 3 * Scale.get(), legion.getLongMarkerId(), client, true); legionToMarkerMap.put(legion, marker); hexesNeedAligning.add(legion.getCurrentHex()); } } } alignLegions(hexesNeedAligning); } /** Return the topmost Marker that contains the given point, or * null if none does. */ private Marker getMarkerAtPoint(Point point) { synchronized (legionToMarkerMap) { Marker marker = null; for (Entry<Legion, Marker> entry : legionToMarkerMap.entrySet()) { if (entry.getValue().getBounds().contains(point)) { marker = entry.getValue(); } } return marker; } } // TODO the next couple of methods iterate through all elements of an array // by just returning false as predicate value. It should be a different method // without return value IMO -- it didn't look as bad when it was called visitor, // but a true Visitor shouldn't have the return value. Now the error is the other // way around, but at least generified :-) void unselectAllHexes() { ArrayHelper.findFirstMatch(guiHexArray, new NullCheckPredicate<GUIMasterHex>(false) { @Override public boolean matchesNonNullValue(GUIMasterHex hex) { if (hex.isSelected()) { hex.unselect(); hex.repaint(); } return false; // keep going } }); } void selectHex(final MasterHex modelHex) { ArrayHelper.findFirstMatch(guiHexArray, new NullCheckPredicate<GUIMasterHex>(false) { @Override public boolean matchesNonNullValue(GUIMasterHex hex) { if (!hex.isSelected() && modelHex.equals(hex.getHexModel())) { hex.select(); hex.repaint(); } return false; // keep going } }); } private void selectHexes(final Set<MasterHex> hexes) { ArrayHelper.findFirstMatch(guiHexArray, new NullCheckPredicate<GUIMasterHex>(false) { @Override public boolean matchesNonNullValue(GUIMasterHex hex) { if (!hex.isSelected() && hexes.contains(hex.getHexModel())) { hex.select(); hex.repaint(); } return false; // keep going } }); } private void selectHexes(final Set<MasterHex> hexes, final Color color) { ArrayHelper.findFirstMatch(guiHexArray, new NullCheckPredicate<GUIMasterHex>(false) { @Override public boolean matchesNonNullValue(GUIMasterHex hex) { if (hexes.contains(hex.getHexModel())) { hex.select(); hex.setSelectColor(color); hex.repaint(); } return false; // keep going } }); repaint(); } /* private String getFocusOwnerText() { KeyboardFocusManager kfm = DefaultKeyboardFocusManager .getCurrentKeyboardFocusManager(); if (kfm != null) { Component fo = kfm.getFocusOwner(); if (fo != null) { return fo.toString(); } } return "none"; } */ public void focusBackToMasterboard() { /* Enforce focus back to MasterBoard. After pressing done itmight be * in the bottom-panel (in particular the Pause button, it seems) and * masterboard doesn't get KeyEvents => legion flyouts don't work */ // ("Fowcus owner WAS: " + getFocusOwnerText()); this.requestFocusInWindow(); // ("Fowcus owner NOW: " + getFocusOwnerText()); } void actOnMisclick() { focusBackToMasterboard(); Phase phase = gui.getGame().getPhase(); if (phase == Phase.SPLIT) { highlightTallLegions(); } else if (phase == Phase.MOVE) { clearPossibleRecruitChits(); highlightUnmovedLegions(); } else if (phase == Phase.FIGHT) { highlightEngagements(); } else if (phase == Phase.MUSTER) { highlightPossibleRecruitLegionHexes(); } } /** Return true if the MouseEvent e came from button 2 or 3. * In theory, isPopupTrigger() is the right way to check for * this. In practice, the poor design choice of only having * isPopupTrigger() fire on mouse release on Windows makes * it useless here. */ private static boolean isPopupButton(MouseEvent e) { int modifiers = e.getModifiers(); return (((modifiers & InputEvent.BUTTON2_MASK) != 0) || ((modifiers & InputEvent.BUTTON3_MASK) != 0) || e.isAltDown() || e .isControlDown()); } class MasterBoardMouseHandler extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { Point point = e.getPoint(); Marker marker = getMarkerAtPoint(point); GUIMasterHex hex = getHexContainingPoint(point); if (gui.isPointSideMovementDie(point) && gui.getOwningPlayer().getMulligansLeft() > 0) { client.mulligan(); return; } if (marker != null) { String markerId = marker.getId(); // Move the clicked-on marker to the top of the z-order. LegionClientSide legion = client.getLegion(markerId); gui.setMarker(legion, marker); // Right-click means to show the contents of the legion. if (isPopupButton(e)) { int viewMode = gui.getEffectiveViewMode(); boolean dubiousAsBlanks = gui.getOptions().getOption( Options.dubiousAsBlanks); boolean showMarker = gui.getOptions().getOption( Options.showMarker); if (gui.getOptions().getOption(Options.editModeActive)) { new EditLegion(gui, masterFrame, legion, point, scrollPane, 4 * Scale.get(), viewMode, client.isMyLegion(legion), dubiousAsBlanks, showMarker); } else { new ShowLegion(masterFrame, legion, point, scrollPane, 4 * Scale.get(), viewMode, client.isMyLegion(legion), dubiousAsBlanks, showMarker); } return; } else if (client.isMyLegion(legion)) { if (hex != null) { actOnLegion(legion, hex.getHexModel()); } else { LOGGER.log(Level.WARNING, "null hex in MasterBoard.mousePressed()"); } return; } } // No hits on chits, so check map. if (hex != null) { if (isPopupButton(e)) { lastPoint = point; if (gui.getGame().getLegionsByHex(hex.getHexModel()) .size() > 0) { popupMenuWithLegions.setLabel(hex.getHexModel() .getDescription()); popupMenuWithLegions.show(e.getComponent(), point.x, point.y); } else { popupMenu.setLabel(hex.getHexModel().getDescription()); popupMenu.show(e.getComponent(), point.x, point.y); } return; } // Otherwise, the action to take depends on the phase. // Only the current player can manipulate game state. if (gui.isMyTurn()) { actOnHex(hex.getHexModel()); hex.repaint(); return; } } // No hits on chits or map, so re-highlight. if (gui.isMyTurn()) { actOnMisclick(); } } } class MasterBoardMouseMotionHandler extends MouseMotionAdapter { @Override public void mouseMoved(MouseEvent e) { Point point = e.getPoint(); Marker marker = getMarkerAtPoint(point); if (marker != null) { gui.showMarker(marker); } else { GUIMasterHex hex = getHexContainingPoint(point); if (hex != null) { gui.showHexRecruitTree(hex); } } } } private void actOnLegion(LegionClientSide legion, MasterHex hex) { if (!gui.isMyTurn()) { return; } if (!gui.client.ensureThatConnected()) { return; } Phase phase = gui.getGame().getPhase(); if (phase == Phase.SPLIT) { client.doSplit(legion); } else if (phase == Phase.MOVE) { legion.setVisitedThisPhase(true); updateLegionsLeftToMoveText(true); client.setCurrentLegionMarkerId(legion.getMarkerId()); // Allow spin cycle by clicking on chit again. if (legion.equals(gui.getMover())) { actOnHex(hex); } else { gui.setMover(legion); getGUIHexByMasterHex(hex).repaint(); highlightMoves(legion); } } else if (phase == Phase.FIGHT) { attemptEngage(hex); } else if (phase == Phase.MUSTER) { client.doRecruit(legion); } } private void actOnHex(MasterHex hex) { if (!gui.client.ensureThatConnected()) { return; } Phase phase = gui.getGame().getPhase(); if (relocateOngoing != null) { gui.getClient().editRelocateLegion( relocateOngoing.getLegion().getMarkerId(), hex.getLabel()); relocateOngoing.dispose(); relocateOngoing = null; } else if (phase == Phase.SPLIT) { highlightTallLegions(); } else if (phase == Phase.MOVE) { // If we're moving, and have selected a legion which // has not yet moved, and this hex is a legal // destination, move the legion here. clearRecruitedChits(); clearPossibleRecruitChits(); gui.doMove(hex); // Would a simple highlightUnmovedLegions() be good enough? // Right now its needed also to set mover null... // Would a simple highlightUnmovedLegions() be good enough? // Right now its needed also to set mover null actOnMisclick(); // Yes, even if the move was good. } else if (phase == Phase.FIGHT) { attemptEngage(hex); } } public void actOnEditLegionMaybe(Legion legion) { if (gui.getOptions().getOption(Options.editModeActive)) { if (editLegionOngoing != null && editLegionOngoing.getLegion().equals(legion)) { viewEditLegion((LegionClientSide)legion); } } } public void setEditOngoing(EditLegion editLegion) { editLegionOngoing = editLegion; } public void setRelocateOngoing(EditLegion editLegion) { relocateOngoing = editLegion; } /** * * @param legion the legion which shall be edited */ public void viewEditLegion(LegionClientSide legion) { int viewMode = gui.getEffectiveViewMode(); boolean dubiousAsBlanks = gui.getOptions().getOption( Options.dubiousAsBlanks); boolean showMarker = gui.getOptions().getOption(Options.showMarker); Marker marker = legionToMarkerMap.get(legion); Point point = marker.getLocation(); editLegionOngoing = new EditLegion(gui, masterFrame, legion, point, scrollPane, 4 * Scale.get(), viewMode, client.isMyLegion(legion), dubiousAsBlanks, showMarker); } /** * tellEngagement calls this, now "engaging" is not pending, instead * there is a real engagement to be resolved. */ public void clearEngagingPending() { client.logMsgToServer("I", "clearEngagingPending, was: " + engagingPendingHex); engagingPendingHex = null; // so, right now defender might be asked whether he wants to flee defenderFleePhase = true; gui.defaultCursor(); } /** * We got showConcede or showNegotiate messages, i.e. the phase in which * defender might flee is over, thus the message dialog should not tell * that any more. This client here should now have some dialog or even * the actual battle map anyway. */ public void clearDefenderFlee() { defenderFleePhase = false; } private void attemptEngage(MasterHex hex) { // If we're in FIGHT phase and there are two opposing legions here, // initiate the engaging; if already engaging or engaged, inform // about that. if (gui.getGame().containsOpposingLegions(hex)) { if (gui.getGame().isEngagementOngoing()) { if (defenderFleePhase) { final String msg = "Already engaged on " + gui.getGame().getEngagement() + ";\nprobably other player is " + "asked right now whether to flee."; final String title = "Already engaged!"; Runnable messageDialogRunnable = new Runnable() { public void run() { JOptionPane.showMessageDialog(masterFrame, msg, title, JOptionPane.INFORMATION_MESSAGE); gui.getBoard().getToolkit().beep(); } }; new Thread(messageDialogRunnable).start(); } else { // player should now have the concede or negotiate dialog // or even actual battle, don't show any message any more. } } else if (engagingPendingHex != null) { // server.engage() already sent but tellEngagement() not // received yet client.logMsgToServer("W", "engagingPendingHex is " + "already set: " + engagingPendingHex); final String msg = "Engagement initiated already on hex " + engagingPendingHex + ";\n" + "still waiting for server reply."; final String title = "Already engaging!"; Runnable messageDialogRunnable = new Runnable() { public void run() { JOptionPane.showMessageDialog(masterFrame, msg, title, JOptionPane.INFORMATION_MESSAGE); gui.getBoard().getToolkit().beep(); } }; new Thread(messageDialogRunnable).start(); } else { // nothing yet, ok: engage if (gui.getClient().ensureThatConnected()) { gui.waitCursor(); engagingPendingHex = hex; client.engage(hex); } } } else { // ignore all other clicks LOGGER.warning("You clicked on hex " + hex + " - ignored."); } } class MasterBoardItemHandler implements ItemListener { public MasterBoardItemHandler() { super(); net.sf.colossus.util.InstanceTracker.register(this, cachedPlayerName); } public void itemStateChanged(ItemEvent e) { JMenuItem source = (JMenuItem)e.getSource(); String text = source.getText(); boolean selected = (e.getStateChange() == ItemEvent.SELECTED); gui.getOptions().setOption(text, selected); } } class MasterBoardWindowHandler extends WindowAdapter { @Override public void windowClosing(WindowEvent e) { gui.askNewCloseQuitCancel(masterFrame, false); } } void repaintAfterOverlayChanged() { overlayChanged = true; this.getFrame().repaint(); } @Override public void paintComponent(Graphics g) { // Abort if called too early. if (g.getClipBounds() == null) { return; } if (offScreenBuffer == null || overlayChanged || (!(offScreenBuffer.getWidth(this) == this.getSize().width && offScreenBuffer .getHeight(this) == this.getSize().height))) { overlayChanged = false; offScreenBuffer = this.createImage(this.getWidth(), this.getHeight()); Graphics g_im = offScreenBuffer.getGraphics(); super.paintComponent(g_im); try { paintHexes(g_im); } catch (ConcurrentModificationException ex) { LOGGER.log(Level.FINEST, "harmless " + ex.toString()); // Don't worry about it -- we'll just paint again. } } g.drawImage(offScreenBuffer, 0, 0, this); try { paintHighlights((Graphics2D)g); paintMarkers(g); paintRecruitedChits(g); paintPossibleRecruitChits(g); paintMovementDie(g); } catch (ConcurrentModificationException ex) { LOGGER.log(Level.FINEST, "harmless " + ex.toString()); // Don't worry about it -- we'll just paint again. } } private void paintHexes(final Graphics g) { ArrayHelper.findFirstMatch(guiHexArray, new NullCheckPredicate<GUIMasterHex>(false) { @Override public boolean matchesNonNullValue(GUIMasterHex hex) { hex.paint(g); return false; // keep going } }); } private void paintHighlights(final Graphics2D g) { ArrayHelper.findFirstMatch(guiHexArray, new NullCheckPredicate<GUIMasterHex>(false) { @Override public boolean matchesNonNullValue(GUIMasterHex hex) { hex.paintHighlightIfNeeded(g); return false; // keep going } }); } /** Paint markers in z-order. */ private void paintMarkers(Graphics g) { synchronized (legionToMarkerMap) { for (Marker marker : legionToMarkerMap.values()) { if (g.getClipBounds().intersects(marker.getBounds())) { marker.paintComponent(g); } } } } public void paintRecruitedChits(Graphics g) { for (Chit chit : recruitedChits.values()) { chit.paintComponent(g); } } // all hexes public void addPossibleRecruitChits(LegionClientSide legion, Set<MasterHex> hexes) { clearPossibleRecruitChits(); // set is a set of possible target hexes List<CreatureType> oneElemList = new ArrayList<CreatureType>(); for (MasterHex hex : hexes) { List<CreatureType> recruits = client.findEligibleRecruits(legion, hex); if (recruits != null && recruits.size() > 0) { switch (gui.getRecruitChitMode()) { case Options.showRecruitChitsNumAll: break; case Options.showRecruitChitsNumRecruitHint: oneElemList.clear(); CreatureType hint = client.chooseBestPotentialRecruit( legion, hex, recruits); oneElemList.add(hint); recruits = oneElemList; break; case Options.showRecruitChitsNumStrongest: oneElemList.clear(); CreatureType strongest = recruits .get(recruits.size() - 1); oneElemList.add(strongest); recruits = oneElemList; break; } addPossibleRecruitChits(recruits, hex); } } } void addRecruitedChit(Legion legion) { if (legion.getRecruit() != null) { MasterHex masterHex = legion.getCurrentHex(); int scale = 2 * Scale.get(); GUIMasterHex hex = getGUIHexByMasterHex(masterHex); Chit chit = Chit.newCreatureChit(scale, legion.getRecruit()); recruitedChits.put(legion, chit); Point startingPoint = hex.getOffCenter(); Point point = new Point(startingPoint); point.x -= scale / 2; point.y -= scale / 2; chit.setLocation(point); } repaint(); } void cleanRecruitedChit(LegionClientSide legion) { recruitedChits.remove(legion); repaint(); } // all possible recruit chits, one hex private void addPossibleRecruitChits(List<CreatureType> imageNameList, MasterHex masterHex) { List<Chit> list = new ArrayList<Chit>(); int size = imageNameList.size(); int num = size; for (CreatureType creatureType : imageNameList) { int scale = 2 * Scale.get(); GUIMasterHex hex = getGUIHexByMasterHex(masterHex); Chit chit = Chit.newCreatureChit(scale, creatureType); Point startingPoint = hex.getOffCenter(); Point point = new Point(startingPoint); point.x -= scale / 2; point.y -= scale / 2; int offset = (num - ((size / 2) + 1)); point.x += ((offset * scale) + ((size % 2 == 0 ? (scale / 2) : 0))) / size; point.y += ((offset * scale) + ((size % 2 == 0 ? (scale / 2) : 0))) / size; num--; chit.setLocation(point); list.add(chit); } possibleRecruitChits.put(masterHex, list); } public void clearRecruitedChits() { for (Chit chit : recruitedChits.values()) { remove(chit); } recruitedChits.clear(); } public void clearPossibleRecruitChits() { Set<MasterHex> hexes = possibleRecruitChits.keySet(); possibleRecruitChits.clear(); for (MasterHex hex : hexes) { GUIMasterHex guiHex = getGUIHexByMasterHex(hex); guiHex.repaint(); } } private void paintPossibleRecruitChits(Graphics g) { // Each returned list is the list of chits for one hex for (List<Chit> chits : possibleRecruitChits.values()) { for (Chit chit : chits) { chit.paintComponent(g); } } } private void paintMovementDie(Graphics g) { if (client != null) { MovementDie die = gui.getMovementDie(); if (die != null) { die.setLocation(0, 0); die.paintComponent(g); } else { // Paint a black square in the upper-left corner. g.setColor(Color.black); g.fillRect(0, 0, 4 * Scale.get(), 4 * Scale.get()); } } } public void markLegionSkip() { Legion activeLegion = null; if (gui.getGame().isPhase(Phase.SPLIT)) { // Not implemented yet // ("Mark Legion Skip Split"); } else if (gui.getGame().isPhase(Phase.MOVE)) { if ((activeLegion = gui.getMover()) != null && !activeLegion.getSkipThisTime() && !activeLegion.hasMoved()) { activeLegion.setSkipThisTime(true); gui.pushUndoStack(activeLegion.getMarkerId()); updateLegionsLeftToMoveText(true); clearPossibleRecruitChits(); highlightUnmovedLegions(); repaint(); } else { // no active legion, or active legion already marked } } else if (gui.getGame().isPhase(Phase.MUSTER)) { // implemented as part of the PickRecruit dialog // System.out.println("Mark Legion Skip Move Muster"); } else { LOGGER.warning("Mark Legion Skip not meaningful in phase " + gui.getGame().getPhase()); } } /** * user pressed "N". Find the next legion that "deserves" handling, activate * (as if user had clicked it), and position the mouse cursor over it. */ private void jumpToNextUnhandledLegion() { Phase phase = client.getPhase(); if (!(phase == Phase.MOVE || phase == Phase.SPLIT || phase == Phase.MUSTER) || !gui.isMyTurn()) { // Not my Split, Move or Recruit phase - nothing to do. return; } boolean allSplitableLegions = false; Player player = gui.getClient().getActivePlayer(); if (phase == Phase.SPLIT) { Set<String> markersAvailable = player.getMarkersAvailable(); // Need a legion marker to split. if (markersAvailable.size() < 1) { return; } // On the first turn, can only split once, the 8 count legion // Need flag set to false, or, after splitting, next will then // cycle through the two size 4 legions. allSplitableLegions = client.getTurnNumber() == 1 ? false : gui .getOptions().getOption(Options.nextSplitAllSplitable, true); } boolean first = true; boolean found = false; String markerId = null; String curMarkerId = client.getCurrentLegionMarkerId(); Legion nextLegion = null; if (curMarkerId != null) { nextLegion = client.getLegion(curMarkerId); } for (Legion legion : player.getLegions()) { if (first) { // Handle case where current legion is not in set // or current legion is last entry in set if ((phase == Phase.MOVE && !legion.hasMoved()) || (phase == Phase.SPLIT && !allSplitableLegions && (legion .getHeight() >= 7)) || (phase == Phase.SPLIT && allSplitableLegions && (legion .getHeight() >= 4)) || (phase == Phase.MUSTER && client.canRecruit(legion))) { nextLegion = legion; first = false; } } if (found && !legion.getSkipThisTime()) { if ((phase == Phase.MOVE && !legion.hasMoved()) || (phase == Phase.SPLIT && !allSplitableLegions && (legion .getHeight() == 7)) || (phase == Phase.SPLIT && allSplitableLegions && (legion .getHeight() >= 4)) || (phase == Phase.MUSTER && client.canRecruit(legion))) { nextLegion = legion; break; } } markerId = legion.getMarkerId(); if (markerId.equals(curMarkerId)) { found = true; } } // Eclipse thinks nextLegion might be null ... // to be on safe side, do nothing in that case. if (!first && nextLegion != null) { activateNextLegionAndPlaceMouse(nextLegion); } } /** * @param nextLegion */ private void activateNextLegionAndPlaceMouse(Legion nextLegion) { assert nextLegion != null : "nextLegion must not be null when " + "calling this!"; LegionClientSide newCurLegion = client.getLegion(nextLegion .getMarkerId()); MasterHex newHex = newCurLegion.getCurrentHex(); GUIMasterHex newGHex = getGUIHexByMasterHex(newHex); Point point = newGHex.findCenter(); try { Robot robot = new Robot(); Rectangle rect = new Rectangle(point.x - 250, point.y - 250, 500, 500); scrollRectToVisible(rect); SwingUtilities.convertPointToScreen(point, MasterBoard.this); robot.mouseMove(point.x, point.y); client.setCurrentLegionMarkerId(nextLegion.getMarkerId()); Phase phase = client.getPhase(); if ((phase == Phase.MUSTER && gui.getOptions().getOption( Options.nextMuster, true)) || (phase == Phase.MOVE && gui.getOptions().getOption( Options.nextMove, true)) || (phase == Phase.SPLIT && (gui.getNextSplitClickMode() == Options.nextSplitNumLeftClick))) { actOnLegion(newCurLegion, nextLegion.getCurrentHex()); GUIMasterHex hex = getGUIHexByMasterHex(nextLegion .getCurrentHex()); hex.select(); hex.setSelectColor(Color.black); hex.repaint(); } else if (phase == Phase.SPLIT && (gui.getNextSplitClickMode() == Options.nextSplitNumRightClick)) { int viewMode = gui.getEffectiveViewMode(); LegionClientSide clientSideLegion = client .getLegion(nextLegion.getMarkerId()); new ShowLegion(masterFrame, clientSideLegion, point, scrollPane, 4 * Scale.get(), viewMode, client.isMyLegion(clientSideLegion), false, true); } } catch (AWTException exception) { LOGGER.log(Level.WARNING, "Robot creation failed"); } } private void destroyThisLegion() { if (gui.getGame().isPhase(Phase.MOVE)) { Legion activeLegion = gui.getMover(); if (activeLegion != null) { if (activeLegion.hasTitan()) { LOGGER.warning("Ignored attempt to destroy Titan Legion!"); } else { gui.getClient().destroyLegion(activeLegion); } } } else { LOGGER.warning("destroyThisLegion called, but not move phase."); } } @Override public Dimension getMinimumSize() { int scale = Scale.get(); return new Dimension(((getMasterBoard().getHorizSize() + 1) * 4) * scale, (getMasterBoard().getVertSize() * 7) * scale); } @Override public Dimension getPreferredSize() { return getMinimumSize(); } void rescale() { setupGUIHexes(); recreateMarkers(); setSize(getPreferredSize()); masterFrame.pack(); repaint(); } void deiconify() { if (masterFrame.getState() == JFrame.ICONIFIED) { masterFrame.setState(JFrame.NORMAL); } } public void dispose() { setVisible(false); setEnabled(false); saveWindow.saveLocation(masterFrame.getLocation()); saveWindow = null; cleanCBListeners(); masterFrame.setVisible(false); masterFrame.setEnabled(false); masterFrame.removeWindowListener(mbwh); masterFrame.dispose(); masterFrame = null; scrollPane = null; removeKeyListener(this.iph); if (showReadme != null) { showReadme.dispose(); showReadme = null; } if (showHelpDoc != null) { showHelpDoc.dispose(); showHelpDoc = null; } offScreenBuffer = null; iph = null; cleanGUIHexes(); // not those, they are static (common for all objects) // first client disposing would set it null, others get NPE's ... //plainHexArray = null; //towerSet = null; this.client = null; } void pack() { masterFrame.pack(); } void updateComponentTreeUI() { SwingUtilities.updateComponentTreeUI(this); SwingUtilities.updateComponentTreeUI(masterFrame); } void fullRepaint() { masterFrame.repaint(); repaint(); } /** * If and only if stealFocus option is enabled, this does both * requestFocus and getFrame().toFront(). */ void maybeRequestFocusAndToFront() { if (gui.getOptions().getOption(Options.stealFocus)) { requestFocus(); getFrame().toFront(); } } void myTurnBottomBarActions(boolean isDue) { bottomBar.myTurnActions(isDue); } public void updateLegionsLeftToMusterText() { int legionCount = client.getPossibleRecruitHexes().size(); bottomBar.setLegionsLeftToMuster(legionCount); } // When enter Move phase, do not yet have roll, therefore can not // determine which units are blocked. Attempting to do so yields // bad results, and a null pointer exception on the first turn, so // send flag to prevent this. public void updateLegionsLeftToMoveText(boolean have_roll) { int legionStatus[] = { 0, 0, 0, 0 }; client.legionsNotMoved(legionStatus, have_roll); bottomBar.setLegionsStatus(legionStatus); } class BottomBar extends JPanel { private final JLabel playerLabel; private final Color originalBackgroundColor; /** quick access button to the doneWithPhase action. * must be en- and disabled often. */ private final JButton doneButton; private final JButton pauseButton; /** display the current phase in the bottom bar */ private final JLabel phaseLabel; /** * Displays reasons why "Done" can not be used. */ private final JLabel todoLabel; /* * Displays count of actions still to do. e.g. legions to muster */ private final JLabel countLabel; public void setPlayerName(String s) { playerLabel.setText(s); } public void setPlayerColor(Color color) { playerLabel.setForeground(color); } public void myTurnActions(boolean isDue) { this.setBackground(isDue ? Color.yellow : originalBackgroundColor); } public void setPhase(String s) { phaseLabel.setText(s); } public void setPendingText(String text) { phaseLabel.setText(text); } public void setReasonForDisabledDone(String reason) { todoLabel.setText("(" + reason + ")"); } public void setLegionsLeftToMuster(int legionCount) { if (legionCount == 0) countLabel.setText(""); else if (legionCount == 1) countLabel.setText(legionCount + " legion to muster"); else countLabel.setText(legionCount + " legions to muster"); } public void setLegionsStatus(int legionStatus[]) { int unmoved = legionStatus[Constants.legionStatusCount] - legionStatus[Constants.legionStatusMoved]; if (unmoved > 0) { String labelText = legionStatus[Constants.legionStatusCount] + " legions: " + unmoved + (unmoved == 1 ? " has not moved" : " have not moved"); if (legionStatus[Constants.legionStatusBlocked] > 0) { if (unmoved == 1) { labelText = labelText + ", it is blocked"; } else { labelText = labelText + ", " + legionStatus[Constants.legionStatusBlocked] + (legionStatus[Constants.legionStatusBlocked] == 1 ? " of those is blocked" : " of those are blocked"); } } if (legionStatus[Constants.legionStatusNotVisitedSkippedBlocked] > 0) { if (unmoved == 1) { labelText = labelText + ", it has not been visited or skipped"; } else { labelText = labelText + ", " + legionStatus[Constants.legionStatusNotVisitedSkippedBlocked] + (legionStatus[Constants.legionStatusNotVisitedSkippedBlocked] == 1 ? " of those has not been visited or skipped" : " of those have not been visited or skipped"); } } labelText = labelText + "."; countLabel.setText(labelText); } else { countLabel.setText(legionStatus[Constants.legionStatusCount] + " legions, all have moved"); } } public void setLegionsLeftToMove(int legionCount) { if (legionCount == 0) countLabel.setText(""); else if (legionCount == 1) countLabel.setText(legionCount + " legion to move"); else countLabel.setText(legionCount + " legions to move"); } // Add Marker Count Text to the bottom display bar public void setMarkerCount(int markerCount) { countLabel.setText("(" + markerCount + " marker" + ((markerCount == 1) ? "" : "s") + ")"); } public BottomBar() { super(); setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT)); String text = gui.getClient().isPaused() ? "Continue" : "Pause"; pauseButton = new JButton(text); pauseButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { MasterBoard.this.bottomBar.toggleSuspend(); focusBackToMasterboard(); } }); // Not useful in any remote client if (!client.isRemote()) { add(pauseButton); } originalBackgroundColor = this.getBackground(); playerLabel = new JLabel("- player -"); add(playerLabel); doneButton = new JButton(doneWithPhaseAction); doneWithPhaseAction .addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals("enabled") && evt.getNewValue().equals(Boolean.TRUE)) { todoLabel.setText(""); } } }); add(doneButton); phaseLabel = new JLabel("- phase -"); add(phaseLabel); todoLabel = new JLabel(); add(todoLabel); countLabel = new JLabel(); add(countLabel); } public void toggleSuspend() { boolean oldState = gui.getClient().isPaused(); boolean newState = !oldState; gui.getClient().setPauseState(newState); String text = gui.getClient().isPaused() ? "Continue" : "Pause"; pauseButton.setText(text); } /** * Called when game is over; enable done button again and make it call * "close", which brings then the "New/Close/Quit/Cancel" dialog, * so that one does not have to pick it from menu or right upper corner. */ private void makeDoneCloseWindow() { gameOverStateReached = true; enableDoneAction(); doneButton.setText("Close"); } } public void enableDoneAction() { doneWithPhaseAction.setEnabled(true); } /** * Disable the Done action, and update the reason text in bottomBar * * @param reason Information why one is not ready to be Done */ public void disableDoneAction(String reason) { doneWithPhaseAction.setEnabled(false); bottomBar.setReasonForDisabledDone(reason); } /** * Clear bottomBar phase text and call disableDoneAction, as reason the * standard text "<active player> doesWhat" * * @param doesWhat Information what the active player currently does */ private void disableDoneActionActivePlayerDoes(String doesWhat) { bottomBar.setPhase(""); // String name = gui.getClient().getActivePlayer().getName(); disableDoneAction(doesWhat); } public void setServerClosedMessage(boolean gameOver) { if (gameOver) { bottomBar.setPhase("Game over"); disableDoneAction("game server closed connection"); } else { bottomBar.setPhase("Unable to continue game"); disableDoneAction("connection to server lost"); } } public void setReconnectedMessage() { bottomBar.setPhase("-suspended-"); disableDoneAction("Connection restored - resyc needed...."); } public void setReplayMode() { disableDoneAction("please wait..."); } public void updateReplayText(int currTurn, int maxTurn) { bottomBar.setPhase("Replay ongoing, processing turn " + currTurn + " of " + maxTurn); } /** * Updates the window's title. For active player, tell what to do; * for inactive player, inform what active player is doing. * OwningPlayer: Split stacks * OwningPlayer: OtherPlayer musters * * @param text a string like 'Split stacks' or "player xxx moves" */ private void setTitleInfoText(String text) { masterFrame.setTitle(client.getOwningPlayer().getName() + ": Turn " + client.getTurnNumber() + " - " + text); } public void setTempDisconnectedState(String message) { setTitleInfoText("Problem -- " + message); bottomBar.setPhase("Problem: " + message); disableDoneAction("connection with server interrupted"); } public void setGameOverState(String message) { setTitleInfoText(message); bottomBar.setPhase(message); disableDoneAction("connection closed from server side"); bottomBar.makeDoneCloseWindow(); } public void setPhaseInfo(String message) { bottomBar.setPhase(message); } }