package games.strategy.triplea.ui; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Graphics; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Image; import java.awt.Insets; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Timer; import java.util.TimerTask; import java.util.Vector; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import java.util.prefs.Preferences; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.border.EtchedBorder; import javax.swing.border.LineBorder; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellRenderer; import games.strategy.debug.ClientLogger; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Territory; import games.strategy.engine.data.TerritoryEffect; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; import games.strategy.engine.framework.system.SystemProperties; import games.strategy.net.GUID; import games.strategy.triplea.Constants; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.BattleCalculator; import games.strategy.triplea.delegate.DiceRoll; import games.strategy.triplea.delegate.Die; import games.strategy.triplea.delegate.IBattle.BattleType; import games.strategy.triplea.delegate.Matches; import games.strategy.triplea.delegate.TerritoryEffectHelper; import games.strategy.triplea.delegate.dataObjects.CasualtyDetails; import games.strategy.triplea.delegate.dataObjects.CasualtyList; import games.strategy.triplea.image.UnitImageFactory; import games.strategy.triplea.util.UnitCategory; import games.strategy.triplea.util.UnitOwner; import games.strategy.triplea.util.UnitSeperator; import games.strategy.ui.SwingAction; import games.strategy.ui.Util; import games.strategy.util.Match; import games.strategy.util.Tuple; /** * Displays a running battle. */ public class BattleDisplay extends JPanel { private static final long serialVersionUID = -7939993104972562765L; private static final String DICE_KEY = "D"; private static final String CASUALTIES_KEY = "C"; private static final String MESSAGE_KEY = "M"; // private static Map<Unit, Territory> m_ScrambledUnits = new HashMap<Unit, Territory>(); private final GUID m_battleID; private final PlayerID m_defender; private final PlayerID m_attacker; private final Territory m_location; private final GameData m_data; private final JButton m_actionButton = new JButton(""); private final BattleModel m_defenderModel; private final BattleModel m_attackerModel; private BattleStepsPanel m_steps; private DicePanel m_dicePanel; private final CasualtyNotificationPanel m_casualties; private JPanel m_actionPanel; private final CardLayout m_actionLayout = new CardLayout(); private final JPanel m_messagePanel = new JPanel(); private final MapPanel m_mapPanel; private final JPanel m_casualtiesInstantPanelDefender = new JPanel(); private final JPanel m_casualtiesInstantPanelAttacker = new JPanel(); private final JLabel LABEL_NONE_ATTACKER = new JLabel("None"); private final JLabel LABEL_NONE_DEFENDER = new JLabel("None"); // private MovePerformer m_tempMovePerformer; private final IUIContext m_uiContext; private final JLabel m_messageLabel = new JLabel(); private final Action m_nullAction = SwingAction.of(" ", e -> { }); public BattleDisplay(final GameData data, final Territory territory, final PlayerID attacker, final PlayerID defender, final Collection<Unit> attackingUnits, final Collection<Unit> defendingUnits, final Collection<Unit> killedUnits, final Collection<Unit> attackingWaitingToDie, final Collection<Unit> defendingWaitingToDie, final GUID battleID, final MapPanel mapPanel, final boolean isAmphibious, final BattleType battleType, final Collection<Unit> amphibiousLandAttackers) { m_battleID = battleID; m_defender = defender; m_attacker = attacker; m_location = territory; m_mapPanel = mapPanel; m_data = data; final Collection<TerritoryEffect> territoryEffects = TerritoryEffectHelper.getEffects(territory); m_defenderModel = new BattleModel(defendingUnits, false, battleType, defender, m_data, m_location, territoryEffects, isAmphibious, Collections.emptySet(), m_mapPanel.getUIContext()); m_attackerModel = new BattleModel(attackingUnits, true, battleType, attacker, m_data, m_location, territoryEffects, isAmphibious, amphibiousLandAttackers, m_mapPanel.getUIContext()); m_defenderModel.setEnemyBattleModel(m_attackerModel); m_attackerModel.setEnemyBattleModel(m_defenderModel); m_defenderModel.refresh(); m_attackerModel.refresh(); m_uiContext = mapPanel.getUIContext(); m_casualties = new CasualtyNotificationPanel(data, m_mapPanel.getUIContext()); if (killedUnits != null && attackingWaitingToDie != null && defendingWaitingToDie != null) { final Collection<Unit> attackerUnitsKilled = Match.getMatches(killedUnits, Matches.unitIsOwnedBy(attacker)); attackerUnitsKilled.addAll(attackingWaitingToDie); if (!attackerUnitsKilled.isEmpty()) { updateKilledUnits(attackerUnitsKilled, attacker); } final Collection<Unit> defenderUnitsKilled = Match.getMatches(killedUnits, Matches.unitIsOwnedBy(defender)); defenderUnitsKilled.addAll(defendingWaitingToDie); if (!defenderUnitsKilled.isEmpty()) { updateKilledUnits(defenderUnitsKilled, defender); } } initLayout(); } public void cleanUp() { m_actionButton.setAction(m_nullAction); m_steps.deactivate(); m_mapPanel.getUIContext().removeActive(m_steps); m_steps = null; } void takeFocus() { // we want a component on this frame to take focus // so that pressing space will work (since it requires in focused // window). Only seems to be an issue on windows m_actionButton.requestFocus(); } public Territory getBattleLocation() { return m_location; } public GUID getBattleID() { return m_battleID; } public void bombingResults(final List<Die> dice, final int cost) { m_dicePanel.setDiceRollForBombing(dice, cost); m_actionLayout.show(m_actionPanel, DICE_KEY); } public static boolean getShowEnemyCasualtyNotification() { final Preferences prefs = Preferences.userNodeForPackage(BattleDisplay.class); return prefs.getBoolean(Constants.SHOW_ENEMY_CASUALTIES_USER_PREF, true); } public static void setShowEnemyCasualtyNotification(final boolean aVal) { final Preferences prefs = Preferences.userNodeForPackage(BattleDisplay.class); prefs.putBoolean(Constants.SHOW_ENEMY_CASUALTIES_USER_PREF, aVal); } public static boolean getFocusOnOwnCasualtiesNotification() { final Preferences prefs = Preferences.userNodeForPackage(BattleDisplay.class); return prefs.getBoolean(Constants.FOCUS_ON_OWN_CASUALTIES_USER_PREF, false); } public static void setFocusOnOwnCasualtiesNotification(final boolean aVal) { final Preferences prefs = Preferences.userNodeForPackage(BattleDisplay.class); prefs.putBoolean(Constants.FOCUS_ON_OWN_CASUALTIES_USER_PREF, aVal); } public static void setConfirmDefensiveRolls(final boolean aVal) { final Preferences prefs = Preferences.userNodeForPackage(BattleDisplay.class); prefs.putBoolean(Constants.CONFIRM_DEFENSIVE_ROLLS, aVal); } public static boolean getConfirmDefensiveRolls() { final Preferences prefs = Preferences.userNodeForPackage(BattleDisplay.class); return prefs.getBoolean(Constants.CONFIRM_DEFENSIVE_ROLLS, false); } /** * updates the panel content according to killed units for the player. * * @param aKilledUnits * list of units killed * @param aPlayerID * player kills belongs to */ private Collection<Unit> updateKilledUnits(final Collection<Unit> aKilledUnits, final PlayerID aPlayerID) { final JPanel lCausalityPanel; if (aPlayerID.equals(m_defender)) { lCausalityPanel = m_casualtiesInstantPanelDefender; } else { lCausalityPanel = m_casualtiesInstantPanelAttacker; } Map<Unit, Collection<Unit>> dependentsMap; m_data.acquireReadLock(); try { dependentsMap = BattleCalculator.getDependents(aKilledUnits); } finally { m_data.releaseReadLock(); } final Collection<Unit> dependentUnitsReturned = new ArrayList<>(); final Iterator<Collection<Unit>> dependentUnitsCollections = dependentsMap.values().iterator(); while (dependentUnitsCollections.hasNext()) { final Collection<Unit> dependentCollection = dependentUnitsCollections.next(); dependentUnitsReturned.addAll(dependentCollection); } for (final UnitCategory category : UnitSeperator.categorize(aKilledUnits, dependentsMap, false, false)) { final JPanel panel = new JPanel(); JLabel unit = m_uiContext.createUnitImageJLabel(category.getType(), category.getOwner(), m_data); panel.add(unit); panel.add(new JLabel("x " + category.getUnits().size())); for (final UnitOwner owner : category.getDependents()) { unit = m_uiContext.createUnitImageJLabel(owner.getType(), owner.getOwner(), m_data); panel.add(unit); // TODO this size is of the transport collection size, not the transportED collection size. panel.add(new JLabel("x " + category.getUnits().size())); } lCausalityPanel.add(panel); } return dependentUnitsReturned; } public void casualtyNotification(final String step, final DiceRoll dice, final PlayerID player, final Collection<Unit> killed, final Collection<Unit> damaged, final Map<Unit, Collection<Unit>> dependents) { setStep(step); m_casualties.setNotification(dice, killed, damaged, dependents); m_actionLayout.show(m_actionPanel, CASUALTIES_KEY); killed.addAll(updateKilledUnits(killed, player)); if (player.equals(m_defender)) { m_defenderModel.removeCasualties(killed); } else { m_attackerModel.removeCasualties(killed); } } public void deadUnitNotification(final PlayerID player, final Collection<Unit> killed, final Map<Unit, Collection<Unit>> dependents) { m_casualties.setNotificationShort(killed, dependents); m_actionLayout.show(m_actionPanel, CASUALTIES_KEY); killed.addAll(updateKilledUnits(killed, player)); if (player.equals(m_defender)) { m_defenderModel.removeCasualties(killed); } else { m_attackerModel.removeCasualties(killed); } } public void changedUnitsNotification(final PlayerID player, final Collection<Unit> removedUnits, final Collection<Unit> addedUnits, final Map<Unit, Collection<Unit>> dependents) { if (player.equals(m_defender)) { if (removedUnits != null) { m_defenderModel.removeCasualties(removedUnits); } if (addedUnits != null) { m_defenderModel.addUnits(addedUnits); } } else { if (removedUnits != null) { m_attackerModel.removeCasualties(removedUnits); } if (addedUnits != null) { m_attackerModel.addUnits(addedUnits); } } } protected void waitForConfirmation(final String message) { if (SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("This cannot be in dispatch thread"); } final CountDownLatch continueLatch = new CountDownLatch(1); final AbstractAction buttonAction = SwingAction.of(message, e -> continueLatch.countDown()); SwingUtilities.invokeLater(() -> m_actionButton.setAction(buttonAction)); m_mapPanel.getUIContext().addShutdownLatch(continueLatch); // Set a auto-wait expiration if the option is set. if (!getConfirmDefensiveRolls()) { final int maxWaitTime = 1500; final Timer t = new Timer(); t.schedule(new TimerTask() { @Override public void run() { continueLatch.countDown(); if (continueLatch.getCount() > 0) { SwingUtilities.invokeLater(() -> m_actionButton.setAction(m_nullAction)); } } }, maxWaitTime); } try { // wait for the button to be pressed. continueLatch.await(); } catch (final InterruptedException ie) { Thread.currentThread().interrupt(); } finally { m_mapPanel.getUIContext().removeShutdownLatch(continueLatch); } SwingUtilities.invokeLater(() -> m_actionButton.setAction(m_nullAction)); } public void endBattle(final String message, final Window enclosingFrame) { m_steps.walkToLastStep(); final Action close = SwingAction.of(message + " : (Press Space to Close)", e -> enclosingFrame.setVisible(false)); SwingUtilities.invokeLater(() -> m_actionButton.setAction(close)); } public void notifyRetreat(final Collection<Unit> retreating) { m_defenderModel.notifyRetreat(retreating); m_attackerModel.notifyRetreat(retreating); } public Territory getRetreat(final String message, final Collection<Territory> possible, final boolean submerge) { if (!submerge || possible.size() > 1) { return getRetreatInternal(message, possible); } else { return getSubmerge(message); } } private Territory getSubmerge(final String message) { if (SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("Should not be called from dispatch thread"); } final Territory[] retreatTo = new Territory[1]; final CountDownLatch latch = new CountDownLatch(1); final Action action = SwingAction.of("Submerge Subs?", e -> { final String ok = "Submerge"; final String cancel = "Remain"; final String wait = "Ask Me Later"; final String[] options = {ok, cancel, wait}; final int choice = JOptionPane.showOptionDialog(BattleDisplay.this, message, "Submerge Subs?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, cancel); // dialog dismissed if (choice == -1) { return; } // wait if (choice == 2) { return; } // remain if (choice == 1) { latch.countDown(); return; } // submerge retreatTo[0] = m_location; latch.countDown(); }); SwingUtilities.invokeLater(() -> m_actionButton.setAction(action)); SwingUtilities.invokeLater(() -> action.actionPerformed(null)); m_mapPanel.getUIContext().addShutdownLatch(latch); try { latch.await(); } catch (final InterruptedException e1) { Thread.currentThread().interrupt(); } finally { m_mapPanel.getUIContext().removeShutdownLatch(latch); } SwingUtilities.invokeLater(() -> m_actionButton.setAction(m_nullAction)); return retreatTo[0]; } private Territory getRetreatInternal(final String message, final Collection<Territory> possible) { if (SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("Should not be called from dispatch thread"); } final Territory[] retreatTo = new Territory[1]; final CountDownLatch latch = new CountDownLatch(1); final Action action = SwingAction.of("Retreat?", e -> { final String yes = possible.size() == 1 ? "Retreat to " + possible.iterator().next().getName() : "Retreat"; final String no = "Remain"; final String cancel = "Ask Me Later"; final String[] options = {yes, no, cancel}; final int choice = JOptionPane.showOptionDialog(BattleDisplay.this, message, "Retreat?", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, no); // dialog dismissed if (choice == -1) { return; } // wait if (choice == JOptionPane.CANCEL_OPTION) { return; } // remain if (choice == JOptionPane.NO_OPTION) { latch.countDown(); return; } // if you have eliminated the impossible, whatever remains, no matter // how improbable, must be the truth // retreat if (possible.size() == 1) { retreatTo[0] = possible.iterator().next(); latch.countDown(); } else { final RetreatComponent comp = new RetreatComponent(possible); final int option = JOptionPane.showConfirmDialog(BattleDisplay.this, comp, message, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null); if (option == JOptionPane.OK_OPTION) { if (comp.getSelection() != null) { retreatTo[0] = comp.getSelection(); latch.countDown(); } } } }); SwingUtilities.invokeLater(() -> m_actionButton.setAction(action)); SwingUtilities.invokeLater(() -> action.actionPerformed(null)); m_mapPanel.getUIContext().addShutdownLatch(latch); try { latch.await(); } catch (final InterruptedException e1) { e1.printStackTrace(); } finally { m_mapPanel.getUIContext().removeShutdownLatch(latch); } SwingUtilities.invokeLater(() -> m_actionButton.setAction(m_nullAction)); return retreatTo[0]; } private class RetreatComponent extends JPanel { private static final long serialVersionUID = 3855054934860687832L; private final JList<Territory> m_list; private final JLabel m_retreatTerritory = new JLabel(""); RetreatComponent(final Collection<Territory> possible) { this.setLayout(new BorderLayout()); final JLabel label = new JLabel("Retreat to..."); label.setBorder(new EmptyBorder(0, 0, 10, 0)); this.add(label, BorderLayout.NORTH); final JPanel imagePanel = new JPanel(); imagePanel.setLayout(new FlowLayout(FlowLayout.CENTER)); imagePanel.add(m_retreatTerritory); imagePanel.setBorder(new EmptyBorder(10, 10, 10, 0)); this.add(imagePanel, BorderLayout.EAST); final Vector<Territory> listElements = new Vector<>(possible); m_list = new JList<>(listElements); m_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); if (listElements.size() >= 1) { m_list.setSelectedIndex(0); } final JScrollPane scroll = new JScrollPane(m_list); this.add(scroll, BorderLayout.CENTER); scroll.setBorder(new EmptyBorder(10, 0, 10, 0)); updateImage(); m_list.addListSelectionListener(e -> updateImage()); } private void updateImage() { final int width = 250; final int height = 250; final Image img = m_mapPanel.getTerritoryImage(m_list.getSelectedValue(), m_location); final Image finalImage = Util.createImage(width, height, true); final Graphics g = finalImage.getGraphics(); g.drawImage(img, 0, 0, width, height, this); g.dispose(); m_retreatTerritory.setIcon(new ImageIcon(finalImage)); } public Territory getSelection() { return m_list.getSelectedValue(); } } public CasualtyDetails getCasualties(final Collection<Unit> selectFrom, final Map<Unit, Collection<Unit>> dependents, final int count, final String message, final DiceRoll dice, final PlayerID hit, final CasualtyList defaultCasualties, final boolean allowMultipleHitsPerUnit) { if (SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("This method should not be run in the event dispatch thread"); } final AtomicReference<CasualtyDetails> casualtyDetails = new AtomicReference<>(); final CountDownLatch continueLatch = new CountDownLatch(1); SwingUtilities.invokeLater(() -> { final boolean isEditMode = (dice == null); if (!isEditMode) { m_actionLayout.show(m_actionPanel, DICE_KEY); m_dicePanel.setDiceRoll(dice); } final boolean plural = isEditMode || (count > 1); final String countStr = isEditMode ? "" : "" + count; final String btnText = hit.getName() + ", press space to select " + countStr + (plural ? " casualties" : " casualty"); m_actionButton.setAction(new AbstractAction(btnText) { private static final long serialVersionUID = -2156028313292233568L; private UnitChooser chooser; private JScrollPane chooserScrollPane; @Override public void actionPerformed(final ActionEvent e) { final String messageText = message + " " + btnText + "."; if (chooser == null || chooserScrollPane == null) { chooser = new UnitChooser(selectFrom, defaultCasualties, dependents, m_data, allowMultipleHitsPerUnit, m_mapPanel.getUIContext()); chooser.setTitle(messageText); if (isEditMode) { chooser.setMax(selectFrom.size()); } else { chooser.setMax(count); } chooserScrollPane = new JScrollPane(chooser); final Dimension screenResolution = Toolkit.getDefaultToolkit().getScreenSize(); int availHeight = screenResolution.height - 80; final int availWidth = screenResolution.width - 30; availHeight -= 50; chooserScrollPane.setPreferredSize(new Dimension( (chooserScrollPane.getPreferredSize().width > availWidth ? availWidth : (chooserScrollPane.getPreferredSize().height > availHeight ? chooserScrollPane.getPreferredSize().width + 22 : chooserScrollPane.getPreferredSize().width)), (chooserScrollPane.getPreferredSize().height > availHeight ? availHeight : chooserScrollPane.getPreferredSize().height))); chooserScrollPane.setBorder(new LineBorder(chooserScrollPane.getBackground())); } final String[] options = {"Ok", "Cancel"}; final String focus = BattleDisplay.getFocusOnOwnCasualtiesNotification() ? options[0] : null; final int option = JOptionPane.showOptionDialog(BattleDisplay.this, chooserScrollPane, hit.getName() + " select casualties", JOptionPane.OK_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, focus); if (option != 0) { return; } final List<Unit> killed = chooser.getSelected(false); final List<Unit> damaged = chooser.getSelectedDamagedMultipleHitPointUnits(); if (!isEditMode && (killed.size() + damaged.size() != count)) { JOptionPane.showMessageDialog(BattleDisplay.this, "Wrong number of casualties selected", hit.getName() + " select casualties", JOptionPane.ERROR_MESSAGE); } else { final CasualtyDetails response = new CasualtyDetails(killed, damaged, false); casualtyDetails.set(response); m_dicePanel.clear(); m_actionButton.setEnabled(false); m_actionButton.setAction(m_nullAction); continueLatch.countDown(); } } }); }); m_mapPanel.getUIContext().addShutdownLatch(continueLatch); try { continueLatch.await(); } catch (final InterruptedException ex) { ClientLogger.logQuietly(ex); } finally { m_mapPanel.getUIContext().removeShutdownLatch(continueLatch); } return casualtyDetails.get(); } private void initLayout() { final JPanel attackerUnits = new JPanel(); attackerUnits.setLayout(new BoxLayout(attackerUnits, BoxLayout.Y_AXIS)); attackerUnits.add(getPlayerComponent(m_attacker)); attackerUnits.add(Box.createGlue()); final JTable attackerTable = new BattleTable(m_attackerModel); attackerUnits.add(attackerTable); attackerUnits.add(attackerTable.getTableHeader()); final JPanel defenderUnits = new JPanel(); defenderUnits.setLayout(new BoxLayout(defenderUnits, BoxLayout.Y_AXIS)); defenderUnits.add(getPlayerComponent(m_defender)); defenderUnits.add(Box.createGlue()); final JTable defenderTable = new BattleTable(m_defenderModel); defenderUnits.add(defenderTable); defenderUnits.add(defenderTable.getTableHeader()); final JPanel north = new JPanel(); north.setLayout(new BoxLayout(north, BoxLayout.X_AXIS)); north.add(attackerUnits); north.add(getTerritoryComponent()); north.add(defenderUnits); m_messagePanel.setLayout(new BorderLayout()); m_messagePanel.add(m_messageLabel, BorderLayout.CENTER); m_steps = new BattleStepsPanel(); m_mapPanel.getUIContext().addActive(m_steps); m_steps.setBorder(new EtchedBorder(EtchedBorder.LOWERED)); m_dicePanel = new DicePanel(m_mapPanel.getUIContext(), m_data); m_actionPanel = new JPanel(); m_actionPanel.setLayout(m_actionLayout); m_actionPanel.add(m_dicePanel, DICE_KEY); m_actionPanel.add(m_casualties, CASUALTIES_KEY); m_actionPanel.add(m_messagePanel, MESSAGE_KEY); final JPanel diceAndSteps = new JPanel(); diceAndSteps.setLayout(new BorderLayout()); diceAndSteps.add(m_steps, BorderLayout.WEST); diceAndSteps.add(m_actionPanel, BorderLayout.CENTER); m_casualtiesInstantPanelAttacker.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 2)); m_casualtiesInstantPanelAttacker.setBorder(new EtchedBorder(EtchedBorder.LOWERED)); m_casualtiesInstantPanelAttacker.add(LABEL_NONE_ATTACKER); m_casualtiesInstantPanelDefender.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 2)); m_casualtiesInstantPanelDefender.setBorder(new EtchedBorder(EtchedBorder.LOWERED)); m_casualtiesInstantPanelDefender.add(LABEL_NONE_DEFENDER); final JPanel lInstantCasualtiesPanel = new JPanel(); lInstantCasualtiesPanel.setBorder(new EtchedBorder(EtchedBorder.LOWERED)); lInstantCasualtiesPanel.setLayout(new GridBagLayout()); final JLabel lCausalities = new JLabel("Casualties", SwingConstants.CENTER); lCausalities.setFont(getPlayerComponent(m_attacker).getFont().deriveFont(Font.BOLD, 14)); lInstantCasualtiesPanel.add(lCausalities, new GridBagConstraints(0, 0, 2, 1, 1.0d, 1.0d, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); lInstantCasualtiesPanel.add(m_casualtiesInstantPanelAttacker, new GridBagConstraints(0, 2, 1, 1, 1.0d, 1.0d, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); lInstantCasualtiesPanel.add(m_casualtiesInstantPanelDefender, new GridBagConstraints(1, 2, 1, 1, 1.0d, 1.0d, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); diceAndSteps.add(lInstantCasualtiesPanel, BorderLayout.SOUTH); setLayout(new BorderLayout()); add(north, BorderLayout.NORTH); add(diceAndSteps, BorderLayout.CENTER); add(m_actionButton, BorderLayout.SOUTH); m_actionButton.setEnabled(false); if (!SystemProperties.isMac()) { m_actionButton.setBackground(Color.lightGray.darker()); m_actionButton.setForeground(Color.white); } setDefaultWidths(defenderTable); setDefaultWidths(attackerTable); final Action continueAction = SwingAction.of(e -> { final Action a = m_actionButton.getAction(); if (a != null) { a.actionPerformed(null); } }); // press space to continue final String key = "battle.display.press.space.to.continue"; getActionMap().put(key, continueAction); getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), key); } /** * Shorten columns with no units. */ private static void setDefaultWidths(final JTable table) { for (int column = 0; column < table.getColumnCount(); column++) { boolean hasData = false; for (int row = 0; row < table.getRowCount(); row++) { hasData |= (table.getValueAt(row, column) != TableData.NULL); } if (!hasData) { table.getColumnModel().getColumn(column).setPreferredWidth(8); } } } public void setStep(final String step) { m_steps.setStep(step); } public void battleInfo(final DiceRoll message, final String step) { setStep(step); m_dicePanel.setDiceRoll(message); m_actionLayout.show(m_actionPanel, DICE_KEY); } public void battleInfo(final String message, final String step) { m_messageLabel.setText(message); setStep(step); m_actionLayout.show(m_actionPanel, MESSAGE_KEY); } public void listBattle(final List<String> steps) { m_steps.listBattle(steps); } private static JComponent getPlayerComponent(final PlayerID id) { final JLabel player = new JLabel(id.getName()); player.setBorder(new javax.swing.border.EmptyBorder(5, 5, 5, 5)); player.setFont(player.getFont().deriveFont((float) 14)); return player; } private static final int MY_WIDTH = 100; private static final int MY_HEIGHT = 100; private JComponent getTerritoryComponent() { final Image finalImage = Util.createImage(MY_WIDTH, MY_HEIGHT, true); final Image territory = m_mapPanel.getTerritoryImage(m_location); final Graphics g = finalImage.getGraphics(); g.drawImage(territory, 0, 0, MY_WIDTH, MY_HEIGHT, this); g.dispose(); return new JLabel(new ImageIcon(finalImage)); } } class BattleTable extends JTable { private static final long serialVersionUID = 6737857639382012817L; BattleTable(final BattleModel model) { super(model); setDefaultRenderer(Object.class, new Renderer()); setRowHeight(UnitImageFactory.DEFAULT_UNIT_ICON_SIZE + 5); setBackground(new JButton().getBackground()); setShowHorizontalLines(false); getTableHeader().setReorderingAllowed(false); // getTableHeader().setResizingAllowed(false); } } class BattleModel extends DefaultTableModel { private static final long serialVersionUID = 6913324191512043963L; private final IUIContext m_uiContext; private final GameData m_data; // is the player the aggressor? private final boolean m_attack; private final Collection<Unit> m_units; private final Territory m_location; private final BattleType m_battleType; private final Collection<TerritoryEffect> m_territoryEffects; private final boolean m_isAmphibious; private final Collection<Unit> m_amphibiousLandAttackers; private BattleModel m_enemyBattleModel = null; private static String[] varDiceArray(final GameData data) { // TODO Soft set the maximum bonus to-hit plus 1 for 0 based count(+2 total currently) final String[] diceColumns = new String[data.getDiceSides() + 1]; { for (int i = 0; i < diceColumns.length; i++) { if (i == 0) { diceColumns[i] = " "; } else { diceColumns[i] = Integer.toString(i); } } } return diceColumns; } BattleModel(final Collection<Unit> units, final boolean attack, final BattleType battleType, final PlayerID player, final GameData data, final Territory battleLocation, final Collection<TerritoryEffect> territoryEffects, final boolean isAmphibious, final Collection<Unit> amphibiousLandAttackers, final IUIContext uiContext) { super(new Object[0][0], varDiceArray(data)); m_uiContext = uiContext; m_data = data; m_attack = attack; // were going to modify the units m_units = new ArrayList<>(units); m_location = battleLocation; m_battleType = battleType; m_territoryEffects = territoryEffects; m_isAmphibious = isAmphibious; m_amphibiousLandAttackers = amphibiousLandAttackers; } public void setEnemyBattleModel(final BattleModel enemyBattleModel) { m_enemyBattleModel = enemyBattleModel; } public void notifyRetreat(final Collection<Unit> retreating) { m_units.removeAll(retreating); refresh(); } public void removeCasualties(final Collection<Unit> killed) { m_units.removeAll(killed); refresh(); } public void addUnits(final Collection<Unit> units) { m_units.addAll(units); refresh(); } Collection<Unit> getUnits() { return m_units; } /** * refresh the model from m_units. */ public void refresh() { // TODO Soft set the maximum bonus to-hit plus 1 for 0 based count(+2 total currently) // Soft code the # of columns final List<List<TableData>> columns = new ArrayList<>(m_data.getDiceSides() + 1); for (int i = 0; i <= m_data.getDiceSides(); i++) { columns.add(i, new ArrayList<>()); } final List<Unit> units = new ArrayList<>(m_units); DiceRoll.sortByStrength(units, !m_attack); final Map<Unit, Tuple<Integer, Integer>> unitPowerAndRollsMap; m_data.acquireReadLock(); try { if (m_battleType.isAirPreBattleOrPreRaid()) { unitPowerAndRollsMap = null; } else { unitPowerAndRollsMap = DiceRoll.getUnitPowerAndRollsForNormalBattles(units, new ArrayList<>(m_enemyBattleModel.getUnits()), !m_attack, false, m_data, m_location, m_territoryEffects, m_isAmphibious, m_amphibiousLandAttackers); } } finally { m_data.releaseReadLock(); } final int diceSides = m_data.getDiceSides(); final Collection<UnitCategory> unitCategories = UnitSeperator.categorize(units, null, false, false, false); for (final UnitCategory category : unitCategories) { int strength; final UnitAttachment attachment = UnitAttachment.get(category.getType()); final int[] shift = new int[m_data.getDiceSides() + 1]; for (final Unit current : category.getUnits()) { if (m_battleType.isAirPreBattleOrPreRaid()) { if (m_attack) { strength = attachment.getAirAttack(category.getOwner()); } else { strength = attachment.getAirDefense(category.getOwner()); } } else { // normal battle strength = unitPowerAndRollsMap.get(current).getFirst(); } strength = Math.min(Math.max(strength, 0), diceSides); shift[strength]++; } for (int i = 0; i <= m_data.getDiceSides(); i++) { if (shift[i] > 0) { columns.get(i).add(new TableData(category.getOwner(), shift[i], category.getType(), m_data, category.hasDamageOrBombingUnitDamage(), category.getDisabled(), m_uiContext)); } } // TODO Kev determine if we need to identify if the unit is hit/disabled } // find the number of rows // this will be the size of the largest column int rowCount = 1; for (final List<TableData> column : columns) { rowCount = Math.max(rowCount, column.size()); } setNumRows(rowCount); for (int row = 0; row < rowCount; row++) { for (int column = 0; column < columns.size(); column++) { // if the column has that many items, add to the table, else add null if (columns.get(column).size() > row) { setValueAt(columns.get(column).get(row), row, column); } else { setValueAt(TableData.NULL, row, column); } } } } @Override public boolean isCellEditable(final int row, final int column) { return false; } } class Renderer implements TableCellRenderer { JLabel m_stamp = new JLabel(); @Override public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) { ((TableData) value).updateStamp(m_stamp); return m_stamp; } } class TableData { static final TableData NULL = new TableData(); private int m_count; private Optional<ImageIcon> m_icon; private TableData() {} TableData(final PlayerID player, final int count, final UnitType type, final GameData data, final boolean damaged, final boolean disabled, final IUIContext uiContext) { m_count = count; m_icon = uiContext.getUnitImageFactory().getIcon(type, player, data, damaged, disabled); } public void updateStamp(final JLabel stamp) { if (m_count == 0) { stamp.setText(""); stamp.setIcon(null); } else { stamp.setText("x" + m_count); if (m_icon.isPresent()) { stamp.setIcon(m_icon.get()); } } } } class CasualtyNotificationPanel extends JPanel { private static final long serialVersionUID = -8254027929090027450L; private final DicePanel m_dice; private final JPanel m_killed = new JPanel(); private final JPanel m_damaged = new JPanel(); private final GameData m_data; private final IUIContext m_uiContext; public CasualtyNotificationPanel(final GameData data, final IUIContext uiContext) { m_data = data; m_uiContext = uiContext; m_dice = new DicePanel(uiContext, data); setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); add(m_dice); add(m_killed); add(m_damaged); } protected void setNotification(final DiceRoll dice, final Collection<Unit> killed, final Collection<Unit> damaged, final Map<Unit, Collection<Unit>> dependents) { final boolean isEditMode = (dice == null); if (!isEditMode) { m_dice.setDiceRoll(dice); } m_killed.removeAll(); m_damaged.removeAll(); if (!killed.isEmpty()) { m_killed.add(new JLabel("Killed")); } final Iterator<UnitCategory> killedIter = UnitSeperator.categorize(killed, dependents, false, false).iterator(); categorizeUnits(killedIter, false, false); damaged.removeAll(killed); if (!damaged.isEmpty()) { m_damaged.add(new JLabel("Damaged")); } final Iterator<UnitCategory> damagedIter = UnitSeperator.categorize(damaged, dependents, false, false).iterator(); categorizeUnits(damagedIter, true, true); invalidate(); validate(); } protected void setNotificationShort(final Collection<Unit> killed, final Map<Unit, Collection<Unit>> dependents) { m_killed.removeAll(); if (!killed.isEmpty()) { m_killed.add(new JLabel("Killed")); } final Iterator<UnitCategory> killedIter = UnitSeperator.categorize(killed, dependents, false, false).iterator(); categorizeUnits(killedIter, false, false); invalidate(); validate(); } private void categorizeUnits(final Iterator<UnitCategory> categoryIter, final boolean damaged, final boolean disabled) { while (categoryIter.hasNext()) { final UnitCategory category = categoryIter.next(); final JPanel panel = new JPanel(); // TODO Kev determine if we need to identify if the unit is hit/disabled final Optional<ImageIcon> unitImage = m_uiContext.getUnitImageFactory().getIcon(category.getType(), category.getOwner(), m_data, damaged && category.hasDamageOrBombingUnitDamage(), disabled && category.getDisabled()); final JLabel unit = unitImage.isPresent() ? new JLabel(unitImage.get()) : new JLabel(); panel.add(unit); for (final UnitOwner owner : category.getDependents()) { unit.add(m_uiContext.createUnitImageJLabel(owner.getType(), owner.getOwner(), m_data)); } panel.add(new JLabel("x " + category.getUnits().size())); if (damaged) { m_damaged.add(panel); } else { m_killed.add(panel); } } } }