diff --git a/megamek/src/megamek/client/AbstractClient.java b/megamek/src/megamek/client/AbstractClient.java index f57ad9e6d1a..a1905fde4ec 100644 --- a/megamek/src/megamek/client/AbstractClient.java +++ b/megamek/src/megamek/client/AbstractClient.java @@ -156,7 +156,7 @@ protected void disconnected() { } if (!host.equals(MMConstants.LOCALHOST)) { - getIGame().fireGameEvent(new GamePlayerDisconnectedEvent(this, getLocalPlayer())); + getGame().fireGameEvent(new GamePlayerDisconnectedEvent(this, getLocalPlayer())); } } } @@ -185,7 +185,7 @@ public void sendNextPlayer() { * Sends the info associated with the local player. */ public void sendPlayerInfo() { - send(new Packet(PacketCommand.PLAYER_UPDATE, getIGame().getPlayer(localPlayerNumber))); + send(new Packet(PacketCommand.PLAYER_UPDATE, getGame().getPlayer(localPlayerNumber))); } /** @@ -217,9 +217,9 @@ protected void receivePlayerInfo(Packet c) { int pindex = c.getIntValue(0); Player newPlayer = (Player) c.getObject(1); if (!playerExists(newPlayer.getId())) { - getIGame().addPlayer(pindex, newPlayer); + getGame().addPlayer(pindex, newPlayer); } else { - getIGame().setPlayer(pindex, newPlayer); + getGame().setPlayer(pindex, newPlayer); } } @@ -301,11 +301,11 @@ protected synchronized void checkDuplicateNamesDuringAdd(Entity entity) { protected void receiveUnitReplace(Packet packet) { @SuppressWarnings(value = "unchecked") List forces = (List) packet.getObject(1); - forces.forEach(force -> getIGame().getForces().replace(force.getId(), force)); + forces.forEach(force -> getGame().getForces().replace(force.getId(), force)); @SuppressWarnings(value = "unchecked") List units = (List) packet.getObject(0); - getIGame().replaceUnits(units); + getGame().replaceUnits(units); } /** @@ -441,25 +441,25 @@ protected boolean handleGameIndependentPacket(Packet packet) { Player player = getPlayer(packet.getIntValue(0)); if (player != null) { player.setDone(packet.getBooleanValue(1)); - getIGame().fireGameEvent(new GamePlayerChangeEvent(player, player)); + getGame().fireGameEvent(new GamePlayerChangeEvent(player, player)); } break; case PLAYER_REMOVE: bots.values().removeIf(bot -> bot.localPlayerNumber == packet.getIntValue(0)); - getIGame().removePlayer(packet.getIntValue(0)); + getGame().removePlayer(packet.getIntValue(0)); break; case CHAT: possiblyWriteToLog((String) packet.getObject(0)); - getIGame().fireGameEvent(new GamePlayerChatEvent(this, null, (String) packet.getObject(0))); + getGame().fireGameEvent(new GamePlayerChatEvent(this, null, (String) packet.getObject(0))); break; case ENTITY_ADD: receiveUnitReplace(packet); break; case SENDING_BOARD: - getIGame().receiveBoards((Map) packet.getObject(0)); + getGame().receiveBoards((Map) packet.getObject(0)); break; case ROUND_UPDATE: - getIGame().setCurrentRound(packet.getIntValue(0)); + getGame().setCurrentRound(packet.getIntValue(0)); break; case PHASE_CHANGE: changePhase((GamePhase) packet.getObject(0)); @@ -485,7 +485,7 @@ protected void possiblyWriteToLog(String message) { * Changes the game phase, and the displays that go along with it. */ public void changePhase(GamePhase phase) { - getIGame().receivePhase(phase); + getGame().receivePhase(phase); switch (phase) { case STARTING_SCENARIO: case EXCHANGE: diff --git a/megamek/src/megamek/client/Client.java b/megamek/src/megamek/client/Client.java index eaa3718a956..c8bb6ff8ca7 100644 --- a/megamek/src/megamek/client/Client.java +++ b/megamek/src/megamek/client/Client.java @@ -99,16 +99,10 @@ public Client(String name, String host, int port) { } } - @Override - public IGame getIGame() { - return game; - } - public Game getGame() { return game; } - /** * Get hexes designated for automatic artillery hits. */ @@ -205,9 +199,7 @@ public void changePhase(GamePhase phase) { } } - /** - * is it my turn? - */ + @Override public boolean isMyTurn() { if (getGame().getPhase().isSimultaneous(getGame())) { return game.getTurnForPlayer(localPlayerNumber) != null; @@ -702,13 +694,13 @@ protected void receiveAttack(Packet c) { // Should be private? - public String receiveReport(List v) { - if (v == null) { + public String receiveReport(List reports) { + if (reports == null) { return "[null report vector]"; } StringBuffer report = new StringBuffer(); - for (Report r : v) { + for (Report r : reports) { report.append(r.getText()); } diff --git a/megamek/src/megamek/client/IClient.java b/megamek/src/megamek/client/IClient.java index 747e972632a..e956147a0d0 100644 --- a/megamek/src/megamek/client/IClient.java +++ b/megamek/src/megamek/client/IClient.java @@ -56,26 +56,31 @@ public interface IClient { * * @return The game of this client */ - IGame getIGame(); + IGame getGame(); /** @return The in-game object associated with the given ID. */ default Optional getInGameObject(int id) { - return getIGame().getInGameObject(id); + return getGame().getInGameObject(id); } /** @return A list of all in-game objects of the game. This list is copied and may be safely modified. */ default List getInGameObjects() { - return getIGame().getInGameObjects(); + return getGame().getInGameObjects(); } /** @return The player with the given ID, or null if there is no such player. */ default Player getPlayer(int id) { - return getIGame().getPlayer(id); + return getGame().getPlayer(id); } /** @return The ID of the player playing at this Client. */ int getLocalPlayerNumber(); + /** + * @return True when the currently active turn is a turn for the local player + */ + boolean isMyTurn(); + /** * Sets the ID of the player playing at this Client. * // TODO : only used by AddBotUtil -> could be included in a bot's constructor and removed here diff --git a/megamek/src/megamek/client/SBFClient.java b/megamek/src/megamek/client/SBFClient.java index dc06b47c7dc..1716f486130 100644 --- a/megamek/src/megamek/client/SBFClient.java +++ b/megamek/src/megamek/client/SBFClient.java @@ -18,20 +18,13 @@ */ package megamek.client; -import megamek.client.ui.swing.tooltip.PilotToolTip; -import megamek.client.ui.swing.util.UIUtil; import megamek.common.*; import megamek.common.net.packets.Packet; import megamek.common.strategicBattleSystems.SBFGame; -import megamek.common.util.ImageUtil; import org.apache.logging.log4j.LogManager; -import java.awt.*; -import java.awt.image.BufferedImage; import java.util.*; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -60,10 +53,15 @@ public SBFClient(String name, String host, int port) { } @Override - public IGame getIGame() { + public SBFGame getGame() { return game; } + @Override + public boolean isMyTurn() { + return (game.getTurn() != null) && game.getTurn().isValid(localPlayerNumber, game); + } + @Override @SuppressWarnings("unchecked") protected boolean handleGameSpecificPacket(Packet packet) { diff --git a/megamek/src/megamek/client/bot/BotClient.java b/megamek/src/megamek/client/bot/BotClient.java index 4bd99eece90..0525060cf5c 100644 --- a/megamek/src/megamek/client/bot/BotClient.java +++ b/megamek/src/megamek/client/bot/BotClient.java @@ -1188,7 +1188,7 @@ protected void receiveBuildingCollapse(Packet packet) { * Let's save ourselves a little processing time and not deal with any of it */ @Override - public String receiveReport(List v) { + public String receiveReport(List reports) { return ""; } diff --git a/megamek/src/megamek/client/bot/princess/Princess.java b/megamek/src/megamek/client/bot/princess/Princess.java index e60758f55a8..4a1b1151a5f 100644 --- a/megamek/src/megamek/client/bot/princess/Princess.java +++ b/megamek/src/megamek/client/bot/princess/Princess.java @@ -1610,11 +1610,6 @@ protected void initMovement() { } } - @Override - public Game getGame() { - return game; - } - @Override public void initialize() { try { diff --git a/megamek/src/megamek/client/ui/swing/AbstractClientGUI.java b/megamek/src/megamek/client/ui/swing/AbstractClientGUI.java index 2b20de99326..402d3081445 100644 --- a/megamek/src/megamek/client/ui/swing/AbstractClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/AbstractClientGUI.java @@ -18,7 +18,9 @@ */ package megamek.client.ui.swing; +import megamek.client.IClient; import megamek.client.ui.Messages; +import megamek.client.ui.swing.boardview.*; import megamek.client.ui.swing.util.UIUtil; import megamek.common.Configuration; import megamek.common.preference.ClientPreferences; @@ -30,7 +32,9 @@ import java.awt.event.WindowEvent; import java.io.File; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public abstract class AbstractClientGUI implements IClientGUI { @@ -49,6 +53,16 @@ public abstract class AbstractClientGUI implements IClientGUI { protected static final String FILENAME_ICON_256X256 = "megamek-icon-256x256.png"; protected final JFrame frame = new JFrame(Messages.getString("ClientGUI.title")); + private final IClient iClient; + + // BoardViews + protected final Map boardViews = new HashMap<>(); + protected final BoardViewsContainer boardViewsContainer = new BoardViewsContainer(this); + + public AbstractClientGUI(IClient iClient) { + this.iClient = iClient; + initializeFrame(); + } @Override public JFrame getFrame() { @@ -125,4 +139,7 @@ void saveSettings() { protected abstract boolean saveGame(); + public List boardViews() { + return new ArrayList<>(boardViews.values()); + } } diff --git a/megamek/src/megamek/client/ui/swing/AbstractPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/AbstractPhaseDisplay.java index 1b3a311016f..bf29073f495 100644 --- a/megamek/src/megamek/client/ui/swing/AbstractPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/AbstractPhaseDisplay.java @@ -18,269 +18,160 @@ import megamek.client.ui.swing.util.KeyCommandBind; import megamek.client.ui.swing.util.UIUtil; import megamek.client.ui.swing.widget.*; -import megamek.common.Configuration; import megamek.common.event.*; import megamek.common.util.Distractable; import megamek.common.util.DistractableAdapter; -import megamek.common.util.fileUtils.MegaMekFile; -import org.apache.logging.log4j.LogManager; -import javax.swing.*; -import java.awt.event.ActionEvent; -import java.io.File; -import java.net.URI; +import java.util.Objects; import static megamek.client.ui.swing.util.UIUtil.guiScaledFontHTML; public abstract class AbstractPhaseDisplay extends SkinnedJPanel implements BoardViewListener, GameListener, Distractable { - private static final long serialVersionUID = 4421205210788230341L; public static final int DONE_BUTTON_WIDTH = 160; - // Distraction implementation. - protected DistractableAdapter distracted = new DistractableAdapter(); + protected DistractableAdapter distracted = new DistractableAdapter(); protected MegamekButton butDone; + private final IClientGUI clientgui; - protected ClientGUI clientgui; - - ImageIcon backgroundIcon = null; - - protected AbstractPhaseDisplay(ClientGUI cg) { + protected AbstractPhaseDisplay(IClientGUI cg) { this(cg, SkinSpecification.UIComponents.PhaseDisplay.getComp(), SkinSpecification.UIComponents.PhaseDisplayDoneButton.getComp()); } - protected AbstractPhaseDisplay(ClientGUI cg, String borderSkinComp, - String buttonSkinComp) { + protected AbstractPhaseDisplay(IClientGUI cg, String borderSkinComp, String buttonSkinComp) { super(borderSkinComp, 0); - this.clientgui = cg; - SkinSpecification pdSkinSpec = SkinXMLHandler.getSkin(borderSkinComp); - - try { - if (!pdSkinSpec.backgrounds.isEmpty()) { - File file = new MegaMekFile(Configuration.widgetsDir(), - pdSkinSpec.backgrounds.get(0)).getFile(); - URI imgURL = file.toURI(); - if (!file.exists()) { - LogManager.getLogger().error("PhaseDisplay icon doesn't exist: " + file.getAbsolutePath()); - } else { - backgroundIcon = new ImageIcon(imgURL.toURL()); - } - } - } catch (Exception e) { - LogManager.getLogger().error("Error loading PhaseDisplay background image!", e); - } - + clientgui = Objects.requireNonNull(cg); setBorder(new MegamekBorder(borderSkinComp)); butDone = new MegamekButton("DONE", buttonSkinComp); String f = guiScaledFontHTML(UIUtil.uiLightViolet()) + KeyCommandBind.getDesc(KeyCommandBind.DONE)+ ""; butDone.setToolTipText("" + f + ""); butDone.setActionCommand("doneButton"); - if (clientgui != null) { - butDone.addActionListener(new AbstractAction() { - private static final long serialVersionUID = -5034474968902280850L; - - @Override - public void actionPerformed(ActionEvent e) { - if (isIgnoringEvents()) { - return; - } - if ((clientgui.getClient().isMyTurn()) - || (clientgui.getClient().getGame().getTurn() == null) - || (clientgui.getClient().getGame().getPhase().isReport())) { - ready(); - // When the turn is ended, we could miss a key release event - // This will ensure no repeating keys are stuck down - clientgui.controller.stopAllRepeating(); - } - } - }); - - clientgui.controller.registerCommandAction(KeyCommandBind.DONE, this::shouldPerformKeyCommands, - this::ready); - } + butDone.addActionListener(e -> { + if (shouldPerformKeyCommands()) { + done(); + } + }); + + MegaMekGUI.getKeyDispatcher().registerCommandAction(KeyCommandBind.DONE, this::shouldPerformKeyCommands, this::done); + } + + private void done() { + // When the turn is ended, we could miss a key release event + // This will ensure no repeating keys are stuck down + MegaMekGUI.getKeyDispatcher().stopAllRepeating(); + ready(); } private boolean shouldPerformKeyCommands() { return ((clientgui.getClient().isMyTurn() || (clientgui.getClient().getGame().getTurn() == null) || (clientgui.getClient().getGame().getPhase().isReport()))) - && !clientgui.getBoardView().getChatterBoxActive() + && !clientgui.shouldIgnoreHotKeys() && !isIgnoringEvents() && isVisible() && butDone.isEnabled(); } - /** - * Determine if the listener is currently distracted. - * - * @return true if the listener is ignoring events. - */ @Override - public boolean isIgnoringEvents() { + public final boolean isIgnoringEvents() { return distracted.isIgnoringEvents(); } - /** - * Specify if the listener should be distracted. - * - * @param distracted - * true if the listener should ignore events - * false if the listener should pay attention - * again. Events that occurred while the listener was distracted - * NOT going to be processed. - */ @Override - public void setIgnoringEvents(boolean distracted) { + public final void setIgnoringEvents(boolean distracted) { this.distracted.setIgnoringEvents(distracted); } - // + public void ready() { } + + public IClientGUI getClientgui() { + return clientgui; + } + // BoardListener - // + @Override - public void hexMoused(BoardViewEvent b) { - //noaction default - } + public void hexMoused(BoardViewEvent b) { } @Override - public void hexSelected(BoardViewEvent b) { - //noaction default - } + public void hexSelected(BoardViewEvent b) { } @Override - public void hexCursor(BoardViewEvent b) { - //noaction default - } + public void hexCursor(BoardViewEvent b) { } @Override - public void boardHexHighlighted(BoardViewEvent b) { - //noaction default - } + public void boardHexHighlighted(BoardViewEvent b) { } @Override - public void firstLOSHex(BoardViewEvent b) { - //noaction default - } + public void firstLOSHex(BoardViewEvent b) { } @Override - public void secondLOSHex(BoardViewEvent b) { - //noaction default - } + public void secondLOSHex(BoardViewEvent b) { } @Override - public void finishedMovingUnits(BoardViewEvent b) { - //noaction default - } + public void finishedMovingUnits(BoardViewEvent b) { } @Override - public void unitSelected(BoardViewEvent b) { - //noaction default - } + public void unitSelected(BoardViewEvent b) { } // GameListener - // @Override - public void gamePlayerConnected(GamePlayerConnectedEvent e) { - //noaction default - } + public void gamePlayerConnected(GamePlayerConnectedEvent e) { } @Override - public void gamePlayerDisconnected(GamePlayerDisconnectedEvent e) { - //noaction default - } + public void gamePlayerDisconnected(GamePlayerDisconnectedEvent e) { } @Override - public void gamePlayerChange(GamePlayerChangeEvent e) { - //noaction default - } + public void gamePlayerChange(GamePlayerChangeEvent e) { } @Override - public void gamePlayerChat(GamePlayerChatEvent e) { - //noaction default - } + public void gamePlayerChat(GamePlayerChatEvent e) { } @Override - public void gamePhaseChange(GamePhaseChangeEvent e) { - //noaction default - } + public void gamePhaseChange(GamePhaseChangeEvent e) { } @Override - public void gameTurnChange(GameTurnChangeEvent e) { - //noaction default - } + public void gameTurnChange(GameTurnChangeEvent e) { } @Override - public void gameReport(GameReportEvent e) { - //noaction default - } + public void gameReport(GameReportEvent e) { } @Override - public void gameEnd(GameEndEvent e) { - //noaction default - } + public void gameEnd(GameEndEvent e) { } @Override - public void gameBoardNew(GameBoardNewEvent e) { - //noaction default - } + public void gameBoardNew(GameBoardNewEvent e) { } @Override - public void gameBoardChanged(GameBoardChangeEvent e) { - //noaction default - } + public void gameBoardChanged(GameBoardChangeEvent e) { } @Override - public void gameSettingsChange(GameSettingsChangeEvent e) { - //noaction default - } + public void gameSettingsChange(GameSettingsChangeEvent e) { } @Override - public void gameMapQuery(GameMapQueryEvent e) { - //noaction default - } + public void gameMapQuery(GameMapQueryEvent e) { } @Override - public void gameEntityNew(GameEntityNewEvent e) { - //noaction default - } + public void gameEntityNew(GameEntityNewEvent e) { } @Override - public void gameEntityNewOffboard(GameEntityNewOffboardEvent e) { - //noaction default - } + public void gameEntityNewOffboard(GameEntityNewOffboardEvent e) { } @Override - public void gameEntityRemove(GameEntityRemoveEvent e) { - //noaction default - } + public void gameEntityRemove(GameEntityRemoveEvent e) { } @Override - public void gameEntityChange(GameEntityChangeEvent e) { - //noaction default - } + public void gameEntityChange(GameEntityChangeEvent e) { } @Override - public void gameNewAction(GameNewActionEvent e) { - //noaction default - } + public void gameNewAction(GameNewActionEvent e) { } @Override - public void gameClientFeedbackRequest(GameCFREvent evt) { - //noaction default - } + public void gameClientFeedbackRequest(GameCFREvent evt) { } @Override - public void gameVictory(GameVictoryEvent e) { - //noaction default - } - - public void ready() { - } - // needed for turn timer to add timer display to GUI - public ClientGUI getClientgui() { - return clientgui; - } + public void gameVictory(GameVictoryEvent e) { } } diff --git a/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java index 5eb0e43bd72..39eb1e399c1 100644 --- a/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java @@ -46,7 +46,7 @@ protected ActionPhaseDisplay(ClientGUI cg) { protected UIUtil.FixedXPanel setupDonePanel() { var donePanel = super.setupDonePanel(); butSkipTurn = new MegamekButton("SKIP", SkinSpecification.UIComponents.PhaseDisplayDoneButton.getComp()); - butSkipTurn.setPreferredSize(new Dimension(UIUtil.scaleForGUI(DONE_BUTTON_WIDTH), MIN_BUTTON_SIZE.height * 1)); + butSkipTurn.setPreferredSize(new Dimension(UIUtil.scaleForGUI(DONE_BUTTON_WIDTH), MIN_BUTTON_SIZE.height)); String f = guiScaledFontHTML(UIUtil.uiLightViolet()) + KeyCommandBind.getDesc(KeyCommandBind.DONE_NO_ACTION)+ ""; butSkipTurn.setToolTipText("" + f + ""); addToDonePanel(donePanel, butSkipTurn); @@ -108,7 +108,7 @@ protected void initDonePanelForNewTurn() { } /** called to reset, show, hide and relabel the Done panel buttons. Override to change button labels and states, - * being sure to call {@link #updateDonePanelButtons(String,String,boolean) UpdateDonePanelButtons} + * being sure to call {@link #updateDonePanelButtons(String, String, boolean, List)} * to set the button labels and states */ abstract protected void updateDonePanel(); @@ -349,6 +349,6 @@ protected void updateDonePanelButtons(final String doneButtonLabel, final String } private void adaptToGUIScale() { - butSkipTurn.setPreferredSize(new Dimension(UIUtil.scaleForGUI(DONE_BUTTON_WIDTH), MIN_BUTTON_SIZE.height * 1)); + butSkipTurn.setPreferredSize(new Dimension(UIUtil.scaleForGUI(DONE_BUTTON_WIDTH), MIN_BUTTON_SIZE.height)); } } diff --git a/megamek/src/megamek/client/ui/swing/BoardViewsContainer.java b/megamek/src/megamek/client/ui/swing/BoardViewsContainer.java new file mode 100644 index 00000000000..d6b21ab5a31 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/BoardViewsContainer.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ +package megamek.client.ui.swing; + +import megamek.client.ui.swing.boardview.IBoardView; +import megamek.client.ui.swing.util.UIUtil; +import megamek.common.Board; + +import javax.swing.*; +import java.awt.*; +import java.util.Objects; + +/** + * The BoardViewsContainer manages the JPanel that contains the BoardView(s) of a ClientGUI. When only + * one BoardView is present, it is shown by itself. When multiple BoardViews are present, + * they are arranged as tabs of a TabbedPane. The panel that holds the BoardView(s) is obtained from + * {@link #getPanel()}. + *

The display contents are not automatically updated. Use {@link #updateMapTabs()} after construction + * and later to make it reflect the current set of BoardViews. + */ +public class BoardViewsContainer { + + /** The panel that displays the BoardView(s) */ + private final JPanel boardViewsContainer = new JPanel(new GridLayout(1, 1)); + + /** The tabbed pane is used when there are multiple boards to display */ + private final JTabbedPane mapTabPane = new JTabbedPane(); + + private final AbstractClientGUI clientGUI; + + /** + * Returns a new BoardViewsContainer. Call {@link #updateMapTabs()} after construction to make it + * reflect the current BoardViews. Requires a non-null AbstractClientGUI as parent. + * + * @param clientGUI The AbstractClientGUI parent + */ + public BoardViewsContainer(AbstractClientGUI clientGUI) { + this.clientGUI = Objects.requireNonNull(clientGUI); + } + + /** + * Returns the JPanel that holds the BoardView(s), either one BoardView by itself or multiple + * BoardViews in a tabbed pane. Add this panel to the view area of the ClientGUI. + * + * @return The panel holding all present BoardViews + */ + public Component getPanel() { + return boardViewsContainer; + } + + /** + * Updates the BoardViewsContainer to reflect the current state of ClientGUI's BoardViews. + */ + public void updateMapTabs() { + boardViewsContainer.removeAll(); + if (clientGUI.boardViews.size() > 1) { + arrangeMultipleBoardViews(); + } else if (clientGUI.boardViews.size() == 1) { + arrangeSingleBoardView(); + } + boardViewsContainer.validate(); + } + + private void arrangeMultipleBoardViews() { + mapTabPane.removeAll(); + for (int boardId : clientGUI.boardViews.keySet()) { + mapTabPane.add(board(boardId).getMapName(), boardView(boardId).getComponent()); + mapTabPane.setToolTipTextAt(mapTabPane.getTabCount() - 1, getBoardViewTabTooltip(boardId)); + } + boardViewsContainer.add(mapTabPane); + } + + private void arrangeSingleBoardView() { + // The single BoardView does not use the tabbed pane + int boardId = clientGUI.boardViews.keySet().iterator().next(); + boardViewsContainer.add(board(boardId).getMapName(), boardView(boardId).getComponent()); + } + + private String getBoardViewTabTooltip(int boardId) { + //TODO Add embedded and enclosing boards +// Game game = getIClient().getGame(); +// if (game.hasEnclosingBoard(boardId)) { +// Board enclosingBoard = game.getEnclosingBoard(boardView.getBoard()); +// tooltip += "
Located at " + enclosingBoard.embeddedBoardPosition(boardId).getBoardNum() + +// " in " + enclosingBoard; +// } + return "" + UIUtil.guiScaledFontHTML() + board(boardId).getMapName() + ""; + } + + private Board board(int id) { + return clientGUI.getClient().getGame().getBoard(id); + } + + private IBoardView boardView(int id) { + return clientGUI.boardViews.get(id); + } + + public void setName(String name) { + boardViewsContainer.setName(name); + } +} diff --git a/megamek/src/megamek/client/ui/swing/ClientGUI.java b/megamek/src/megamek/client/ui/swing/ClientGUI.java index 7539dcc8880..4c702770154 100644 --- a/megamek/src/megamek/client/ui/swing/ClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java @@ -23,7 +23,6 @@ import megamek.MMConstants; import megamek.client.AbstractClient; import megamek.client.Client; -import megamek.client.IClient; import megamek.client.TimerSingleton; import megamek.client.bot.BotClient; import megamek.client.bot.princess.BehaviorSettings; @@ -58,6 +57,7 @@ import megamek.common.actions.WeaponAttackAction; import megamek.common.annotations.Nullable; import megamek.common.enums.GamePhase; +import megamek.common.equipment.WeaponMounted; import megamek.common.event.*; import megamek.common.icons.Camouflage; import megamek.common.preference.IPreferenceChangeListener; @@ -83,8 +83,7 @@ import java.util.*; public class ClientGUI extends AbstractClientGUI implements BoardViewListener, - ActionListener, ComponentListener, IPreferenceChangeListener, MechDisplayListener { - + ActionListener, IPreferenceChangeListener, MechDisplayListener { // region Variable Declarations public static final int WEAPON_NONE = -1; @@ -93,6 +92,7 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener, // Note: anything located in menu bars is not located here but in their menu public static final String MAIN_SKIN_NEW = "mainSkinNew"; public static final String MAIN_QUIT = "mainQuit"; + // endregion // region file menu // game submenu public static final String FILE_GAME_NEW = "fileGameNew"; @@ -234,8 +234,8 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener, private SensorRangeSpriteHandler sensorRangeSpriteHandler; private CollapseWarningSpriteHandler collapseWarningSpriteHandler; private FiringSolutionSpriteHandler firingSolutionSpriteHandler; + private FiringArcSpriteHandler firingArcSpriteHandler; private final List spriteHandlers = new ArrayList<>(); - private Component bvc; private JPanel panTop; private JSplitPane splitPaneA; private JPanel panA1; @@ -301,7 +301,7 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener, */ private final JPanel panSecondary = new JPanel(); - private ReportDisplay reportDisply; + private ReportDisplay reportDisplay; private StatusBarPhaseDisplay currPhaseDisplay; @@ -350,17 +350,15 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener, * clean up after itself as much as possible, but will not call * System.exit(). */ - public ClientGUI(IClient client, MegaMekController c) { - if (!(client instanceof Client)) { - throw new IllegalArgumentException("TW ClientGUI must use TW Client!"); - } - this.client = (Client) client; + public ClientGUI(Client client, MegaMekController c) { + super(client); + this.client = client; controller = c; panMain.setLayout(cardsMain); panSecondary.setLayout(cardsSecondary); clientGuiPanel.setLayout(new BorderLayout()); - clientGuiPanel.addComponentListener(this); + clientGuiPanel.addComponentListener(resizeListener); clientGuiPanel.add(panMain, BorderLayout.CENTER); clientGuiPanel.add(panSecondary, BorderLayout.SOUTH); @@ -483,27 +481,39 @@ private void initializeSpriteHandlers() { sensorRangeSpriteHandler = new SensorRangeSpriteHandler(bv, client.getGame()); collapseWarningSpriteHandler = new CollapseWarningSpriteHandler(bv); firingSolutionSpriteHandler = new FiringSolutionSpriteHandler(bv, client); + firingArcSpriteHandler = new FiringArcSpriteHandler(bv, this); spriteHandlers.addAll(List.of(movementEnvelopeHandler, movementModifierSpriteHandler, sensorRangeSpriteHandler, flareSpritesHandler, collapseWarningSpriteHandler, - firingSolutionSpriteHandler)); + firingSolutionSpriteHandler, firingArcSpriteHandler)); spriteHandlers.forEach(BoardViewSpriteHandler::initialize); } @Override public void initialize() { + menuBar = CommonMenuBar.getMenuBarForGame(); + frame.setJMenuBar(menuBar); + initializeFrame(); super.initialize(); try { client.getGame().addGameListener(gameListener); bv = new BoardView(client.getGame(), controller, this); + boardViews.put(0, bv); bv.addOverlay(new KeyBindingsOverlay(bv)); bv.addOverlay(new PlanetaryConditionsOverlay(bv)); bv.addOverlay(new TurnDetailsOverlay(bv)); bv.getPanel().setPreferredSize(clientGuiPanel.getSize()); bv.setTooltipProvider(new TWBoardViewTooltip(client.getGame(), this, bv)); - bvc = bv.getComponent(); - bvc.setName(CG_BOARDVIEW); + cb2 = new ChatterBox2(this, bv, controller); + bv.addOverlay(cb2); + bv.getPanel().addKeyListener(cb2); + offBoardOverlay = new OffBoardTargetOverlay(this); + bv.addOverlay(new UnitOverview(this)); + bv.addOverlay(offBoardOverlay); + + boardViewsContainer.setName(CG_BOARDVIEW); + boardViewsContainer.updateMapTabs(); initializeSpriteHandlers(); panTop = new JPanel(new BorderLayout()); @@ -543,9 +553,6 @@ public void initialize() { aw.setLocation(0, 0); aw.setSize(300, 300); - bv.addOverlay(new UnitOverview(this)); - bv.addOverlay(offBoardOverlay); - setUnitDisplay(new UnitDisplay(this, controller)); getUnitDisplay().addMechDisplayListener(this); setUnitDisplayDialog(new UnitDisplayDialog(getFrame(), this)); @@ -682,16 +689,16 @@ public void miniReportDisplayAddReportPages() { } public void reportDisplayResetDone() { - if ((reportDisply != null) && (!getClient().getLocalPlayer().isDone())) { - reportDisply.setDoneEnabled(true); + if ((reportDisplay != null) && (!getClient().getLocalPlayer().isDone())) { + reportDisplay.setDoneEnabled(true); } } public void reportDisplayResetRerollInitiative() { - if ((reportDisply != null) + if ((reportDisplay != null) && (!getClient().getLocalPlayer().isDone()) && (getClient().getGame().hasTacticalGenius(getClient().getLocalPlayer()))) { - reportDisply.resetRerollInitiativeEnabled(); + reportDisplay.resetRerollInitiativeEnabled(); } } @@ -1359,17 +1366,17 @@ private JComponent initializePanel(GamePhase phase) { case VICTORY: main = CG_BOARDVIEW; secondary = CG_REPORTDISPLAY; - if (reportDisply == null) { - reportDisply = new ReportDisplay(this); - reportDisply.setName(secondary); + if (reportDisplay == null) { + reportDisplay = new ReportDisplay(this); + reportDisplay.setName(secondary); } if (!mainNames.containsValue(main)) { panMain.add(panTop, main); } - currPhaseDisplay = reportDisply; - component = reportDisply; + currPhaseDisplay = reportDisplay; + component = reportDisplay; if (!secondaryNames.containsValue(secondary)) { - panSecondary.add(reportDisply, secondary); + panSecondary.add(reportDisplay, secondary); } break; default: @@ -1645,7 +1652,7 @@ public void setUnitDisplayLocation(boolean visible) { if (GUIP.getDockOnLeft()) { switch (GUIP.getUnitDisplayLocaton()) { case 0: - panA2.add(bvc); + panA2.add(boardViewsContainer.getPanel()); panA2.setVisible(true); getUnitDisplayDialog().add(getUnitDisplay(), BorderLayout.CENTER); getUnitDisplayDialog().setVisible(visible); @@ -1654,7 +1661,7 @@ public void setUnitDisplayLocation(boolean visible) { hideEmptyPanel(panA1, splitPaneA, 0.0); break; case 1: - panA2.add(bvc); + panA2.add(boardViewsContainer.getPanel()); panA2.setVisible(true); panA1.add(getUnitDisplay()); getUnitDisplayDialog().setVisible(false); @@ -1666,7 +1673,7 @@ public void setUnitDisplayLocation(boolean visible) { } else { switch (GUIP.getUnitDisplayLocaton()) { case 0: - panA1.add(bvc); + panA1.add(boardViewsContainer.getPanel()); panA1.setVisible(true); getUnitDisplayDialog().add(getUnitDisplay(), BorderLayout.CENTER); getUnitDisplayDialog().setVisible(visible); @@ -1675,7 +1682,7 @@ public void setUnitDisplayLocation(boolean visible) { hideEmptyPanel(panA2, splitPaneA, 1.0); break; case 1: - panA1.add(bvc); + panA1.add(boardViewsContainer.getPanel()); panA1.setVisible(true); panA2.add(getUnitDisplay()); getUnitDisplayDialog().setVisible(false); @@ -1697,7 +1704,7 @@ public void setMiniReportLocation(boolean visible) { if (GUIP.getDockOnLeft()) { switch (GUIP.getMiniReportLocaton()) { case 0: - panA2.add(bvc); + panA2.add(boardViewsContainer.getPanel()); panA2.setVisible(true); getMiniReportDisplayDialog().add(getMiniReportDisplay(), BorderLayout.CENTER); getMiniReportDisplayDialog().setVisible(visible); @@ -1705,7 +1712,7 @@ public void setMiniReportLocation(boolean visible) { hideEmptyPanel(panA1, splitPaneA, 0.0); break; case 1: - panA2.add(bvc); + panA2.add(boardViewsContainer.getPanel()); panA2.setVisible(true); panA1.add(getMiniReportDisplay()); getMiniReportDisplayDialog().setVisible(false); @@ -1716,7 +1723,7 @@ public void setMiniReportLocation(boolean visible) { } else { switch (GUIP.getMiniReportLocaton()) { case 0: - panA1.add(bvc); + panA1.add(boardViewsContainer.getPanel()); panA1.setVisible(true); getMiniReportDisplayDialog().add(getMiniReportDisplay(), BorderLayout.CENTER); getMiniReportDisplayDialog().setVisible(visible); @@ -1724,7 +1731,7 @@ public void setMiniReportLocation(boolean visible) { hideEmptyPanel(panA2, splitPaneA, 1.0); break; case 1: - panA1.add(bvc); + panA1.add(boardViewsContainer.getPanel()); panA1.setVisible(true); panA2.add(getMiniReportDisplay()); getMiniReportDisplayDialog().setVisible(false); @@ -2227,7 +2234,7 @@ public void gameReport(GameReportEvent e) { @Override public void gameEnd(GameEndEvent e) { bv.clearMovementData(); - bv.clearFieldOfFire(); + clearFieldOfFire(); clearTemporarySprites(); getLocalBots().values().forEach(AbstractClient::die); getLocalBots().clear(); @@ -2530,6 +2537,11 @@ public Client getClient() { return client; } + @Override + public JComponent turnTimerComponent() { + return menuBar; + } + public Map getLocalBots() { return client.getBots(); } @@ -2723,25 +2735,12 @@ public boolean shouldIgnoreHotKeys() { || ((aw != null) && aw.isVisible()); } - @Override - public void componentHidden(ComponentEvent evt) { - - } - - @Override - public void componentMoved(ComponentEvent evt) { - - } - - @Override - public void componentResized(ComponentEvent evt) { - bv.getPanel().setPreferredSize(clientGuiPanel.getSize()); - } - - @Override - public void componentShown(ComponentEvent evt) { - - } + private final ComponentListener resizeListener = new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent evt) { + boardViewsContainer.getPanel().setPreferredSize(clientGuiPanel.getSize()); + } + }; void editBots() { var rpd = new EditBotsDialog(frame, this); @@ -2860,6 +2859,7 @@ public void clearTemporarySprites() { sensorRangeSpriteHandler.clear(); collapseWarningSpriteHandler.clear(); firingSolutionSpriteHandler.clear(); + firingArcSpriteHandler.clear(); } /** @@ -2913,6 +2913,7 @@ public void showFiringSolutions(Entity entity) { public void weaponSelected(MechDisplayEvent b) { setSelectedEntityNum(b.getEntityId()); setSelectedWeapon(b.getWeaponId()); + firingArcSpriteHandler.updateSelectedWeapon(b.getEntity(), getSelectedWeapon()); } private void setSelectedWeapon(int equipmentNumber) { @@ -2928,8 +2929,8 @@ public int getSelectedWeaponId() { } @Nullable - public Mounted getSelectedWeapon() { - return hasSelectedWeapon() ? getSelectedUnit().getEquipment(selectedWeapon) : null; + public WeaponMounted getSelectedWeapon() { + return hasSelectedWeapon() ? (WeaponMounted) getSelectedUnit().getEquipment(selectedWeapon) : null; } @Nullable @@ -2945,4 +2946,55 @@ public boolean hasSelectedWeapon() { public JPanel getMainPanel() { return clientGuiPanel; } + + /** + * Updates the position used for showing the field of fire to the end point of the given movepath. This + * method does nothing if the given entity is not the one viewed in the unit viewer (and therefore + * not the one for which a weapon field of fire is currently shown). + * + * @param entity The unit to take the facing from if it is the currently viewed unit + * @param movePath A planned movement path to take the end position from + */ + public void setFiringArcPosition(Entity entity, MovePath movePath) { + // Only update when the selected weapon is on the unit that's moving + if (entity.getId() == getSelectedEntityNum()) { + firingArcSpriteHandler.updatePosition(movePath); + } + } + + /** + * Updates the position used for showing the field of fire to the given position. This + * method does nothing if the given entity is not the one viewed in the unit viewer (and therefore + * not the one for which a weapon field of fire is currently shown). + * + * @param entity The unit to take the facing from if it is the currently viewed unit + * @param coords The position to center the field of fire on + */ + public void setFiringArcPosition(Entity entity, Coords coords) { + // Only update when the selected weapon is on the unit that's changing position + if (entity.getId() == getSelectedEntityNum()) { + firingArcSpriteHandler.updatePosition(coords); + } + } + + /** + * Updates the facing used for showing the field of fire to the basic facing of the given unit. This + * method does nothing if the given entity is not the one viewed in the unit viewer (and therefore + * not the one for which a weapon field of fire is currently shown). + * + * @param entity The unit to take the facing from if it is the currently viewed unit + */ + public void setFiringArcFacing(Entity entity) { + // Only update when the selected weapon is on the unit that's changing its facing + if (entity.getId() == getSelectedEntityNum()) { + firingArcSpriteHandler.updateFacing(entity.getFacing()); + } + } + + /** + * Removes the field of fire from the BoardView and clears the cached values in the sprite handler. + */ + public void clearFieldOfFire() { + firingArcSpriteHandler.clearValues(); + } } diff --git a/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java b/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java index 53274612f28..2b8b8e4e1c3 100644 --- a/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java +++ b/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java @@ -226,13 +226,13 @@ public void selectEntity(int en) { clientgui.getUnitDisplay().displayEntity(ce()); clientgui.getUnitDisplay().showPanel("movement"); - clientgui.getBoardView().setWeaponFieldOfFire(ce().getFacing(), ce().getPosition()); + clientgui.setFiringArcPosition(ce(), ce().getPosition()); clientgui.showSensorRanges(ce()); computeCFWarningHexes(ce()); } else { disableButtons(); setNextEnabled(true); - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); } } @@ -515,7 +515,7 @@ public void hexMoused(BoardViewEvent b) { ce().setFacing(ce().getPosition().direction(moveto)); ce().setSecondaryFacing(ce().getFacing()); clientgui.getBoardView().redrawEntity(ce()); - clientgui.getBoardView().setWeaponFieldOfFire(ce().getFacing(), ce().getPosition()); + clientgui.setFiringArcFacing(ce()); clientgui.showSensorRanges(ce()); turnMode = false; } else if (ce().isBoardProhibited(board.getType())) { @@ -581,7 +581,7 @@ public void hexMoused(BoardViewEvent b) { ce().setPosition(moveto); clientgui.getBoardView().redrawEntity(ce()); - clientgui.getBoardView().setWeaponFieldOfFire(ce().getFacing(), moveto); + clientgui.setFiringArcPosition(ce(), moveto); clientgui.showSensorRanges(ce()); clientgui.getBoardView().getPanel().repaint(); butDone.setEnabled(true); @@ -871,7 +871,7 @@ public void unitSelected(BoardViewEvent b) { if (null == e) { return; } - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); if (client.isMyTurn()) { if (client.getGame().getTurn().isValidEntity(e, client.getGame())) { diff --git a/megamek/src/megamek/client/ui/swing/FiringDisplay.java b/megamek/src/megamek/client/ui/swing/FiringDisplay.java index de0dc051322..dd27a421795 100644 --- a/megamek/src/megamek/client/ui/swing/FiringDisplay.java +++ b/megamek/src/megamek/client/ui/swing/FiringDisplay.java @@ -469,7 +469,7 @@ protected void beginMyTurn() { if (!clientgui.getBoardView().isMovingUnits()) { clientgui.maybeShowUnitDisplay(); } - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); if (GUIP.getAutoSelectNextUnit()) { @@ -534,7 +534,7 @@ protected void endMyTurn() { clientgui.getBoardView().cursor(null); clientgui.getBoardView().clearMovementData(); clientgui.getBoardView().clearStrafingCoords(); - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); clientgui.setSelectedEntityNum(Entity.NONE); disableButtons(); diff --git a/megamek/src/megamek/client/ui/swing/IClientGUI.java b/megamek/src/megamek/client/ui/swing/IClientGUI.java index 914f8d40dee..61ca5b53eed 100644 --- a/megamek/src/megamek/client/ui/swing/IClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/IClientGUI.java @@ -18,6 +18,7 @@ */ package megamek.client.ui.swing; +import megamek.client.IClient; import megamek.common.InGameObject; import javax.swing.*; @@ -49,4 +50,8 @@ public interface IClientGUI { void die(); InGameObject getSelectedUnit(); + + IClient getClient(); + + JComponent turnTimerComponent(); } diff --git a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java index 1d6c0a6965f..e1dd9c52362 100644 --- a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java +++ b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java @@ -46,7 +46,6 @@ import megamek.codeUtilities.StringUtility; import megamek.common.*; import megamek.common.annotations.Nullable; -import megamek.common.enums.GamePhase; import megamek.common.options.IBasicOption; import megamek.common.options.IOption; import megamek.common.preference.IPreferenceChangeListener; @@ -99,7 +98,7 @@ public class MegaMekGUI implements IPreferenceChangeListener { private IGameManager gameManager; private CommonSettingsDialog settingsDialog; - private MegaMekController controller; + private static MegaMekController controller; public void start(boolean show) { createGUI(show); @@ -185,10 +184,13 @@ public void createController() { controller = new MegaMekController(); KeyboardFocusManager kbfm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); kbfm.addKeyEventDispatcher(controller); - KeyBindParser.parseKeyBindings(controller); } + public static MegaMekController getKeyDispatcher() { + return controller; + } + /** * Display the main menu. */ @@ -491,9 +493,9 @@ private IClientGUI getClientGUI(GameType gameType, IClient client, MegaMekContro return new BFGameManager(); */ case SBF: - return new SBFClientGUI(client, controller); + return new SBFClientGUI((SBFClient) client, controller); default: - return new ClientGUI(client, controller); + return new ClientGUI((Client) client, controller); } } @@ -909,8 +911,8 @@ void connectBot() { } client = Princess.createPrincess(bcd.getBotName(), cd.getServerAddress(), cd.getPort(), bcd.getBehaviorSettings()); - client.getIGame().addGameListener(new BotGUI(frame, (BotClient) client)); - ClientGUI gui = new ClientGUI(client, controller); + client.getGame().addGameListener(new BotGUI(frame, (BotClient) client)); + ClientGUI gui = new ClientGUI((Client) client, controller); controller.clientgui = gui; gui.initialize(); if (!client.connect()) { diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index 5a8e95d5988..4ca3b147cca 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -351,7 +351,6 @@ public static MoveCommand[] values(int f, GameOptions opts, boolean forwardIni) public MovementDisplay(final ClientGUI clientgui) { super(clientgui); - this.clientgui = clientgui; if (clientgui != null) { clientgui.getClient().getGame().addGameListener(this); clientgui.getBoardView().addBoardViewListener(this); @@ -442,7 +441,11 @@ private void computeEnvelope() { private void cancel() { clear(); - computeMovementEnvelope(ce()); + Entity currentEntity = ce(); + if (currentEntity != null) { + computeMovementEnvelope(currentEntity); + clientgui.setFiringArcPosition(currentEntity, currentEntity.getPosition()); + } updateMove(); } @@ -669,7 +672,7 @@ public synchronized void selectEntity(int en) { } else { setStatusBarText(yourTurnMsg); } - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); computeMovementEnvelope(ce); updateMove(); computeCFWarningHexes(ce); @@ -1071,7 +1074,7 @@ private void beginMyTurn() { initDonePanelForNewTurn(); setNextEnabled(true); setForwardIniEnabled(true); - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); if (numButtonGroups > 1) { getBtn(MoveCommand.MOVE_MORE).setEnabled(true); @@ -1115,7 +1118,7 @@ private synchronized void endMyTurn() { clientgui.getBoardView().selectEntity(null); clientgui.setSelectedEntityNum(Entity.NONE); clientgui.getBoardView().clearMovementData(); - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); } @@ -1203,7 +1206,6 @@ public void clear() { // clear board cursors clientgui.getBoardView().select(null); clientgui.getBoardView().cursor(null); - clientgui.clearTemporarySprites(); if (ce == null) { return; @@ -1224,7 +1226,7 @@ public void clear() { // create new current and considered paths cmd = new MovePath(clientgui.getClient().getGame(), ce); - clientgui.getBoardView().setWeaponFieldOfFire(ce, cmd); + clientgui.setFiringArcPosition(ce, cmd); clientgui.showSensorRanges(ce, cmd.getFinalCoords()); computeCFWarningHexes(ce); @@ -1312,7 +1314,7 @@ private void removeLastStep() { clientgui.getBoardView().select(cmd.getFinalCoords()); clientgui.getBoardView().cursor(cmd.getFinalCoords()); clientgui.getBoardView().drawMovementData(entity, cmd); - clientgui.getBoardView().setWeaponFieldOfFire(entity, cmd); + clientgui.setFiringArcPosition(entity, cmd); clientgui.showSensorRanges(entity, cmd.getFinalCoords()); //FIXME what is this @@ -1825,7 +1827,7 @@ private void currentMove(Coords dest) { } clientgui.showSensorRanges(ce(), cmd.getFinalCoords()); - clientgui.getBoardView().setWeaponFieldOfFire(ce(), cmd); + clientgui.setFiringArcPosition(ce(), cmd); } // @@ -2725,7 +2727,7 @@ private void updateBraceButton() { MovePath movePath = cmd; if (null == movePath) { - movePath = new MovePath(this.getClientgui().getClient().getGame(), ce()); + movePath = new MovePath(clientgui.getClient().getGame(), ce()); } if (!movePath.contains(MoveStepType.BRACE) && @@ -4355,7 +4357,6 @@ public void gamePhaseChange(GamePhaseChangeEvent e) { public void computeMovementEnvelope(Entity suggestion) { // do nothing if deactivated in the settings if (!GUIP.getMoveEnvelope()) { - clientgui.clearTemporarySprites(); return; } @@ -5711,17 +5712,6 @@ private void updateTurnButton() { && !(cmd.isJumping() && (ce instanceof Mech) && (ce.getJumpType() == Mech.JUMP_BOOSTER))); } - @Override - public void setWeaponFieldOfFire(Entity unit, int[][] ranges, int arc, int loc) { - // If the unit is the current unit, then work with - // the current considered movement - if (unit.equals(ce())) { - super.setWeaponFieldOfFire(unit, ranges, arc, loc, cmd); - } else { - super.setWeaponFieldOfFire(unit, ranges, arc, loc); - } - } - /** Shortcut to clientgui.getClient().getGame(). */ private Game game() { return clientgui.getClient().getGame(); diff --git a/megamek/src/megamek/client/ui/swing/PointblankShotDisplay.java b/megamek/src/megamek/client/ui/swing/PointblankShotDisplay.java index d07ddd38715..7776095abb9 100644 --- a/megamek/src/megamek/client/ui/swing/PointblankShotDisplay.java +++ b/megamek/src/megamek/client/ui/swing/PointblankShotDisplay.java @@ -310,7 +310,7 @@ public void selectEntity(int en) { @Override public void beginMyTurn() { clientgui.maybeShowUnitDisplay(); - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); butDone.setEnabled(true); @@ -341,7 +341,7 @@ protected void endMyTurn() { clientgui.getBoardView().cursor(null); clientgui.getBoardView().clearMovementData(); clientgui.getBoardView().clearStrafingCoords(); - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); clientgui.setSelectedEntityNum(Entity.NONE); disableButtons(); diff --git a/megamek/src/megamek/client/ui/swing/PrephaseDisplay.java b/megamek/src/megamek/client/ui/swing/PrephaseDisplay.java index df51a3a2010..38165cd4d9b 100644 --- a/megamek/src/megamek/client/ui/swing/PrephaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/PrephaseDisplay.java @@ -288,7 +288,7 @@ private void beginMyTurn() { setStatusBarText(Messages.getFormattedString("PrephaseDisplay.its_your_turn", phase.toString(), "")); butDone.setText("" + Messages.getString("PrephaseDisplay.Done") + ""); - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); if (GUIP.getAutoSelectNextUnit()) { @@ -313,7 +313,7 @@ private void endMyTurn() { clientgui.getBoardView().highlight(null); clientgui.getBoardView().cursor(null); clientgui.getBoardView().clearMovementData(); - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); clientgui.setSelectedEntityNum(Entity.NONE); diff --git a/megamek/src/megamek/client/ui/swing/ReportDisplay.java b/megamek/src/megamek/client/ui/swing/ReportDisplay.java index 33ec8eb3541..28af3e4628c 100644 --- a/megamek/src/megamek/client/ui/swing/ReportDisplay.java +++ b/megamek/src/megamek/client/ui/swing/ReportDisplay.java @@ -86,12 +86,15 @@ public String getHotKeyDesc() { private static final String RD_REPORTDISPLAY = "ReportDisplay."; private static final String RD_TOOLTIP = ".tooltip"; + private final ClientGUI clientgui; + /** * Creates and lays out a new movement phase display for the specified * clientgui.getClient(). */ public ReportDisplay(ClientGUI clientgui) { super(clientgui); + this.clientgui = clientgui; if (clientgui == null) { return; diff --git a/megamek/src/megamek/client/ui/swing/SBFClientGUI.java b/megamek/src/megamek/client/ui/swing/SBFClientGUI.java index 878bedf1326..6075c80d017 100644 --- a/megamek/src/megamek/client/ui/swing/SBFClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/SBFClientGUI.java @@ -18,25 +18,129 @@ */ package megamek.client.ui.swing; -import megamek.client.Client; -import megamek.client.IClient; import megamek.client.SBFClient; import megamek.client.ui.swing.util.MegaMekController; import megamek.common.InGameObject; +import megamek.common.enums.GamePhase; +import megamek.common.event.GameListener; +import megamek.common.util.Distractable; +import javax.swing.*; import java.awt.*; +import java.awt.event.*; +import java.util.HashMap; +import java.util.Map; -public class SBFClientGUI extends AbstractClientGUI { +public class SBFClientGUI extends AbstractClientGUI implements ActionListener { + + public static final String CG_BOARDVIEW = "BoardView"; + public static final String CG_CHATLOUNGE = "ChatLounge"; + public static final String CG_STARTINGSCENARIO = "JLabel-StartingScenario"; + public static final String CG_EXCHANGE = "JLabel-Exchange"; + public static final String CG_SELECTARTYAUTOHITHEXDISPLAY = "SelectArtyAutoHitHexDisplay"; + public static final String CG_DEPLOYMINEFIELDDISPLAY = "DeployMinefieldDisplay"; + public static final String CG_DEPLOYMENTDISPLAY = "DeploymentDisplay"; + public static final String CG_TARGETINGPHASEDISPLAY = "TargetingPhaseDisplay"; + public static final String CG_PREMOVEMENTDISPLAY = "PremovementDisplay"; + public static final String CG_MOVEMENTDISPLAY = "MovementDisplay"; + public static final String CG_OFFBOARDDISPLAY = "OffboardDisplay"; + public static final String CG_PREFIRING = "Prefiring"; + public static final String CG_FIRINGDISPLAY = "FiringDisplay"; + public static final String CG_POINTBLANKSHOTDISPLAY = "PointblankShotDisplay"; + public static final String CG_PHYSICALDISPLAY = "PhysicalDisplay"; + public static final String CG_REPORTDISPLAY = "ReportDisplay"; + public static final String CG_DEFAULT = "JLabel-Default"; private final SBFClient client; - public SBFClientGUI(IClient client, MegaMekController c) { - if (!(client instanceof SBFClient)) { - throw new IllegalArgumentException("SBF ClientGUI must use SBF Client!"); - } - this.client = (SBFClient) client; + // a frame, to show stuff in + private final JPanel clientGuiPanel = new JPanel(); + + protected JComponent curPanel; + private final JPanel panTop = new JPanel(new BorderLayout()); + + /** + * The JPanel containing the main display area. + */ + private final JPanel panMain = new JPanel(); + + /** + * The CardLayout of the main display area. + */ + private final CardLayout cardsMain = new CardLayout(); + + /** + * Map each phase to the name of the card for the secondary area. + */ + private final Map secondaryNames = new HashMap<>(); + + /** + * The JPanel containing the secondary display area. + */ + private final JPanel panSecondary = new JPanel(); + + + private ReportDisplay reportDisplay; + + private StatusBarPhaseDisplay currPhaseDisplay; + + /** + * The CardLayout of the secondary display area. + */ + private final CardLayout cardsSecondary = new CardLayout(); + + /** + * Map phase component names to phase component objects. + */ + private final Map phaseComponents = new HashMap<>(); + + /** + * Map each phase to the name of the card for the main display area. + */ + private final Map mainNames = new HashMap<>(); + + private final GameListener gameListener = new SBFClientGUIGameListener(this); + private final CommonMenuBar menuBar = CommonMenuBar.getMenuBarForGame(); + + public SBFClientGUI(SBFClient client, MegaMekController c) { + super(client); + this.client = client; frame.getContentPane().setLayout(new BorderLayout()); - frame.getContentPane().add(new UnderConstructionPanel(), BorderLayout.CENTER); + frame.getContentPane().add(clientGuiPanel, BorderLayout.CENTER); + frame.setJMenuBar(menuBar); + menuBar.addActionListener(this); + panMain.setLayout(cardsMain); + panSecondary.setLayout(cardsSecondary); + + panMain.add("UnderConstruction", new UnderConstructionPanel()); + + clientGuiPanel.setLayout(new BorderLayout()); + clientGuiPanel.addComponentListener(resizeListener); + clientGuiPanel.add(panMain, BorderLayout.CENTER); + clientGuiPanel.add(panSecondary, BorderLayout.SOUTH); + } + + private final ComponentListener resizeListener = new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent evt) { + boardViewsContainer.getPanel().setPreferredSize(clientGuiPanel.getSize()); + } + }; + + @Override + public SBFClient getClient() { + return client; + } + + @Override + public JComponent turnTimerComponent() { + return menuBar; + } + + @Override + public void initialize() { + super.initialize(); + client.getGame().addGameListener(gameListener); frame.setVisible(true); } @@ -46,8 +150,230 @@ protected boolean saveGame() { return true; } + @Override + public void die() { + super.die(); + client.getGame().removeGameListener(gameListener); + } + @Override public InGameObject getSelectedUnit() { return null; } + + protected void switchPanel(GamePhase phase) { + // Clear the old panel's listeners. +// if (curPanel instanceof BoardViewListener) { +// bv.removeBoardViewListener((BoardViewListener) curPanel); +// } + + if (curPanel instanceof ActionListener) { + menuBar.removeActionListener((ActionListener) curPanel); + } + + if (curPanel instanceof Distractable) { + ((Distractable) curPanel).setIgnoringEvents(true); + } + + // Get the new panel. + String name = String.valueOf(phase); + curPanel = phaseComponents.get(name); + if (curPanel == null) { + curPanel = initializePanel(phase); + } + + // Handle phase-specific items. + switch (phase) { + case LOUNGE: +// // reset old report tabs and images, if any +// ChatLounge cl = (ChatLounge) phaseComponents.get(String.valueOf(GamePhase.LOUNGE)); +// cb.setDoneButton(cl.butDone); +// cl.setBottom(cb.getComponent()); +// getBoardView().getTilesetManager().reset(); + break; + default: + break; + } + +// maybeShowMinimap(); +// maybeShowUnitDisplay(); +// maybeShowForceDisplay(); +// maybeShowMiniReport(); +// maybeShowPlayerList(); + + cardsMain.show(panMain, mainNames.get(name)); + String secondaryToShow = secondaryNames.get(name); + // only show the secondary component if there is one to show + if (secondaryToShow != null) { + panSecondary.setVisible(true); + cardsSecondary.show(panSecondary, secondaryNames.get(name)); + } else { + // otherwise, hide the panel + panSecondary.setVisible(false); + } + + // Set the new panel's listeners +// if (curPanel instanceof BoardViewListener) { +// bv.addBoardViewListener((BoardViewListener) curPanel); +// } + + if (curPanel instanceof ActionListener) { + menuBar.addActionListener((ActionListener) curPanel); + } + + if (curPanel instanceof Distractable) { + ((Distractable) curPanel).setIgnoringEvents(false); + } + + // Make the new panel the focus, if the Client option says so +// if (GUIP.getFocus() && !(client instanceof BotClient)) { +// curPanel.requestFocus(); +// } + } + + private void initializeSingleComponent(GamePhase phase, JComponent component, String identifier) { + component.setName(identifier); + panMain.add(component, identifier); + String phaseName = String.valueOf(phase); + phaseComponents.put(phaseName, component); + mainNames.put(phaseName, identifier); + } + + private void initializeWithBoardView(GamePhase phase, StatusBarPhaseDisplay component, String secondary) { + String identifier = CG_BOARDVIEW; + String phaseName = String.valueOf(phase); + component.setName(identifier); + panMain.add(component, identifier); + if (!mainNames.containsValue(identifier)) { + panMain.add(panTop, identifier); + } + currPhaseDisplay = component; + panSecondary.add(component, secondary); + phaseComponents.put(phaseName, component); + mainNames.put(phaseName, identifier); + if (secondary != null) { + secondaryNames.put(phaseName, secondary); + } + } + + private JComponent initializePanel(GamePhase phase) { + // Create the components for this phase. + JComponent component = new ReceivingGameDataPanel(); + String secondary; + String main = CG_BOARDVIEW; + switch (phase) { + case LOUNGE: +// initializeSingleComponent(phase, new ChatLounge(this), CG_CHATLOUNGE); + break; + case STARTING_SCENARIO: + initializeSingleComponent(phase, new StartingScenarioPanel(), CG_STARTINGSCENARIO); + break; + case EXCHANGE: + initializeSingleComponent(phase, new ReceivingGameDataPanel(), CG_EXCHANGE); + break; + case SET_ARTILLERY_AUTOHIT_HEXES: +// initializeWithBoardView(phase, new SelectArtyAutoHitHexDisplay(this), CG_SELECTARTYAUTOHITHEXDISPLAY); + break; + case DEPLOY_MINEFIELDS: +// initializeWithBoardView(phase, new DeployMinefieldDisplay(this), CG_DEPLOYMINEFIELDDISPLAY); + break; + case DEPLOYMENT: +// initializeWithBoardView(phase, new DeploymentDisplay(this), CG_DEPLOYMINEFIELDDISPLAY); + break; + case TARGETING: +// initializeWithBoardView(phase, new TargetingPhaseDisplay(this, false), CG_DEPLOYMINEFIELDDISPLAY); +// component = new TargetingPhaseDisplay(this, false); +// ((TargetingPhaseDisplay) component).initializeListeners(); + secondary = CG_TARGETINGPHASEDISPLAY; + component.setName(secondary); + if (!mainNames.containsValue(main)) { + panMain.add(panTop, main); + } + currPhaseDisplay = (StatusBarPhaseDisplay) component; + panSecondary.add(component, secondary); +// offBoardOverlay.setTargetingPhaseDisplay((TargetingPhaseDisplay) component); + break; + case PREMOVEMENT: +// component = new PrephaseDisplay(this, GamePhase.PREMOVEMENT); +// ((PrephaseDisplay) component).initializeListeners(); + secondary = CG_PREMOVEMENTDISPLAY; + component.setName(secondary); + if (!mainNames.containsValue(main)) { + panMain.add(panTop, main); + } + currPhaseDisplay = (StatusBarPhaseDisplay) component; + panSecondary.add(component, secondary); + break; + case MOVEMENT: +// initializeWithBoardView(phase, new MovementDisplay(this), CG_MOVEMENTDISPLAY); + break; + case OFFBOARD: +// component = new TargetingPhaseDisplay(this, true); +// ((TargetingPhaseDisplay) component).initializeListeners(); + secondary = CG_OFFBOARDDISPLAY; + component.setName(secondary); + if (!mainNames.containsValue(main)) { + panMain.add(panTop, main); + } + currPhaseDisplay = (StatusBarPhaseDisplay) component; + panSecondary.add(component, secondary); + break; + case PREFIRING: +// component = new PrephaseDisplay(this, GamePhase.PREFIRING); +// ((PrephaseDisplay) component).initializeListeners(); + secondary = CG_PREFIRING; + component.setName(secondary); + if (!mainNames.containsValue(main)) { + panMain.add(panTop, main); + } + currPhaseDisplay = (StatusBarPhaseDisplay) component; + panSecondary.add(component, secondary); + break; + case FIRING: +// initializeWithBoardView(phase, new FiringDisplay(this), CG_FIRINGDISPLAY); + break; + case POINTBLANK_SHOT: +// initializeWithBoardView(phase, new PointblankShotDisplay(this), CG_POINTBLANKSHOTDISPLAY); + break; + case PHYSICAL: +// initializeWithBoardView(phase, new PhysicalDisplay(this), CG_PHYSICALDISPLAY); + break; + case INITIATIVE_REPORT: + case TARGETING_REPORT: + case MOVEMENT_REPORT: + case OFFBOARD_REPORT: + case FIRING_REPORT: + case PHYSICAL_REPORT: + case END_REPORT: + case VICTORY: +// initializeWithBoardView(phase, new JPanel(), CG_PHYSICALDISPLAY); + secondary = CG_REPORTDISPLAY; + if (reportDisplay == null) { +// reportDisply = new JPanel(); +// reportDisply = new ReportDisplay(this); +// reportDisply.setName(secondary); + } + if (!mainNames.containsValue(main)) { + panMain.add(panTop, main); + } + currPhaseDisplay = reportDisplay; + component = reportDisplay; +// if (!secondaryNames.containsValue(secondary)) { +// panSecondary.add(reportDisply, secondary); +// } + break; + default: + component = new WaitingForServerPanel(); + main = CG_DEFAULT; + component.setName(main); + panMain.add(main, component); + break; + } + return component; + } + + @Override + public void actionPerformed(ActionEvent e) { + + } } diff --git a/megamek/src/megamek/client/ui/swing/SBFClientGUIGameListener.java b/megamek/src/megamek/client/ui/swing/SBFClientGUIGameListener.java new file mode 100644 index 00000000000..7cda42c911e --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/SBFClientGUIGameListener.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ + +package megamek.client.ui.swing; + +import megamek.common.event.*; + +public class SBFClientGUIGameListener extends GameListenerAdapter { + + private final SBFClientGUI clientGUI; + + public SBFClientGUIGameListener(SBFClientGUI clientGUI) { + this.clientGUI = clientGUI; + } + + @Override + public void gamePhaseChange(GamePhaseChangeEvent e) { +// // This is a really lame place for this, but I couldn't find a +// // better one without making massive changes (which didn't seem +// // worth it for one little feature). +// if (bv.getLocalPlayer() != client.getLocalPlayer()) { +// // The adress based comparison is somewhat important. +// // Use of the /reset command can cause the player to get reset, +// // and the equals function of Player isn't powerful enough. +// bv.setLocalPlayer(client.getLocalPlayer()); +// } +// // Make sure the ChatterBox starts out deactived. +// bv.setChatterBoxActive(false); +// + // Swap to this phase's panel. + clientGUI.switchPanel(e.getNewPhase()); +// clientGUI.menuBar.setPhase(phase); +// clientGUI.cb.moveToEnd(); + } +} diff --git a/megamek/src/megamek/client/ui/swing/StartingScenarioPanel.java b/megamek/src/megamek/client/ui/swing/StartingScenarioPanel.java new file mode 100644 index 00000000000..dc557d460fb --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/StartingScenarioPanel.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ + +package megamek.client.ui.swing; + +import megamek.client.ui.swing.util.FontHandler; +import megamek.client.ui.swing.util.UIUtil; + +import javax.swing.*; +import java.awt.*; + +public class StartingScenarioPanel extends JPanel { + + private static final String text = "Starting Scenario"; + private static final String sign = "\uf50e"; + + /** + * Returns a panel that shows the centered notice saying "Under Construction" with a warning sign above. + */ + public StartingScenarioPanel() { + JPanel textPanel = new UIUtil.FixedYPanel(new FlowLayout(FlowLayout.CENTER)); + textPanel.add(new JLabel(text)); + + JPanel symbolPanel = new UIUtil.FixedYPanel(new FlowLayout(FlowLayout.CENTER)); + JLabel symbolLabel = new JLabel(sign); + symbolLabel.setFont(FontHandler.symbolFont()); + symbolPanel.add(symbolLabel); + + setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); + add(Box.createVerticalGlue()); + add(symbolPanel); + add(textPanel); + add(Box.createVerticalGlue()); + } +} diff --git a/megamek/src/megamek/client/ui/swing/StatusBarPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/StatusBarPhaseDisplay.java index 8f79802398d..3fad3c5e305 100644 --- a/megamek/src/megamek/client/ui/swing/StatusBarPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/StatusBarPhaseDisplay.java @@ -54,6 +54,7 @@ public abstract class StatusBarPhaseDisplay extends AbstractPhaseDisplay protected static final GUIPreferences GUIP = GUIPreferences.getInstance(); private static final int BUTTON_ROWS = 2; private static final String SBPD_KEY_CLEARBUTTON = "clearButton"; + protected final ClientGUI clientgui; /** * timer that ends turn if time limit set in options is over @@ -101,6 +102,7 @@ public int compare(PhaseCommand c1, PhaseCommand c2) { protected StatusBarPhaseDisplay(ClientGUI cg) { super(cg); + this.clientgui = cg; getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), SBPD_KEY_CLEARBUTTON); getActionMap().put(SBPD_KEY_CLEARBUTTON, new AbstractAction() { @Override @@ -398,26 +400,4 @@ public void setStatusBarWithNotDonePlayers() { } } } - - public void setWeaponFieldOfFire(Entity unit, int[][] ranges, int arc, int loc) { - setWeaponFieldOfFire(unit, ranges, arc, loc, unit.getFacing()); - } - - public void setWeaponFieldOfFire(Entity unit, int[][] ranges, int arc, int loc, int facing) { - clientgui.getBoardView().fieldOfFireUnit = unit; - clientgui.getBoardView().fieldOfFireRanges = ranges; - clientgui.getBoardView().fieldOfFireWpArc = arc; - clientgui.getBoardView().fieldOfFireWpLoc = loc; - - clientgui.getBoardView().setWeaponFieldOfFire(facing, unit.getPosition()); - } - - public void setWeaponFieldOfFire(Entity unit, int[][] ranges, int arc, int loc, MovePath cmd) { - clientgui.getBoardView().fieldOfFireUnit = unit; - clientgui.getBoardView().fieldOfFireRanges = ranges; - clientgui.getBoardView().fieldOfFireWpArc = arc; - clientgui.getBoardView().fieldOfFireWpLoc = loc; - - clientgui.getBoardView().setWeaponFieldOfFire(unit, cmd); - } } \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/TargetingPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/TargetingPhaseDisplay.java index 82c58e906c2..fa4d306cf5b 100644 --- a/megamek/src/megamek/client/ui/swing/TargetingPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/TargetingPhaseDisplay.java @@ -384,7 +384,7 @@ private void beginMyTurn() { if (!clientgui.getBoardView().isMovingUnits()) { clientgui.maybeShowUnitDisplay(); } - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); if (GUIP.getAutoSelectNextUnit()) { @@ -448,7 +448,7 @@ private void endMyTurn() { clientgui.getBoardView().highlight(null); clientgui.getBoardView().cursor(null); clientgui.getBoardView().clearMovementData(); - clientgui.getBoardView().clearFieldOfFire(); + clientgui.clearFieldOfFire(); clientgui.clearTemporarySprites(); clientgui.setSelectedEntityNum(Entity.NONE); disableButtons(); diff --git a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java index 177b247ad36..4838dcfb430 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java +++ b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java @@ -204,16 +204,6 @@ public final class BoardView extends AbstractBoardView implements BoardListener, // vector of sprites for aero flyover lines private ArrayList flyOverSprites = new ArrayList<>(); - // List of sprites for the weapon field of fire - private ArrayList fieldOfFireSprites = new ArrayList<>(); - public int[][] fieldOfFireRanges = {new int[5], new int[5]}; - public int fieldOfFireWpArc; - public Entity fieldOfFireUnit; - public int fieldOfFireWpLoc; - // int because it acts as an array index - public int fieldOfFireWpUnderwater = 0; - private static final String[] rangeTexts = {"min", "S", "M", "L", "E"}; - TilesetManager tileManager; // polygons for a few things @@ -871,11 +861,6 @@ public void draw(Graphics g) { drawSprites(g, wreckSprites); } - // Field of Fire - if (!useIsometric() && shouldShowFieldOfFire()) { - drawSprites(g, fieldOfFireSprites); - } - // Minefield signs all over the place! drawMinefields(g); @@ -1597,11 +1582,6 @@ public BufferedImage getEntireBoardImage(boolean ignoreUnits, boolean useBaseZoo drawSprites(boardGraph, wreckSprites); } - // Field of Fire - if (!useIsometric() && shouldShowFieldOfFire()) { - drawSprites(boardGraph, fieldOfFireSprites); - } - // Minefield signs all over the place! drawMinefields(boardGraph); @@ -1715,9 +1695,6 @@ private void drawHexes(Graphics g, Rectangle view, boolean saveBoardImage) { if ((hex != null)) { drawHex(c, g, saveBoardImage); drawOrthograph(c, g); - if (shouldShowFieldOfFire()) { - drawHexSpritesForHex(c, g, fieldOfFireSprites); - } drawHexSpritesForHex(c, g, behindTerrainHexSprites); if ((en_Deployer != null) && board.isLegalDeployment(c, en_Deployer)) { @@ -4305,7 +4282,6 @@ public void clearSprites() { vtolAttackSprites.clear(); flyOverSprites.clear(); movementSprites.clear(); - fieldOfFireSprites.clear(); overTerrainSprites.clear(); behindTerrainHexSprites.clear(); @@ -4755,10 +4731,6 @@ private void zoom() { allSprites.forEach(Sprite::prepare); - for (Sprite spr : fieldOfFireSprites) { - spr.prepare(); - } - updateFontSizes(); updateBoard(); @@ -4887,10 +4859,6 @@ public boolean toggleIsometric() { allSprites.forEach(Sprite::prepare); hexSprites.forEach(HexSprite::updateBounds); - for (Sprite spr : fieldOfFireSprites) { - spr.prepare(); - } - clearHexImageCache(); updateBoard(); boardPanel.repaint(); @@ -4979,167 +4947,6 @@ public Polygon getHexPoly() { return hexPoly; } - public void clearFieldOfFire() { - fieldOfFireWpArc = -1; - fieldOfFireUnit = null; - fieldOfFireSprites.clear(); - boardPanel.repaint(); - } - - // this is called from MovementDisplay and checks if - // the unit ends up underwater - public void setWeaponFieldOfFire(Entity ce, MovePath cmd) { - // if lack of data: clear and return - if ((fieldOfFireUnit == null) - || (ce == null) - || (cmd == null)) { - clearFieldOfFire(); - return; - } - - // If the field of fire is not displayed - // for the active unit, then don't change anything - if (fieldOfFireUnit.equals(ce)) { - - fieldOfFireWpUnderwater = 0; - // check if the weapon ends up underwater - Hex hex = game.getBoard().getHex(cmd.getFinalCoords()); - - if ((hex.terrainLevel(Terrains.WATER) > 0) && !cmd.isJumping() - && (cmd.getFinalElevation() < 0)) { - if ((fieldOfFireUnit instanceof Mech) && !fieldOfFireUnit.isProne() - && (hex.terrainLevel(Terrains.WATER) == 1)) { - if ((fieldOfFireWpLoc == Mech.LOC_RLEG) || (fieldOfFireWpLoc == Mech.LOC_LLEG)) { - fieldOfFireWpUnderwater = 1; - } - - if (fieldOfFireUnit instanceof QuadMech) { - if ((fieldOfFireWpLoc == Mech.LOC_RARM) - || (fieldOfFireWpLoc == Mech.LOC_LARM)) { - fieldOfFireWpUnderwater = 1; - } - } - if (fieldOfFireUnit instanceof TripodMech) { - if (fieldOfFireWpLoc == Mech.LOC_CLEG) { - fieldOfFireWpUnderwater = 1; - } - } - } else { - fieldOfFireWpUnderwater = 1; - } - } - setWeaponFieldOfFire(cmd.getFinalFacing(), cmd.getFinalCoords()); - } - } - - // prepares the sprites for a field of fire - public void setWeaponFieldOfFire(int fac, Coords c) { - if (fieldOfFireUnit == null || c == null) { - clearFieldOfFire(); - return; - } - - // Do not display anything for offboard units - if (fieldOfFireUnit.isOffBoard()) { - clearFieldOfFire(); - return; - } - - // check if extreme range is used - int maxrange = 4; - if (!game.getBoard().onGround() || game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_RANGE)) { - maxrange = 5; - } - - // create the lists of hexes - List> fieldFire = new ArrayList<>(5); - int range = 1; - // for all available range brackets Min/S/M/L/E ... - for (int bracket = 0; bracket < maxrange; bracket++) { - fieldFire.add(new HashSet<>()); - // Add all hexes up to the weapon range to separate lists - while (range <= fieldOfFireRanges[fieldOfFireWpUnderwater][bracket]) { - fieldFire.get(bracket).addAll(c.allAtDistance(range)); - range++; - if (range > 100) { - break; // only to avoid hangs - } - } - - // Remove hexes that are not on the board or not in the arc - fieldFire.get(bracket).removeIf(h -> !game.getBoard().contains(h) || !Compute.isInArc(c, fac, h, fieldOfFireWpArc)); - } - - // create the sprites - // - fieldOfFireSprites.clear(); - - // for all available range brackets Min/S/M/L/E ... - for (int bracket = 0; bracket < fieldFire.size(); bracket++) { - if (fieldFire.get(bracket) == null) { - continue; - } - for (Coords loc : fieldFire.get(bracket)) { - // check surrounding hexes - int edgesToPaint = 0; - for (int dir = 0; dir < 6; dir++) { - Coords adjacentHex = loc.translated(dir); - if (!fieldFire.get(bracket).contains(adjacentHex)) { - edgesToPaint += (1 << dir); - } - } - // create sprite if there's a border to paint - if (edgesToPaint > 0) { - FieldofFireSprite ffSprite = new FieldofFireSprite(this, bracket, loc, edgesToPaint); - fieldOfFireSprites.add(ffSprite); - } - } - // Add range markers (m, S, M, L, E) - // this looks for a hex in the middle of the range bracket; - // if outside the board, nearer hexes will be tried until - // the inner edge of the range bracket is reached - // the directions tested are those that fall between the - // hex facings because this makes for a better placement - // ... most of the time... - - // The directions[][] is used to make the marker placement - // fairly symmetrical to the unit facing which a simple for - // loop over the hex facings doesn't do - int[][] directions = {{0, 1}, {0, 5}, {3, 2}, {3, 4}, {1, 2}, {5, 4}}; - // don't paint too many "min" markers - int numMinMarkers = 0; - for (int[] dir : directions) { - // find the middle of the range bracket - int rangeend = Math.max(fieldOfFireRanges[fieldOfFireWpUnderwater][bracket], 0); - int rangebegin = 1; - if (bracket > 0) { - rangebegin = Math.max(fieldOfFireRanges[fieldOfFireWpUnderwater][bracket - 1] + 1, 1); - } - int dist = (rangeend + rangebegin) / 2; - // translate to the middle of the range bracket - Coords mark = c.translated((dir[0] + fac) % 6, (dist + 1) / 2) - .translated((dir[1] + fac) % 6, dist / 2); - // traverse back to the unit until a hex is onboard - while (!game.getBoard().contains(mark)) { - mark = Coords.nextHex(mark, c); - } - - // add a text range marker if the found position is good - if (game.getBoard().contains(mark) && fieldFire.get(bracket).contains(mark) - && ((bracket > 0) || (numMinMarkers < 2))) { - TextMarkerSprite tS = new TextMarkerSprite(this, mark, - rangeTexts[bracket], FieldofFireSprite.getFieldOfFireColor(bracket)); - fieldOfFireSprites.add(tS); - if (bracket == 0) { - numMinMarkers++; - } - } - } - } - - boardPanel.repaint(); - } - /** Displays a dialog and changes the theme of all * board hexes to the user-chosen theme. */ @@ -5193,13 +5000,6 @@ public boolean shouldFovDarken() { return GUIP.getFovDarken() && !(game.getPhase().isReport()); } - public boolean shouldShowFieldOfFire() { - return GUIP.getShowFieldOfFire() && - (game.getPhase().isDeployment() || game.getPhase().isMovement() - || game.getPhase().isTargeting() || game.getPhase().isFiring() - || game.getPhase().isOffboard()); - } - public void setShowLobbyPlayerDeployment(boolean b) { showLobbyPlayerDeployment = b; } diff --git a/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java b/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java new file mode 100644 index 00000000000..d6db776218f --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java @@ -0,0 +1,415 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ +package megamek.client.ui.swing.boardview; + +import megamek.client.ui.swing.*; +import megamek.common.*; +import megamek.common.equipment.WeaponMounted; +import megamek.common.options.OptionsConstants; +import megamek.common.preference.IPreferenceChangeListener; +import megamek.common.preference.PreferenceChangeEvent; +import megamek.common.weapons.infantry.InfantryWeapon; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * This BoardViewSpriteHandler handles the sprites for the firing arcs (field of fire) that can be shown for + * individual weapons or bays. The field of fire depends on a handful of variables (position, unit facing + * and twists, weapon arc and range). These variables can and must be set individually and are cached. + */ +public class FiringArcSpriteHandler extends BoardViewSpriteHandler implements IPreferenceChangeListener { + + private static final String[] rangeTexts = {"min", "S", "M", "L", "E"}; + + private final Game game; + private final ClientGUI clientGUI; + + // In this handler, the values are cached because only some of the values are updated by method calls at a time + private Entity firingEntity; + private Coords firingPosition; + private int facing; + private int arc; + private boolean isUnderWater = false; + private final int[][] ranges = new int[2][5]; + + public FiringArcSpriteHandler(BoardView boardView, ClientGUI clientGUI) { + super(boardView); + this.clientGUI = clientGUI; + game = clientGUI.getClient().getGame(); + } + + /** + * Updates the facing that is used for aligning the field of fire. + * Does not change the assumed unit, position weapon and arc. + * + * @param facing The unit's facing + */ + public void updateFacing(int facing) { + this.facing = facing; + renewSprites(); + } + + /** + * Updates the position that is used for centering the field of fire to the given Coords. + * Does not change the assumed unit, weapon and arc, nor the facing. + * + * @param firingPosition The position to center the field of fire on + */ + public void updatePosition(Coords firingPosition) { + this.firingPosition = firingPosition; + renewSprites(); + } + + /** + * Updates the position and facing that is used for centering the field of fire to the end + * position and facing of the given movement path. Does not change the assumed unit, weapon and arc. + * + * @param movePath The considered movement path + */ + public void updatePosition(MovePath movePath) { + firingPosition = movePath.getFinalCoords(); + facing = movePath.getFinalFacing(); + isUnderWater=testUnderWater(movePath); + renewSprites(); + } + + /** + * Sets the selected unit and weapon. This will recalculate ranges, the facing including possible + * torso/turret twists and the weapon arc. When no firing position is currently stored, the unit's + * position will be used. This method does not check if the weapon is valid or, actually, on the unit. + * + * @param firingEntity the unit carrying the weapon to consider + * @param weapon the weapon to consider + */ + public void updateSelectedWeapon(Entity firingEntity, WeaponMounted weapon) { + this.firingEntity = firingEntity; + arc = firingEntity.getWeaponArc(clientGUI.getSelectedWeaponId()); + findRanges(weapon); + updateFacing(weapon); + if (firingPosition == null) { + firingPosition = firingEntity.getPosition(); + } + renewSprites(); + } + + /** + * Clears the sprites and resets the cached values so no new sprites are drawn at this time. As this + * handler requires cached values for weapon, arc etc. to draw the sprites, the {@link #clear()} method + * may not be overridden to reset those fields as is done in other handlers. + */ + public void clearValues() { + clear(); + firingEntity = null; + firingPosition = null; + isUnderWater = false; + } + + /** + * Draw the sprites for the currently stored values for position, unit, arc etc. Does not draw sprites + * if field of fire is deactivated. + */ + public void renewSprites() { + clear(); + if (!GUIP.getShowFieldOfFire() || (firingEntity == null) || (firingPosition == null) + || firingEntity.isOffBoard() || !clientGUI.hasSelectedWeapon()) { + return; + } + + // check if extreme range is used + int maxrange = 4; + if (!game.getBoard().onGround() || game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_RANGE)) { + maxrange = 5; + } + + // create the lists of hexes + List> fieldFire = new ArrayList<>(5); + int range = 1; + // for all available range brackets Min/S/M/L/E ... + for (int bracket = 0; bracket < maxrange; bracket++) { + fieldFire.add(new HashSet<>()); + // Add all hexes up to the weapon range to separate lists + while (range <= ranges[underWaterIndex()][bracket]) { + fieldFire.get(bracket).addAll(firingPosition.allAtDistance(range)); + range++; + if (range > 100) { + break; // only to avoid hangs + } + } + + // Remove hexes that are not on the board or not in the arc + fieldFire.get(bracket).removeIf(h -> !game.getBoard().contains(h) || !Compute.isInArc(firingPosition, facing, h, arc)); + } + + // for all available range brackets Min/S/M/L/E ... + for (int bracket = 0; bracket < fieldFire.size(); bracket++) { + if (fieldFire.get(bracket) == null) { + continue; + } + for (Coords loc : fieldFire.get(bracket)) { + // check surrounding hexes + int edgesToPaint = 0; + for (int dir = 0; dir < 6; dir++) { + Coords adjacentHex = loc.translated(dir); + if (!fieldFire.get(bracket).contains(adjacentHex)) { + edgesToPaint += (1 << dir); + } + } + // create sprite if there's a border to paint + if (edgesToPaint > 0) { + FieldofFireSprite ffSprite = new FieldofFireSprite(boardView, bracket, loc, edgesToPaint); + currentSprites.add(ffSprite); + } + } + // Add range markers (m, S, M, L, E) + // this looks for a hex in the middle of the range bracket; + // if outside the board, nearer hexes will be tried until + // the inner edge of the range bracket is reached + // the directions tested are those that fall between the + // hex facings because this makes for a better placement + // ... most of the time... + + // The directions[][] is used to make the marker placement + // fairly symmetrical to the unit facing which a simple for + // loop over the hex facings doesn't do + int[][] directions = {{0, 1}, {0, 5}, {3, 2}, {3, 4}, {1, 2}, {5, 4}}; + // don't paint too many "min" markers + int numMinMarkers = 0; + for (int[] dir : directions) { + // find the middle of the range bracket + int rangeend = Math.max(ranges[underWaterIndex()][bracket], 0); + int rangebegin = 1; + if (bracket > 0) { + rangebegin = Math.max(ranges[underWaterIndex()][bracket - 1] + 1, 1); + } + int dist = (rangeend + rangebegin) / 2; + // translate to the middle of the range bracket + Coords mark = firingPosition.translated((dir[0] + facing) % 6, (dist + 1) / 2) + .translated((dir[1] + facing) % 6, dist / 2); + // traverse back to the unit until a hex is onboard + while (!game.getBoard().contains(mark)) { + mark = Coords.nextHex(mark, firingPosition); + } + + // add a text range marker if the found position is good + if (game.getBoard().contains(mark) && fieldFire.get(bracket).contains(mark) + && ((bracket > 0) || (numMinMarkers < 2))) { + TextMarkerSprite tS = new TextMarkerSprite(boardView, mark, + rangeTexts[bracket], FieldofFireSprite.getFieldOfFireColor(bracket)); + currentSprites.add(tS); + if (bracket == 0) { + numMinMarkers++; + } + } + } + } + + boardView.addSprites(currentSprites); + } + + + @Override + public void initialize() { + GUIP.addPreferenceChangeListener(this); + } + + @Override + public void dispose() { + clear(); + GUIP.removePreferenceChangeListener(this); + } + + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + switch (evt.getName()) { + case GUIPreferences.SHOW_FIELD_OF_FIRE: + case GUIPreferences.BOARD_FIELD_OF_FIRE_EXTREME_COLOR: + case GUIPreferences.BOARD_FIELD_OF_FIRE_LONG_COLOR: + case GUIPreferences.BOARD_FIELD_OF_FIRE_MEDIUM_COLOR: + case GUIPreferences.BOARD_FIELD_OF_FIRE_SHORT_COLOR: + case GUIPreferences.BOARD_FIELD_OF_FIRE_MIN_COLOR: + renewSprites(); + break; + } + } + + /** + * @return The ranges lookup index depending on whether the weapon is underWater + */ + private int underWaterIndex() { + return isUnderWater ? 1 : 0; + } + + private void updateFacing(WeaponMounted weapon) { + facing = firingEntity.getFacing(); + if (game.getPhase().isFiring()) { + if (firingEntity.isSecondaryArcWeapon(firingEntity.getEquipmentNum(weapon))) { + facing = firingEntity.getSecondaryFacing(); + } + // If this is mech with turrets, check to see if the weapon is on a turret. + if ((firingEntity instanceof Mech) && (weapon.isMechTurretMounted())) { + // facing is currently adjusted for mek torso twist and facing, adjust for turret facing. + facing = (weapon.getFacing() + facing) % 6; + } + // If this is a tank with dual turrets, check to see if the weapon is a second turret. + if ((firingEntity instanceof Tank) && (weapon.getLocation() == ((Tank) firingEntity).getLocTurret2())) { + facing = ((Tank) firingEntity).getDualTurretFacing(); + } + } else if (game.getPhase().isTargeting()) { + if (firingEntity.isSecondaryArcWeapon(firingEntity.getEquipmentNum(weapon))) { + facing = firingEntity.getSecondaryFacing(); + } + } + } + + private void findRanges(WeaponMounted weapon) { + WeaponType wtype = weapon.getType(); + ranges[0] = wtype.getRanges(weapon); + + AmmoType atype = null; + if ((weapon.getLinked() != null) && (weapon.getLinked().getType() instanceof AmmoType)) { + atype = (AmmoType) weapon.getLinked().getType(); + } + + // gather underwater ranges + ranges[1] = wtype.getWRanges(); + if (atype != null) { + if ((wtype.getAmmoType() == AmmoType.T_SRM) + || (wtype.getAmmoType() == AmmoType.T_SRM_IMP) + || (wtype.getAmmoType() == AmmoType.T_MRM) + || (wtype.getAmmoType() == AmmoType.T_LRM) + || (wtype.getAmmoType() == AmmoType.T_LRM_IMP) + || (wtype.getAmmoType() == AmmoType.T_MML)) { + if (atype.getMunitionType().contains(AmmoType.Munitions.M_TORPEDO)) { + ranges[1] = wtype.getRanges(weapon); + } else if (atype.getMunitionType().contains(AmmoType.Munitions.M_MULTI_PURPOSE)) { + ranges[1] = wtype.getRanges(weapon); + } + } + } + + // Infantry range types 4+ are simplified to + // the usual range types as displaying 5 range circles + // would be visual overkill (and besides this makes + // things easier) + if (wtype instanceof InfantryWeapon) { + InfantryWeapon inftype = (InfantryWeapon) wtype; + int iR = inftype.getInfantryRange(); + ranges[0] = new int[] { 0, iR, iR * 2, iR * 3, 0 }; + ranges[1] = new int[] { 0, iR / 2, (iR / 2) * 2, (iR / 2) * 3, 0 }; + } + + // Artillery gets fixed ranges, 100 as an arbitrary + // large range for the targeting phase and + // 6 to 17 in the other phases as it will be + // direct fire then + if (wtype.hasFlag(WeaponType.F_ARTILLERY)) { + boolean isADA = (weapon.getLinked() != null + && ((AmmoType) weapon.getLinked().getType()).getMunitionType().contains(AmmoType.Munitions.M_ADA)); + if (game.getPhase().isTargeting()) { + ranges[0] = (!isADA? new int[] { 0, 0, 0, 100, 0 } : new int[] { 0, 0, 0, 51, 0 }); + } else { + ranges[0] = (!isADA? new int[] { 6, 0, 0, 17, 0 } : wtype.getRanges(weapon)); + } + ranges[1] = new int[] { 0, 0, 0, 0, 0 }; + } + + // Override for the MML ammos + if (atype != null) { + if (atype.getAmmoType() == AmmoType.T_MML) { + if (atype.hasFlag(AmmoType.F_MML_LRM)) { + if (atype.getMunitionType().contains(AmmoType.Munitions.M_DEAD_FIRE)) { + ranges[0] = new int[]{4, 5, 10, 15, 20}; + } else { + ranges[0] = new int[]{6, 7, 14, 21, 28}; + } + } else { + if (atype.getMunitionType().contains(AmmoType.Munitions.M_DEAD_FIRE)) { + ranges[0] = new int[]{0, 2, 4, 6, 8}; + } else { + ranges[0] = new int[]{0, 3, 6, 9, 12}; + } + } + } + } + + // No minimum range for hotload + if ((weapon.getLinked() != null) && weapon.getLinked().isHotLoaded()) { + ranges[0][0] = 0; + } + + // Aero + if (firingEntity.isAirborne()) { + + // keep original ranges ranges, no underwater + ranges[1] = new int[] { 0, 0, 0, 0, 0 }; + int maxr; + + // In the WeaponPanel, when the weapon is out of ammo + // or otherwise nonfunctional, SHORT range will be listed; + // the field of fire is instead disabled + // Here as well as in WeaponPanel, choosing a specific ammo + // only works for the current player's units + if (!weapon.isBreached() && !weapon.isMissing() + && !weapon.isDestroyed() && !weapon.isJammed() + && ((weapon.getLinked() == null) + || (weapon.getLinked().getUsableShotsLeft() > 0))) { + maxr = wtype.getMaxRange(weapon); + + // set the standard ranges, depending on capital or no + // boolean isCap = wtype.isCapital(); + int rangeMultiplier = wtype.isCapital() ? 2 : 1; + if (game.getBoard().onGround()) { + rangeMultiplier *= 8; + } + + for (int rangeIndex = RangeType.RANGE_MINIMUM; rangeIndex <= RangeType.RANGE_EXTREME; rangeIndex++) { + if (maxr >= rangeIndex) { + ranges[0][rangeIndex] = WeaponType.AIRBORNE_WEAPON_RANGES[rangeIndex] * rangeMultiplier; + } + } + } + } + } + + /** + * @return True when, for the given movement path, the currently selected weapon ends up being underwater. + * @param movePath The movement path that is considered for the selected unit + */ + private boolean testUnderWater(MovePath movePath) { + if ((firingEntity == null) || (movePath == null) || !clientGUI.hasSelectedWeapon()) { + return false; + } + + int location = clientGUI.getSelectedWeapon().getLocation(); + Hex hex = game.getBoard().getHex(movePath.getFinalCoords()); + int waterDepth = hex.terrainLevel(Terrains.WATER); + + if ((waterDepth > 0) && !movePath.isJumping() && (movePath.getFinalElevation() < 0)) { + if ((firingEntity instanceof Mech) && !firingEntity.isProne() && (waterDepth == 1)) { + return firingEntity.locationIsLeg(location); + } else { + return true; + } + } + return false; + } +} diff --git a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java index a54058cf10b..381fabe6696 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java +++ b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java @@ -268,11 +268,13 @@ public class ChatLounge extends AbstractPhaseDisplay implements private static final GUIPreferences GUIP = GUIPreferences.getInstance(); + private ClientGUI clientgui; + /** Creates a new chat lounge for the clientgui.getClient(). */ public ChatLounge(ClientGUI clientgui) { super(clientgui, SkinSpecification.UIComponents.ChatLounge.getComp(), SkinSpecification.UIComponents.ChatLoungeDoneButton.getComp()); - + this.clientgui = clientgui; setLayout(new BorderLayout()); splitPaneMain = new JSplitPane(JSplitPane.VERTICAL_SPLIT); splitPaneMain.setDividerSize(15); @@ -3606,5 +3608,10 @@ public void killPreviewBV() { previewBV.dispose(); } } + + @Override + public ClientGUI getClientgui() { + return clientgui; + } } diff --git a/megamek/src/megamek/client/ui/swing/unitDisplay/UnitDisplay.java b/megamek/src/megamek/client/ui/swing/unitDisplay/UnitDisplay.java index fe157eb62c3..6cb0a6338a4 100644 --- a/megamek/src/megamek/client/ui/swing/unitDisplay/UnitDisplay.java +++ b/megamek/src/megamek/client/ui/swing/unitDisplay/UnitDisplay.java @@ -437,11 +437,14 @@ protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boo * Displays the specified entity in the panel. */ public void displayEntity(Entity en) { - if (en == null) { + if ((en == null) || (currentlyDisplaying == en)) { return; } currentlyDisplaying = en; updateDisplay(); + if (clientgui != null) { + clientgui.clearFieldOfFire(); + } } protected void updateDisplay() { diff --git a/megamek/src/megamek/client/ui/swing/unitDisplay/WeaponPanel.java b/megamek/src/megamek/client/ui/swing/unitDisplay/WeaponPanel.java index a34477ba9df..8fd881b8bd3 100644 --- a/megamek/src/megamek/client/ui/swing/unitDisplay/WeaponPanel.java +++ b/megamek/src/megamek/client/ui/swing/unitDisplay/WeaponPanel.java @@ -2013,177 +2013,12 @@ private void displaySelected() { } // send event to other parts of the UI which care - if (oldmount.isInWaypointLaunchMode()) { - setFieldOfFire(oldmount); - } else { - setFieldOfFire(mounted); - } + unitDisplay.getClientGUI().showSensorRanges(entity); unitDisplay.processMechDisplayEvent(new MechDisplayEvent(this, entity, mounted)); onResize(); addListeners(); } - // this gathers all the range data - // it is a boiled down version of displaySelected, - // updateAttackValues, updateRangeDisplayForAmmo - private void setFieldOfFire(WeaponMounted mounted) { - // No field of fire without ClientGUI - if (unitDisplay.getClientGUI() == null) { - return; - } - - ClientGUI gui = unitDisplay.getClientGUI(); - - WeaponType wtype = mounted.getType(); - int[][] ranges = new int[2][5]; - ranges[0] = wtype.getRanges(mounted); - - AmmoType atype = null; - if ((mounted.getLinked() != null) && (mounted.getLinked().getType() instanceof AmmoType)) { - atype = (AmmoType) mounted.getLinked().getType(); - } - - // gather underwater ranges - ranges[1] = wtype.getWRanges(); - if (atype != null) { - if ((wtype.getAmmoType() == AmmoType.T_SRM) - || (wtype.getAmmoType() == AmmoType.T_SRM_IMP) - || (wtype.getAmmoType() == AmmoType.T_MRM) - || (wtype.getAmmoType() == AmmoType.T_LRM) - || (wtype.getAmmoType() == AmmoType.T_LRM_IMP) - || (wtype.getAmmoType() == AmmoType.T_MML)) { - if (atype.getMunitionType().contains(AmmoType.Munitions.M_TORPEDO)) { - ranges[1] = wtype.getRanges(mounted); - } else if (atype.getMunitionType().contains(AmmoType.Munitions.M_MULTI_PURPOSE)) { - ranges[1] = wtype.getRanges(mounted); - } - } - } - - // Infantry range types 4+ are simplified to - // the usual range types as displaying 5 range circles - // would be visual overkill (and besides this makes - // things easier) - if (wtype instanceof InfantryWeapon) { - InfantryWeapon inftype = (InfantryWeapon) wtype; - int iR = inftype.getInfantryRange(); - ranges[0] = new int[] { 0, iR, iR * 2, iR * 3, 0 }; - ranges[1] = new int[] { 0, iR / 2, (iR / 2) * 2, (iR / 2) * 3, 0 }; - } - - // Artillery gets fixed ranges, 100 as an arbitrary - // large range for the targeting phase and - // 6 to 17 in the other phases as it will be - // direct fire then - if (wtype.hasFlag(WeaponType.F_ARTILLERY)) { - boolean isADA = (mounted.getLinked() != null - && ((AmmoType) mounted.getLinked().getType()).getMunitionType().contains(AmmoType.Munitions.M_ADA)); - if (gui.getCurrentPanel() instanceof TargetingPhaseDisplay) { - ranges[0] = (!isADA? new int[] { 0, 0, 0, 100, 0 } : new int[] { 0, 0, 0, 51, 0 }); - } else { - ranges[0] = (!isADA? new int[] { 6, 0, 0, 17, 0 } : wtype.getRanges(mounted)); - } - ranges[1] = new int[] { 0, 0, 0, 0, 0 }; - } - - // Override for the MML ammos - if (atype != null) { - if (atype.getAmmoType() == AmmoType.T_MML) { - if (atype.hasFlag(AmmoType.F_MML_LRM)) { - if (atype.getMunitionType().contains(AmmoType.Munitions.M_DEAD_FIRE)) { - ranges[0] = new int[]{4, 5, 10, 15, 20}; - } else { - ranges[0] = new int[]{6, 7, 14, 21, 28}; - } - } else { - if (atype.getMunitionType().contains(AmmoType.Munitions.M_DEAD_FIRE)) { - ranges[0] = new int[]{0, 2, 4, 6, 8}; - } else { - ranges[0] = new int[]{0, 3, 6, 9, 12}; - } - } - } - } - - // No minimum range for hotload - if ((mounted.getLinked() != null) && mounted.getLinked().isHotLoaded()) { - ranges[0][0] = 0; - } - - // Aero - if (entity.isAirborne()) { - - // keep original ranges ranges, no underwater - ranges[1] = new int[] { 0, 0, 0, 0, 0 }; - int maxr = WeaponType.RANGE_SHORT; - - // In the WeaponPanel, when the weapon is out of ammo - // or otherwise nonfunctional, SHORT range will be listed; - // the field of fire is instead disabled - // Here as well as in WeaponPanel, choosing a specific ammo - // only works for the current player's units - if (!mounted.isBreached() && !mounted.isMissing() - && !mounted.isDestroyed() && !mounted.isJammed() - && ((mounted.getLinked() == null) - || (mounted.getLinked().getUsableShotsLeft() > 0))) { - maxr = wtype.getMaxRange(mounted); - - // set the standard ranges, depending on capital or no - // boolean isCap = wtype.isCapital(); - int rangeMultiplier = wtype.isCapital() ? 2 : 1; - final Game game = unitDisplay.getClientGUI().getClient().getGame(); - if (game.getBoard().onGround()) { - rangeMultiplier *= 8; - } - - for (int rangeIndex = RangeType.RANGE_MINIMUM; rangeIndex <= RangeType.RANGE_EXTREME; rangeIndex++) { - if (maxr >= rangeIndex) { - ranges[0][rangeIndex] = WeaponType.AIRBORNE_WEAPON_RANGES[rangeIndex] * rangeMultiplier; - } - } - } - } - - // pass all this to the Displays - int weaponId = entity.getEquipmentNum(mounted); - int arc = entity.getWeaponArc(weaponId); - int loc = mounted.getLocation(); - - if (gui.getCurrentPanel() instanceof FiringDisplay) { - // twisting - int facing = entity.getFacing(); - if (entity.isSecondaryArcWeapon(entity.getEquipmentNum(mounted))) { - facing = entity.getSecondaryFacing(); - } - // If this is mech with turrets, check to see if the weapon is on a turret. - if ((entity instanceof Mech) && (entity.getEquipment(weaponId).isMechTurretMounted())) { - // facing is currently adjusted for mek toro twist and facing, adjust for turret facing. - facing = (mounted.getFacing() + facing) % 6; - } - // If this is a tank with dual turrets, check to see if the weapon is a second turret. - if ((entity instanceof Tank) && - (entity.getEquipment(weaponId).getLocation() == ((Tank) entity).getLocTurret2())) { - facing = ((Tank) entity).getDualTurretFacing(); - } - ((FiringDisplay) gui.getCurrentPanel()).setWeaponFieldOfFire(entity, ranges, arc, loc, facing); - } else if (gui.getCurrentPanel() instanceof TargetingPhaseDisplay) { - // twisting - int facing = entity.getFacing(); - if (entity.isSecondaryArcWeapon(entity.getEquipmentNum(mounted))) { - facing = entity.getSecondaryFacing(); - } - ((TargetingPhaseDisplay) gui.getCurrentPanel()).setWeaponFieldOfFire(entity, ranges, arc, loc, facing); - } else if (gui.getCurrentPanel() instanceof MovementDisplay) { - // no twisting here - ((MovementDisplay) gui.getCurrentPanel()).setWeaponFieldOfFire(entity, ranges, arc, loc); - } else if (gui.getCurrentPanel() instanceof DeploymentDisplay) { - // no twisting here - ((DeploymentDisplay) gui.getCurrentPanel()).setWeaponFieldOfFire(entity, ranges, arc, loc); - } - - unitDisplay.getClientGUI().showSensorRanges(entity); - } - private String formatAmmo(Mounted m) { StringBuffer sb = new StringBuffer(64); int ammoIndex = m.getDesc().indexOf(Messages.getString("MechDisplay.0")); @@ -2711,7 +2546,6 @@ public void valueChanged(ListSelectionEvent event) { // Tell the Display to update the // firing arc info when a weapon has been de-selected if (weaponList.getSelectedIndex() == -1) { - unitDisplay.getClientGUI().getBoardView().clearFieldOfFire(); unitDisplay.getClientGUI().clearTemporarySprites(); } } diff --git a/megamek/src/megamek/client/ui/swing/util/TurnTimer.java b/megamek/src/megamek/client/ui/swing/util/TurnTimer.java index af7e0ad929d..5228ec71bef 100644 --- a/megamek/src/megamek/client/ui/swing/util/TurnTimer.java +++ b/megamek/src/megamek/client/ui/swing/util/TurnTimer.java @@ -60,7 +60,7 @@ public TurnTimer(int limit, AbstractPhaseDisplay pD, Client client) { int seconds = timeLimit % 60; int minutes = timeLimit / 60; remaining = new JLabel(String.format("%s:%02d", minutes, seconds)); - phaseDisplay.getClientgui().getMenuBar().add(display); + phaseDisplay.getClientgui().turnTimerComponent().add(display); display.setLayout(new FlowLayout()); display.add(remaining); display.add(progressBar); @@ -97,7 +97,7 @@ public void actionPerformed(ActionEvent ae) { phaseDisplay.ready(); timer.stop(); display.setVisible(false); - phaseDisplay.getClientgui().getMenuBar().remove(display); + phaseDisplay.getClientgui().turnTimerComponent().remove(display); } } }; @@ -106,7 +106,7 @@ public void actionPerformed(ActionEvent ae) { public void startTimer() { SwingUtilities.invokeLater(() -> { timer = new Timer(1000, listener); - phaseDisplay.getClientgui().getMenuBar().add(display); + phaseDisplay.getClientgui().turnTimerComponent().add(display); timer.start(); display.setVisible(true); @@ -116,8 +116,8 @@ public void startTimer() { public void stopTimer() { display.setVisible(false); - if (phaseDisplay.getClientgui().getMenuBar() != null) { - phaseDisplay.getClientgui().getMenuBar().remove(display); + if (phaseDisplay.getClientgui().turnTimerComponent() != null) { + phaseDisplay.getClientgui().turnTimerComponent().remove(display); } if (timer != null) { diff --git a/megamek/src/megamek/common/AbstractGame.java b/megamek/src/megamek/common/AbstractGame.java index f9fe25bc721..b5968b76fae 100644 --- a/megamek/src/megamek/common/AbstractGame.java +++ b/megamek/src/megamek/common/AbstractGame.java @@ -134,11 +134,7 @@ public void addGameListener(GameListener listener) { gameListeners.add(listener); } - /** - * Removes the specified game listener. - * - * @param listener the game listener. - */ + @Override public void removeGameListener(GameListener listener) { // Since gameListeners is transient, it could be null if (gameListeners == null) { diff --git a/megamek/src/megamek/common/Board.java b/megamek/src/megamek/common/Board.java index 14cf0cf979a..4d8f8a5f221 100644 --- a/megamek/src/megamek/common/Board.java +++ b/megamek/src/megamek/common/Board.java @@ -113,6 +113,9 @@ public class Board implements Serializable { /** Tags associated with this board to facilitate searching for it. */ private Set tags = new HashSet<>(); + + private final int boardId = 0; + //endregion Variable Declarations //region Constructors @@ -1790,4 +1793,10 @@ public void removeTag(String tag) { public Set getTags() { return Collections.unmodifiableSet(tags); } + + + /** @return The name of this map; this is meant to be displayed in the GUI. */ + public String getMapName() { + return "Board #" + boardId; + } } diff --git a/megamek/src/megamek/common/Game.java b/megamek/src/megamek/common/Game.java index 175dfc61fe9..4f577171d90 100644 --- a/megamek/src/megamek/common/Game.java +++ b/megamek/src/megamek/common/Game.java @@ -726,11 +726,6 @@ public GamePhase getPhase() { return phase; } - @Override - public void receivePhase(GamePhase phase) { - setPhase(phase); - } - @Override public void setPhase(GamePhase phase) { final GamePhase oldPhase = this.phase; diff --git a/megamek/src/megamek/common/IGame.java b/megamek/src/megamek/common/IGame.java index 07ce46012ab..5d57df52e89 100644 --- a/megamek/src/megamek/common/IGame.java +++ b/megamek/src/megamek/common/IGame.java @@ -89,6 +89,13 @@ default void receivePhase(GamePhase phase) { */ void addGameListener(GameListener listener); + /** + * Removes the specified game listener. + * + * @param listener the game listener. + */ + void removeGameListener(GameListener listener); + /** * @return Whether there is an active claim for victory. */ diff --git a/megamek/src/megamek/common/Mounted.java b/megamek/src/megamek/common/Mounted.java index fc7ce2a9060..437f540e0f3 100644 --- a/megamek/src/megamek/common/Mounted.java +++ b/megamek/src/megamek/common/Mounted.java @@ -1494,40 +1494,32 @@ public void setModeSwitchable(boolean b) { } /** - * Method that checks to see if our capital missile bay is in bearings-only mode + * @return True if our capital missile bay is in bearings-only mode * Only available in space games - * @return */ public boolean isInBearingsOnlyMode() { - if ((curMode().equals(Weapon.MODE_CAP_MISSILE_BEARING_EXT) - || curMode().equals(Weapon.MODE_CAP_MISSILE_BEARING_LONG) - || curMode().equals(Weapon.MODE_CAP_MISSILE_BEARING_MED) - || curMode().equals(Weapon.MODE_CAP_MISSILE_BEARING_SHORT) - || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_EXT) - || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_LONG) - || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_MED) - || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_SHORT)) - && getEntity().isSpaceborne()) { - return true; - } - return false; + return (curMode().equals(Weapon.MODE_CAP_MISSILE_BEARING_EXT) + || curMode().equals(Weapon.MODE_CAP_MISSILE_BEARING_LONG) + || curMode().equals(Weapon.MODE_CAP_MISSILE_BEARING_MED) + || curMode().equals(Weapon.MODE_CAP_MISSILE_BEARING_SHORT) + || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_EXT) + || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_LONG) + || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_MED) + || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_SHORT)) + && getEntity().isSpaceborne(); } /** - * Method that checks to see if our capital missile bay is in waypoint launch mode + * @return True if our capital missile bay is in waypoint launch mode * Only available in space games - * @return */ public boolean isInWaypointLaunchMode() { - if ((curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_EXT) + return (curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_EXT) || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_LONG) || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_MED) || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT_BEARING_SHORT) || curMode().equals(Weapon.MODE_CAP_MISSILE_WAYPOINT)) - && getEntity().isSpaceborne()) { - return true; - } - return false; + && getEntity().isSpaceborne(); } /** diff --git a/megamek/src/megamek/common/event/GameBoardNewEvent.java b/megamek/src/megamek/common/event/GameBoardNewEvent.java index 53526f8fdde..b43a2658088 100644 --- a/megamek/src/megamek/common/event/GameBoardNewEvent.java +++ b/megamek/src/megamek/common/event/GameBoardNewEvent.java @@ -24,12 +24,13 @@ */ public class GameBoardNewEvent extends GameEvent { private static final long serialVersionUID = -4444092727458493689L; - protected Board oldBoard; - protected Board newBoard; + + private final Board oldBoard; + private final Board newBoard; /** * Constructs the new event with the specified old/new board objects - * + * * @param source The event source * @param oldBoard old game board * @param newBoard new game board diff --git a/megamek/src/megamek/common/strategicBattleSystems/SBFGame.java b/megamek/src/megamek/common/strategicBattleSystems/SBFGame.java index ddd66e47aae..e1b67ce6458 100644 --- a/megamek/src/megamek/common/strategicBattleSystems/SBFGame.java +++ b/megamek/src/megamek/common/strategicBattleSystems/SBFGame.java @@ -22,13 +22,15 @@ import megamek.common.alphaStrike.AlphaStrikeElement; import megamek.common.annotations.Nullable; import megamek.common.enums.GamePhase; +import megamek.common.event.GamePhaseChangeEvent; import megamek.common.options.GameOptions; import megamek.common.options.OptionsConstants; import megamek.common.planetaryconditions.PlanetaryConditions; import org.apache.logging.log4j.LogManager; -import java.util.*; import java.util.Map; +import java.util.Collections; +import java.util.List; /** * This is an SBF game's game object that holds all game information. As of 2024, this is under construction. @@ -37,11 +39,10 @@ public final class SBFGame extends AbstractGame implements PlanetaryConditionsUs private final GameOptions options = new GameOptions(); //TODO: SBFGameOptions() private GamePhase phase = GamePhase.UNKNOWN; + private GamePhase lastPhase = GamePhase.UNKNOWN; private final PlanetaryConditions planetaryConditions = new PlanetaryConditions(); private final SBFFullGameReport gameReport = new SBFFullGameReport(); - private GamePhase lastPhase = GamePhase.UNKNOWN; - @Override public GameTurn getTurn() { return null; @@ -68,6 +69,13 @@ public void setPhase(GamePhase phase) { this.phase = phase; } + @Override + public void receivePhase(GamePhase phase) { + GamePhase oldPhase = this.phase; + setPhase(phase); + fireGameEvent(new GamePhaseChangeEvent(this, oldPhase, phase)); + } + @Override public boolean isForceVictory() { //TODO This should not be part of IGame! too specific return false; @@ -171,10 +179,6 @@ private boolean isSupportedUnitType(InGameObject object) { return object instanceof SBFFormation || object instanceof AlphaStrikeElement || object instanceof SBFUnit; } - public void setLastPhase(GamePhase lastPhase) { - this.lastPhase = lastPhase; - } - /** * Adds the given reports this game's reports. * @@ -201,4 +205,8 @@ public SBFFullGameReport getGameReport() { public void replaceAllReports(Map> newReports) { gameReport.replaceAllReports(newReports); } + + public void setLastPhase(GamePhase lastPhase) { + this.lastPhase = lastPhase; + } } diff --git a/megamek/src/megamek/server/GameManager.java b/megamek/src/megamek/server/GameManager.java index b9f422be6ba..1684ec8edf3 100644 --- a/megamek/src/megamek/server/GameManager.java +++ b/megamek/src/megamek/server/GameManager.java @@ -2311,7 +2311,6 @@ protected void endCurrentPhase() { break; case DEPLOYMENT: game.clearDeploymentThisRound(); -// game.checkForCompleteDeployment(); Enumeration pls = game.getPlayers(); while (pls.hasMoreElements()) { Player p = pls.nextElement();