package games.strategy.triplea.ui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import games.strategy.debug.ClientLogger;
import games.strategy.debug.ErrorConsole;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.Unit;
import games.strategy.engine.framework.GameRunner;
import games.strategy.engine.gamePlayer.IGamePlayer;
import games.strategy.net.GUID;
import games.strategy.triplea.TripleAPlayer;
import games.strategy.triplea.delegate.DiceRoll;
import games.strategy.triplea.delegate.Die;
import games.strategy.triplea.delegate.IBattle.BattleType;
import games.strategy.triplea.delegate.dataObjects.CasualtyDetails;
import games.strategy.triplea.delegate.dataObjects.CasualtyList;
import games.strategy.triplea.delegate.dataObjects.FightBattleDetails;
import games.strategy.ui.SwingAction;
import games.strategy.ui.SwingComponents;
import games.strategy.ui.Util;
import games.strategy.ui.Util.Task;
import games.strategy.util.EventThreadJOptionPane;
import games.strategy.util.ThreadUtil;
/**
* UI for fighting battles.
*/
public class BattlePanel extends ActionPanel {
private static final long serialVersionUID = 5304208569738042592L;
private final JLabel m_actionLabel = new JLabel();
private FightBattleDetails m_fightBattleMessage;
private volatile BattleDisplay m_battleDisplay;
// if we are showing a battle, then this will be set to the currently
// displayed battle. This will only be set after the display
// is shown on the screen
private volatile GUID m_currentBattleDisplayed;
// there is a bug in linux jdk1.5.0_6 where frames are not
// being garbage collected
// reuse one frame
private final JFrame m_battleFrame;
Map<BattleType, Collection<Territory>> m_battles;
/** Creates new BattlePanel. */
public BattlePanel(final GameData data, final MapPanel map) {
super(data, map);
m_battleFrame = new JFrame() {
private static final long serialVersionUID = -947813247703330615L;
@Override
public void dispose() {
games.strategy.engine.random.PBEMDiceRoller.setFocusWindow(null);
super.dispose();
}
};
m_battleFrame.setIconImage(GameRunner.getGameIcon(m_battleFrame));
getMap().getUIContext().addShutdownWindow(m_battleFrame);
m_battleFrame.addWindowListener(new WindowListener() {
@Override
public void windowActivated(final WindowEvent e) {
SwingUtilities.invokeLater(() -> {
if (m_battleDisplay != null) {
m_battleDisplay.takeFocus();
}
});
}
@Override
public void windowClosed(final WindowEvent e) {}
@Override
public void windowClosing(final WindowEvent e) {}
@Override
public void windowDeactivated(final WindowEvent e) {}
@Override
public void windowDeiconified(final WindowEvent e) {}
@Override
public void windowIconified(final WindowEvent e) {}
@Override
public void windowOpened(final WindowEvent e) {}
});
}
public void setBattlesAndBombing(final Map<BattleType, Collection<Territory>> battles) {
m_battles = battles;
}
@Override
public void display(final PlayerID id) {
super.display(id);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
removeAll();
m_actionLabel.setText(id.getName() + " battle");
setLayout(new BorderLayout());
final JPanel panel = SwingComponents.gridPanel(0, 1);
panel.add(m_actionLabel);
for (final Entry<BattleType, Collection<Territory>> entry : m_battles.entrySet()) {
for (final Territory t : entry.getValue()) {
addBattleActions(panel, t, entry.getKey().isBombingRun(), entry.getKey());
}
}
add(panel, BorderLayout.NORTH);
SwingUtilities.invokeLater(REFRESH);
}
private void addBattleActions(final JPanel panel, final Territory territory, final boolean bomb,
final BattleType battleType) {
final JPanel innerPanel = new JPanel();
innerPanel.setLayout(new BorderLayout());
innerPanel.add(new JButton(new FightBattleAction(territory, bomb, battleType)), BorderLayout.CENTER);
innerPanel.add(new JButton(new CenterBattleAction(territory)), BorderLayout.EAST);
panel.add(innerPanel);
}
});
}
public void notifyRetreat(final String messageShort, final String messageLong, final String step,
final PlayerID retreatingPlayer) {
SwingUtilities.invokeLater(() -> {
if (m_battleDisplay != null) {
m_battleDisplay.battleInfo(messageLong, step);
}
});
}
public void showDice(final DiceRoll dice, final String step) {
SwingUtilities.invokeLater(() -> {
if (m_battleDisplay != null) {
m_battleDisplay.battleInfo(dice, step);
}
});
}
public void battleEndMessage(final String message) {
SwingUtilities.invokeLater(() -> {
if (m_battleDisplay != null) {
m_battleDisplay.endBattle(message, m_battleFrame);
}
});
}
private void cleanUpBattleWindow() {
if (m_battleDisplay != null) {
m_currentBattleDisplayed = null;
m_battleDisplay.cleanUp();
m_battleFrame.getContentPane().removeAll();
m_battleDisplay = null;
games.strategy.engine.random.PBEMDiceRoller.setFocusWindow(m_battleFrame);
}
}
private boolean ensureBattleIsDisplayed(final GUID battleID) {
if (SwingUtilities.isEventDispatchThread()) {
throw new IllegalStateException("Wrong threads");
}
GUID displayed = m_currentBattleDisplayed;
int count = 0;
while (displayed == null || !battleID.equals(displayed)) {
count++;
ThreadUtil.sleep(count);
// something is wrong, we shouldnt have to wait this long
if (count > 200) {
ErrorConsole.getConsole().dumpStacks();
new IllegalStateException(
"battle not displayed, looking for:" + battleID + " showing:" + m_currentBattleDisplayed).printStackTrace();
return false;
}
displayed = m_currentBattleDisplayed;
}
return true;
}
protected JFrame getBattleFrame() {
return m_battleFrame;
}
public void listBattle(final GUID battleID, final List<String> steps) {
if (!SwingUtilities.isEventDispatchThread()) {
final Runnable r = () -> {
// recursive call
listBattle(battleID, steps);
};
try {
SwingUtilities.invokeLater(r);
} catch (final Exception e) {
ClientLogger.logQuietly(e);
}
return;
}
removeAll();
if (m_battleDisplay != null) {
getMap().centerOn(m_battleDisplay.getBattleLocation());
m_battleDisplay.listBattle(steps);
}
}
public void showBattle(final GUID battleID, final Territory location,
final Collection<Unit> attackingUnits, final Collection<Unit> defendingUnits, final Collection<Unit> killedUnits,
final Collection<Unit> attackingWaitingToDie, final Collection<Unit> defendingWaitingToDie,
final PlayerID attacker, final PlayerID defender,
final boolean isAmphibious, final BattleType battleType, final Collection<Unit> amphibiousLandAttackers) {
SwingAction.invokeAndWait(() -> {
if (m_battleDisplay != null) {
cleanUpBattleWindow();
m_currentBattleDisplayed = null;
}
if (!getMap().getUIContext().getShowMapOnly()) {
m_battleDisplay = new BattleDisplay(getData(), location, attacker, defender, attackingUnits, defendingUnits,
killedUnits, attackingWaitingToDie, defendingWaitingToDie, battleID, BattlePanel.this.getMap(),
isAmphibious, battleType, amphibiousLandAttackers);
m_battleFrame.setTitle(attacker.getName() + " attacks " + defender.getName() + " in " + location.getName());
m_battleFrame.getContentPane().removeAll();
m_battleFrame.getContentPane().add(m_battleDisplay);
m_battleFrame.setSize(800, 600);
m_battleFrame.setLocationRelativeTo(JOptionPane.getFrameForComponent(BattlePanel.this));
games.strategy.engine.random.PBEMDiceRoller.setFocusWindow(m_battleFrame);
boolean foundHumanInBattle = false;
for (final IGamePlayer gamePlayer : getMap().getUIContext().getLocalPlayers().getLocalPlayers()) {
if ((gamePlayer.getPlayerID().equals(attacker) && gamePlayer instanceof TripleAPlayer)
|| (gamePlayer.getPlayerID().equals(defender) && gamePlayer instanceof TripleAPlayer)) {
foundHumanInBattle = true;
break;
}
}
if (getMap().getUIContext().getShowBattlesBetweenAIs() || foundHumanInBattle) {
m_battleFrame.setVisible(true);
m_battleFrame.validate();
m_battleFrame.invalidate();
m_battleFrame.repaint();
} else {
m_battleFrame.setVisible(false);
}
m_battleFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
m_currentBattleDisplayed = battleID;
SwingUtilities.invokeLater(() -> m_battleFrame.toFront());
}
});
}
public FightBattleDetails waitForBattleSelection() {
waitForRelease();
if (m_fightBattleMessage != null) {
getMap().centerOn(m_fightBattleMessage.getWhere());
}
return m_fightBattleMessage;
}
/**
* Ask user which territory to bombard with a given unit.
*/
public Territory getBombardment(final Unit unit, final Territory unitTerritory,
final Collection<Territory> territories, final boolean noneAvailable) {
final BombardComponent comp = Util.runInSwingEventThread(
() -> new BombardComponent(unit, unitTerritory, territories, noneAvailable));
int option = JOptionPane.NO_OPTION;
while (option != JOptionPane.OK_OPTION) {
option = EventThreadJOptionPane.showConfirmDialog(this, comp, "Bombardment Territory Selection",
JOptionPane.OK_OPTION, getMap().getUIContext().getCountDownLatchHandler());
}
return comp.getSelection();
}
public boolean getAttackSubs(final Territory terr) {
getMap().centerOn(terr);
return EventThreadJOptionPane.showConfirmDialog(null, "Attack submarines in " + terr.toString() + "?", "Attack",
JOptionPane.YES_NO_OPTION, getMap().getUIContext().getCountDownLatchHandler()) == 0;
}
public boolean getAttackTransports(final Territory terr) {
getMap().centerOn(terr);
return EventThreadJOptionPane.showConfirmDialog(null, "Attack transports in " + terr.toString() + "?", "Attack",
JOptionPane.YES_NO_OPTION, getMap().getUIContext().getCountDownLatchHandler()) == 0;
}
public boolean getAttackUnits(final Territory terr) {
getMap().centerOn(terr);
return EventThreadJOptionPane.showConfirmDialog(null, "Attack units in " + terr.toString() + "?", "Attack",
JOptionPane.YES_NO_OPTION, getMap().getUIContext().getCountDownLatchHandler()) == 0;
}
public boolean getShoreBombard(final Territory terr) {
getMap().centerOn(terr);
return EventThreadJOptionPane.showConfirmDialog(null, "Conduct naval bombard in " + terr.toString() + "?",
"Bombard", JOptionPane.YES_NO_OPTION, getMap().getUIContext().getCountDownLatchHandler()) == 0;
}
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) {
SwingUtilities.invokeLater(() -> {
if (m_battleDisplay != null) {
m_battleDisplay.casualtyNotification(step, dice, player, killed, damaged, dependents);
}
});
}
public void deadUnitNotification(final PlayerID player, final Collection<Unit> killed,
final Map<Unit, Collection<Unit>> dependents) {
SwingUtilities.invokeLater(() -> {
if (m_battleDisplay != null) {
m_battleDisplay.deadUnitNotification(player, killed, dependents);
}
});
}
public void changedUnitsNotification(final PlayerID player, final Collection<Unit> removedUnits,
final Collection<Unit> addedUnits, final Map<Unit, Collection<Unit>> dependents) {
SwingUtilities.invokeLater(() -> {
if (m_battleDisplay != null) {
m_battleDisplay.changedUnitsNotification(player, removedUnits, addedUnits, dependents);
}
});
}
public void confirmCasualties(final GUID battleId, final String message) {
// something is wrong
if (!ensureBattleIsDisplayed(battleId)) {
return;
}
m_battleDisplay.waitForConfirmation(message);
}
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 GUID battleID, final boolean allowMultipleHitsPerUnit) {
// if the battle display is null, then this is an aa fire during move
if (battleID == null) {
return getCasualtiesAA(selectFrom, dependents, count, message, dice, hit, defaultCasualties,
allowMultipleHitsPerUnit);
} else {
// something is wong
if (!ensureBattleIsDisplayed(battleID)) {
System.out.println("Battle Not Displayed?? " + message);
return new CasualtyDetails(defaultCasualties.getKilled(), defaultCasualties.getDamaged(), true);
}
return m_battleDisplay.getCasualties(selectFrom, dependents, count, message, dice, hit, defaultCasualties,
allowMultipleHitsPerUnit);
}
}
private CasualtyDetails getCasualtiesAA(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) {
final Task<CasualtyDetails> task = () -> {
final boolean isEditMode = (dice == null);
final UnitChooser chooser = new UnitChooser(selectFrom, defaultCasualties, dependents, getData(),
allowMultipleHitsPerUnit, getMap().getUIContext());
chooser.setTitle(message);
if (isEditMode) {
chooser.setMax(selectFrom.size());
} else {
chooser.setMax(count);
}
final DicePanel dicePanel = new DicePanel(getMap().getUIContext(), getData());
if (!isEditMode) {
dicePanel.setDiceRoll(dice);
}
final JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.add(chooser, BorderLayout.CENTER);
dicePanel.setMaximumSize(new Dimension(450, 600));
dicePanel.setPreferredSize(new Dimension(300, (int) dicePanel.getPreferredSize().getHeight()));
panel.add(dicePanel, BorderLayout.SOUTH);
final String[] options = {"OK"};
EventThreadJOptionPane.showOptionDialog(getRootPane(), panel, hit.getName() + " select casualties",
JOptionPane.OK_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, null,
getMap().getUIContext().getCountDownLatchHandler());
final List<Unit> killed = chooser.getSelected(false);
final CasualtyDetails response =
new CasualtyDetails(killed, chooser.getSelectedDamagedMultipleHitPointUnits(), false);
return response;
};
return Util.runInSwingEventThread(task);
}
public Territory getRetreat(final GUID battleID, final String message, final Collection<Territory> possible,
final boolean submerge) {
// something is really wrong
if (!ensureBattleIsDisplayed(battleID)) {
return null;
}
return m_battleDisplay.getRetreat(message, possible, submerge);
}
public void gotoStep(final GUID battleID, final String step) {
SwingUtilities.invokeLater(() -> {
if (m_battleDisplay != null) {
m_battleDisplay.setStep(step);
}
});
}
public void notifyRetreat(final Collection<Unit> retreating) {
SwingUtilities.invokeLater(() -> {
if (m_battleDisplay != null) {
m_battleDisplay.notifyRetreat(retreating);
}
});
}
public void bombingResults(final GUID battleID, final List<Die> dice, final int cost) {
SwingUtilities.invokeLater(() -> {
if (m_battleDisplay != null) {
m_battleDisplay.bombingResults(dice, cost);
}
});
}
Territory m_oldCenteredTerritory = null;
Timer m_CenterBattleActionTimer = null;
class CenterBattleAction extends AbstractAction {
private static final long serialVersionUID = -5071133874755970334L;
Territory m_territory;
CenterBattleAction(final Territory battleSite) {
super("Center");
m_territory = battleSite;
}
@Override
public void actionPerformed(final ActionEvent e) {
if (m_CenterBattleActionTimer != null) {
m_CenterBattleActionTimer.cancel();
}
if (m_oldCenteredTerritory != null) {
getMap().clearTerritoryOverlay(m_oldCenteredTerritory);
}
getMap().centerOn(m_territory);
m_CenterBattleActionTimer = new Timer();
m_CenterBattleActionTimer.scheduleAtFixedRate(new MyTimerTask(m_territory, m_CenterBattleActionTimer), 150, 150);
m_oldCenteredTerritory = m_territory;
}
class MyTimerTask extends TimerTask {
private final Territory territory;
private final Timer m_stopTimer;
private int m_count = 0;
MyTimerTask(final Territory battleSite, final Timer stopTimer) {
territory = battleSite;
m_stopTimer = stopTimer;
}
@Override
public void run() {
if (m_count == 5) {
m_stopTimer.cancel();
}
if ((m_count % 3) == 0) {
getMap().setTerritoryOverlayForBorder(territory, Color.white);
getMap().paintImmediately(getMap().getBounds());
// TODO: getUIContext().getMapData().getBoundingRect(m_territory)); what kind of additional transformation
// needed here?
// TODO: setTerritoryOverlayForBorder is causing invalid ordered lock acquire atempt, why?
} else {
getMap().clearTerritoryOverlay(territory);
getMap().paintImmediately(getMap().getBounds());
// TODO: getUIContext().getMapData().getBoundingRect(m_territory)); what kind of additional transformation
// needed here?
// TODO: setTerritoryOverlayForBorder is causing invalid ordered lock acquire atempt, why?
}
m_count++;
}
}
}
class FightBattleAction extends AbstractAction {
private static final long serialVersionUID = 5510976406003707776L;
Territory m_territory;
boolean m_bomb;
BattleType m_type;
FightBattleAction(final Territory battleSite, final boolean bomb, final BattleType battleType) {
super(battleType.toString() + " in " + battleSite.getName() + "...");
m_territory = battleSite;
m_bomb = bomb;
m_type = battleType;
}
@Override
public void actionPerformed(final ActionEvent actionEvent) {
if (m_oldCenteredTerritory != null) {
getMap().clearTerritoryOverlay(m_oldCenteredTerritory);
}
m_fightBattleMessage = new FightBattleDetails(m_territory, m_bomb, m_type);
release();
}
}
@Override
public String toString() {
return "BattlePanel";
}
private class BombardComponent extends JPanel {
private static final long serialVersionUID = -2388895995673156507L;
private final JList<Object> m_list;
BombardComponent(final Unit unit, final Territory unitTerritory, final Collection<Territory> territories,
final boolean noneAvailable) {
this.setLayout(new BorderLayout());
final String unitName = unit.getUnitType().getName() + " in " + unitTerritory;
final JLabel label = new JLabel("Which territory should " + unitName + " bombard?");
this.add(label, BorderLayout.NORTH);
final Vector<Object> listElements = new Vector<>(territories);
if (noneAvailable) {
listElements.add(0, "None");
}
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);
}
public Territory getSelection() {
final Object selected = m_list.getSelectedValue();
if (selected instanceof Territory) {
return (Territory) selected;
}
// User selected "None" option
return null;
}
}
}