From c3e3157032c23697eea250675009ba7ea1cd2c7a Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Sun, 4 Jun 2023 20:51:17 -0700 Subject: [PATCH 01/13] fix torso twist only activating done button. add condensed action tooltips --- .../client/ui/swing/ActionPhaseDisplay.java | 12 +++++- .../client/ui/swing/AttackPhaseDisplay.java | 12 +++++- .../client/ui/swing/MovementDisplay.java | 38 +++++++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java index ef5e3f4bef..60ec0a87b7 100644 --- a/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java @@ -23,6 +23,7 @@ import megamek.client.ui.swing.util.UIUtil; import megamek.client.ui.swing.widget.MegamekButton; import megamek.client.ui.swing.widget.SkinSpecification; +import megamek.common.annotations.Nullable; import megamek.common.preference.PreferenceChangeEvent; import javax.swing.*; @@ -135,7 +136,7 @@ protected boolean needNagForNoAction() { * @param skipButtonLabel * @param isDoingAction true if user has entered actions for this turn, false if not. */ - protected void updateDonePanelButtons(String doneButtonLabel, String skipButtonLabel, boolean isDoingAction) { + protected void updateDonePanelButtons(final String doneButtonLabel, final String skipButtonLabel, final boolean isDoingAction, @Nullable final String tooltip) { this.isDoingAction = isDoingAction; if (GUIP.getNagForNoAction()) { butDone.setText("" + doneButtonLabel + ""); @@ -161,5 +162,14 @@ protected void updateDonePanelButtons(String doneButtonLabel, String skipButtonL butDone.setEnabled(!GUIP.getNagForNoAction()); butSkipTurn.setEnabled(true); } + + if (tooltip == null || tooltip.isEmpty()) { + String f = guiScaledFontHTML(UIUtil.uiLightViolet()) + KeyCommandBind.getDesc(KeyCommandBind.DONE) + ""; + butDone.setToolTipText("" + f + ""); + } else { + String f = guiScaledFontHTML(UIUtil.uiLightViolet()) + KeyCommandBind.getDesc(KeyCommandBind.DONE) + ""; + butDone.setToolTipText("" + f + "
" + tooltip + ""); + } + } } diff --git a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java index 8da94e4b28..92f634bc5d 100644 --- a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java @@ -50,7 +50,17 @@ protected AttackPhaseDisplay(ClientGUI cg) { @Override protected void updateDonePanel() { - updateDonePanelButtons(getDoneButtonLabel(), getSkipTurnButtonLabel(), !attacks.isEmpty()); + if (attacks.isEmpty() || (attacks.size() == 1 && attacks.firstElement() instanceof TorsoTwistAction)){ + updateDonePanelButtons(getDoneButtonLabel(), getSkipTurnButtonLabel(), false, null); + } else { + + StringBuilder tooltip = new StringBuilder(); + for (var a : attacks) { + tooltip.append(a.toDisplayableString(clientgui.getClient())); + tooltip.append("
"); + } + updateDonePanelButtons(getDoneButtonLabel(), getSkipTurnButtonLabel(), true, tooltip.toString()); + } } protected void removeAttack(Object o) diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index 706a761bdf..8c466d4bd4 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -1048,22 +1048,52 @@ private void updateAeroButtons() { @Override protected void updateDonePanel() { if (cmd == null || cmd.length() == 0) { - updateDonePanelButtons( Messages.getString("MovementDisplay.Move"), Messages.getString("MovementDisplay.Skip"), false); + updateDonePanelButtons( Messages.getString("MovementDisplay.Move"), Messages.getString("MovementDisplay.Skip"), false, null); return; } else { MovePath possible = cmd.clone(); possible.clipToPossible(); if (possible.length() == 0) { - updateDonePanelButtons(Messages.getString("MovementDisplay.Move"), Messages.getString("MovementDisplay.Skip"), false); + updateDonePanelButtons(Messages.getString("MovementDisplay.Move"), Messages.getString("MovementDisplay.Skip"), false, null); } else if (!possible.isMoveLegal()) { - updateDonePanelButtons(Messages.getString("MovementDisplay.IllegalMove"), Messages.getString("MovementDisplay.Skip"), false); + updateDonePanelButtons(Messages.getString("MovementDisplay.IllegalMove"), Messages.getString("MovementDisplay.Skip"), false, null); } else { int mp = possible.countMp(possible.isJumping()); boolean psrCheck = (!SharedUtility.doPSRCheck(cmd.clone()).isBlank()) || (!SharedUtility.doThrustCheck(cmd.clone(), clientgui.getClient()).isBlank()); boolean damageCheck = cmd.shouldMechanicalJumpCauseFallDamage() || cmd.hasActiveMASC() || (!(ce() instanceof VTOL) && cmd.hasActiveSupercharger()) || cmd.willCrushBuildings(); + + StringBuilder tooltip = new StringBuilder(); + MoveStepType lastType = null; + int lastTypeCount = 0; + int lastTypeMP = 0; + boolean lastIsDanger = false; + final String format = "%s x%d (%dMP)%s"; + for (final Enumeration step = cmd.getSteps(); step.hasMoreElements(); ) { + MoveStep thisStep = step.nextElement(); + MoveStepType thisType = thisStep.getType(); + boolean thisIsDanger = thisStep.isDanger(); + + if (lastTypeCount != 0 && lastType == thisType && lastIsDanger == thisIsDanger) { + lastTypeMP += thisStep.getMp(); + lastTypeCount++; + continue; + } + + if (lastTypeCount != 0) { + tooltip.append(String.format(format, lastType, lastTypeCount, lastTypeMP, lastIsDanger ? "*" : "")); + tooltip.append("
"); + } + + lastType = thisType; + lastTypeCount = 1; + lastTypeMP = thisStep.getMp(); + lastIsDanger = thisIsDanger; + } + tooltip.append(String.format(format, lastType, lastTypeCount, lastTypeMP, lastIsDanger ? "*" : "")); String moveMsg = Messages.getString("MovementDisplay.Move") + " (" + mp + "MP)" + (psrCheck ? "*" : "") + (psrCheck ? "*" : "") + (damageCheck ? "!" : ""); - updateDonePanelButtons(moveMsg, Messages.getString("MovementDisplay.Skip"), true); + + updateDonePanelButtons(moveMsg, Messages.getString("MovementDisplay.Skip"), true, tooltip.toString()); } } } From 5e95120f15ae5477c789d826bd1160e116f8ed3b Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Mon, 5 Jun 2023 18:24:58 -0700 Subject: [PATCH 02/13] TurnDetailsOverlay --- .../megamek/client/ui/swing/boardview/TurnDetailsOverlay.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java diff --git a/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java new file mode 100644 index 0000000000..a2d2fa9014 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java @@ -0,0 +1,4 @@ +package megamek.client.ui.swing.boardview; + +public class TurnDetailsOverlay { +} From da79db7dc79a168e65d9cf20351829c1b8fdc9f1 Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Tue, 6 Jun 2023 10:58:48 -0700 Subject: [PATCH 03/13] move shared overlay code to AbstractBoardViewOverlay --- .../boardview/AbstractBoardViewOverlay.java | 278 +++++++++++++++++ .../swing/boardview/KeyBindingsOverlay.java | 226 ++------------ .../boardview/PlanetaryConditionsOverlay.java | 281 +++--------------- .../swing/boardview/TurnDetailsOverlay.java | 67 ++++- 4 files changed, 397 insertions(+), 455 deletions(-) create mode 100644 megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java diff --git a/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java new file mode 100644 index 0000000000..a24205b43b --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java @@ -0,0 +1,278 @@ +/* + * MegaMek - Copyright (C) 2023 - The MegaMek Team + * + * This program 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 2 of the License, or (at your option) any later + * version. + * + * This program 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. + */ +package megamek.client.ui.swing.boardview; + +import megamek.client.ui.IDisplayable; +import megamek.client.ui.swing.ClientGUI; +import megamek.client.ui.swing.GUIPreferences; +import megamek.client.ui.swing.util.UIUtil; +import megamek.common.Game; +import megamek.common.KeyBindParser; +import megamek.common.enums.GamePhase; +import megamek.common.event.GameListener; +import megamek.common.event.GameListenerAdapter; +import megamek.common.event.GamePhaseChangeEvent; +import megamek.common.event.GameTurnChangeEvent; +import megamek.common.preference.IPreferenceChangeListener; +import megamek.common.preference.PreferenceChangeEvent; +import megamek.common.util.ImageUtil; +import org.apache.logging.log4j.LogManager; + +import java.awt.font.TextAttribute; +import java.text.AttributedString; +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.util.List; + +/** + * An overlay for the Boardview that displays a selection of Planetary Conditions + * for the current game situation + * + * + */ +public abstract class AbstractBoardViewOverlay implements IDisplayable, IPreferenceChangeListener { + private static final Font FONT = new Font("SansSerif", Font.PLAIN, 13); + private static final int PADDING_X = 10; + private static final int PADDING_Y = 5; + private static final Color SHADOW_COLOR = Color.DARK_GRAY; + private static final float FADE_SPEED = 0.2f; + ClientGUI clientGui; + protected static final GUIPreferences GUIP = GUIPreferences.getInstance(); + + /** True when the overlay is displayed or fading in. */ + private boolean visible; + /** True indicates the strings should be redrawn. */ + private boolean changed = true; + /** The cached image for this Display. */ + Image displayImage; + /** The current game phase. */ + GamePhase currentPhase; + Game currentGame; + /** True while fading in this overlay. */ + private boolean fadingIn = false; + /** True while fading out this overlay. */ + private boolean fadingOut = false; + /** The transparency of the overlay. Only used while fading in/out. */ + private float alpha = 1; + private int overlayWidth = 500; + private int overlayHeight = 500; + /** + * An overlay for the Boardview + */ + public AbstractBoardViewOverlay(Game game, ClientGUI cg) { + this.visible = getVisibilityGUIPreference(); + currentGame = game; + currentPhase = game.getPhase(); + game.addGameListener(gameListener); + clientGui = cg; + KeyBindParser.addPreferenceChangeListener(this); + GUIP.addPreferenceChangeListener(this); + } + + @Override + public void draw(Graphics graph, Rectangle clipBounds) { + if (!visible && !isSliding()) { + return; + } + + if ((clientGui == null) || (currentGame == null)) { + return; + } + + // At startup, phase and turn change and when the Planetary Conditions change, + // the cached image is (re)created + if (changed) { + changed = false; + + // calculate the size from the text lines, font and padding + Font newFont = FONT.deriveFont(FONT.getSize() * GUIP.getGUIScale()); + graph.setFont(newFont); + FontMetrics fm = graph.getFontMetrics(newFont); + List allLines = assembleTextLines(); + Rectangle r = getSize(graph, allLines, fm); + r = new Rectangle(r.width + 2 * PADDING_X, r.height + 2 * PADDING_Y); + overlayWidth = r.width; + overlayHeight = r.height; + + displayImage = ImageUtil.createAcceleratedImage(r.width, r.height); + Graphics intGraph = displayImage.getGraphics(); + UIUtil.setHighQualityRendering(intGraph); + + // draw a semi-transparent background rectangle + Color colorBG = GUIP.getPlanetaryConditionsColorBackground(); + intGraph.setColor(new Color(colorBG.getRed(), colorBG.getGreen(), colorBG.getBlue(), GUIP.getPlanetaryConditionsBackgroundTransparency())); + intGraph.fillRoundRect(0, 0, r.width, r.height, PADDING_X, PADDING_Y); + + // The coordinates to write the texts to + int x = PADDING_X; + int y = PADDING_Y + fm.getAscent(); + + // write the strings + for (String line: allLines) { + drawShadowedString(intGraph, line, x, y); + y += fm.getHeight(); + } + } + + // draw the cached image to the boardview + // uses Composite to draw the image with variable transparency + int distSide = getDistSide(clipBounds, overlayWidth); + int distTop = getDistTop(clipBounds, overlayHeight); + + + if (alpha < 1) { + // Save the former composite and set an alpha blending composite + Composite saveComp = ((Graphics2D) graph).getComposite(); + int type = AlphaComposite.SRC_OVER; + ((Graphics2D) graph).setComposite(AlphaComposite.getInstance(type, alpha)); + graph.drawImage(displayImage, clipBounds.x + distSide, clipBounds.y + distTop, null); + ((Graphics2D) graph).setComposite(saveComp); + } else { + graph.drawImage(displayImage, clipBounds.x + distSide, clipBounds.y + distTop, null); + } + } + + /** Calculates the pixel size of the display from the necessary text lines. */ + private Rectangle getSize(Graphics graph, List lines, FontMetrics fm) { + int width = 0; + for (String line: lines) { + if (fm.stringWidth(line) > width) { + if (line.startsWith("#") && line.length() > 7) { + line = line.substring(7); + } + + width = fm.stringWidth(line); + } + } + int height = fm.getHeight() * lines.size(); + return new Rectangle(width, height); + } + + /** Returns an ArrayList of all text lines to be shown. */ + protected abstract List assembleTextLines(); + + /** + * Draws the String s to the Graphics graph at position x, y + * with a shadow. If the string starts with #789ABC then 789ABC + * is converted to a color to write the rest of the text, + * otherwise TEXT_COLOR is used. + */ + private void drawShadowedString(Graphics graph, String s, int x, int y) { + Color textColor = getTextColorGUIPreference(); + // Extract a color code from the start of the string + // used to display headlines if it's there + if (s.startsWith("#") && s.length() > 7) { + try { + int red = Integer.parseInt(s.substring(1, 3), 16); + int grn = Integer.parseInt(s.substring(3, 5), 16); + int blu = Integer.parseInt(s.substring(5, 7), 16); + textColor = new Color(red, grn, blu); + } catch (Exception e) { + LogManager.getLogger().error("", e); + } + s = s.substring(7); + } + + if (s.length() > 0) { + AttributedString text = new AttributedString(s); + text.addAttribute(TextAttribute.FONT, new Font(FONT.getFontName(), Font.PLAIN, (int) (FONT.getSize() * GUIP.getGUIScale())), 0, s.length()); + + graph.setColor(SHADOW_COLOR); + graph.drawString(text.getIterator(), x + 1, y + 1); + graph.setColor(textColor); + graph.drawString(text.getIterator(), x, y); + } + } + + /** + * Activates or deactivates the overlay, fading it in or out. + * Also saves the visibility to the GUIPreferences so + * MegaMek remembers it. + * */ + public void setVisible(boolean vis) { + visible = vis; + setVisibilityGUIPreference(vis); + + if (vis) { + fadingIn = true; + fadingOut = false; + } else { + fadingIn = false; + fadingOut = true; + } + } + + public boolean isVisible() { + return visible; + } + + @Override + public boolean isSliding() { + return fadingOut || fadingIn; + } + + @Override + public boolean slide() { + if (fadingIn) { + alpha += FADE_SPEED; + if (alpha > 1) { + alpha = 1; + fadingIn = false; + } + return true; + } else if (fadingOut) { + alpha -= FADE_SPEED; + if (alpha < 0) { + alpha = 0; + fadingOut = false; + } + return true; + } + return false; + } + + /** Detects phase and turn changes to display Planetary Conditions. */ + private GameListener gameListener = new GameListenerAdapter() { + @Override + public void gamePhaseChange(GamePhaseChangeEvent e) { + currentPhase = e.getNewPhase(); + changed = true; + } + + @Override + public void gameTurnChange(GameTurnChangeEvent e) { + // The active player has changed + changed = true; + } + }; + + @Override + public void preferenceChange(PreferenceChangeEvent e) { + // change on any preference change + changed = true; + } + + protected abstract void setVisibilityGUIPreference(boolean value); + protected abstract boolean getVisibilityGUIPreference(); + protected abstract Color getTextColorGUIPreference(); + protected abstract int getDistTop(Rectangle clipBounds, int overlayHeight); + protected abstract int getDistSide(Rectangle clipBounds, int overlayWidth); +} diff --git a/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java index 84da2b407c..5b41f49bc3 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java @@ -13,29 +13,13 @@ */ package megamek.client.ui.swing.boardview; -import megamek.MMConstants; -import megamek.client.ui.IDisplayable; import megamek.client.ui.Messages; import megamek.client.ui.swing.ClientGUI; import megamek.client.ui.swing.GUIPreferences; import megamek.client.ui.swing.util.KeyCommandBind; -import megamek.client.ui.swing.util.UIUtil; import megamek.common.Game; -import megamek.common.KeyBindParser; -import megamek.common.enums.GamePhase; -import megamek.common.event.GameListener; -import megamek.common.event.GameListenerAdapter; -import megamek.common.event.GamePhaseChangeEvent; -import megamek.common.event.GameTurnChangeEvent; -import megamek.common.preference.IPreferenceChangeListener; -import megamek.common.preference.PreferenceChangeEvent; -import megamek.common.util.ImageUtil; -import org.apache.logging.log4j.LogManager; import java.awt.*; -import java.awt.font.TextAttribute; -import java.text.AttributedString; -import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -46,14 +30,7 @@ * * @author SJuliez */ -public class KeyBindingsOverlay implements IDisplayable, IPreferenceChangeListener { - private static final Font FONT = new Font(MMConstants.FONT_SANS_SERIF, Font.PLAIN, 13); - private static final int DIST_TOP = 30; - private static final int DIST_SIDE = 30; - private static final int PADDING_X = 10; - private static final int PADDING_Y = 5; - private static final Color SHADOW_COLOR = Color.DARK_GRAY; - private static final float FADE_SPEED = 0.2f; +public class KeyBindingsOverlay extends AbstractBoardViewOverlay { /** The keybinds to be shown during the firing phases (incl. physical etc.) */ private static final List BINDS_FIRE = Arrays.asList( @@ -102,109 +79,17 @@ public class KeyBindingsOverlay implements IDisplayable, IPreferenceChangeListen private static final List ADDTL_BINDS_BOARD_EDITOR = Arrays.asList( Messages.getString("KeyBindingsDisplay.fixedBindsBoardEd").split("\n")); - - ClientGUI clientGui; - private static final GUIPreferences GUIP = GUIPreferences.getInstance(); - - /** True when the overlay is displayed or fading in. */ - private boolean visible; - /** True indicates the strings should be redrawn. */ - private boolean changed = true; - /** The cached image for this Display. */ - Image displayImage; - /** The current game phase. */ - GamePhase currentPhase; - /** True while fading in this overlay. */ - private boolean fadingIn = false; - /** True while fading out this overlay. */ - private boolean fadingOut = false; - /** The transparency of the overlay. Only used while fading in/out. */ - private float alpha = 1; - /** * An overlay for the Boardview that displays a selection of keybinds * for the current game situation. */ public KeyBindingsOverlay(Game game, ClientGUI cg) { - visible = GUIP.getShowKeybindsOverlay(); - currentPhase = game.getPhase(); - game.addGameListener(gameListener); - clientGui = cg; - KeyBindParser.addPreferenceChangeListener(this); - GUIP.addPreferenceChangeListener(this); + super(game, cg); } + /** @return an ArrayList of all text lines to be shown. */ @Override - public void draw(Graphics graph, Rectangle clipBounds) { - if (!visible && !isSliding()) { - return; - } - - // At startup, phase and turn change and when the keybinds change, - // the cached image is (re)created - if (changed) { - changed = false; - - // calculate the size from the text lines, font and padding - Font newFont = FONT.deriveFont(FONT.getSize() * GUIP.getGUIScale()); - graph.setFont(newFont); - FontMetrics fm = graph.getFontMetrics(newFont); - List allLines = assembleTextLines(); - Rectangle r = getSize(graph, allLines, fm); - r = new Rectangle(r.width + 2 * PADDING_X, r.height + 2 * PADDING_Y); - - displayImage = ImageUtil.createAcceleratedImage(r.width, r.height); - Graphics intGraph = displayImage.getGraphics(); - UIUtil.setHighQualityRendering(intGraph); - - // draw a semi-transparent background rectangle - Color colorBG = GUIP.getPlanetaryConditionsColorBackground(); - intGraph.setColor(new Color(colorBG.getRed(), colorBG.getGreen(), colorBG.getBlue(), GUIP.getPlanetaryConditionsBackgroundTransparency())); - intGraph.fillRoundRect(0, 0, r.width, r.height, PADDING_X, PADDING_X); - - // The coordinates to write the texts to - int x = PADDING_X; - int y = PADDING_Y + fm.getAscent(); - - // write the strings - for (String line: allLines) { - drawShadowedString(intGraph, line, x, y); - y += fm.getHeight(); - } - } - - // draw the cached image to the boardview - // uses Composite to draw the image with variable transparency - if (alpha < 1) { - // Save the former composite and set an alpha blending composite - Composite saveComp = ((Graphics2D) graph).getComposite(); - int type = AlphaComposite.SRC_OVER; - ((Graphics2D) graph).setComposite(AlphaComposite.getInstance(type, alpha)); - graph.drawImage(displayImage, clipBounds.x + DIST_SIDE, clipBounds.y + DIST_TOP, null); - ((Graphics2D) graph).setComposite(saveComp); - } else { - graph.drawImage(displayImage, clipBounds.x + DIST_SIDE, clipBounds.y + DIST_TOP, null); - } - } - - /** Calculates the pixel size of the display from the necessary text lines. */ - private Rectangle getSize(Graphics graph, List lines, FontMetrics fm) { - int width = 0; - for (String line: lines) { - if (fm.stringWidth(line) > width) { - if (line.startsWith("#") && line.length() > 7) { - line = line.substring(7); - } - - width = fm.stringWidth(line); - } - } - int height = fm.getHeight() * lines.size(); - return new Rectangle(width, height); - } - - /** Returns an ArrayList of all text lines to be shown. */ - private List assembleTextLines() { + protected List assembleTextLines() { List result = new ArrayList<>(); Color colorTitle = GUIP.getPlanetaryConditionsColorTitle(); @@ -262,104 +147,27 @@ private List convertToStrings(List kcbs) { return result; } - /** - * Draws the String s to the Graphics graph at position x, y - * with a shadow. If the string starts with #789ABC then 789ABC - * is converted to a color to write the rest of the text, - * otherwise TEXT_COLOR is used. - */ - private void drawShadowedString(Graphics graph, String s, int x, int y) { - Color textColor = GUIP.getPlanetaryConditionsColorText(); - // Extract a color code from the start of the string - // used to display headlines if it's there - if (s.startsWith("#") && s.length() > 7) { - try { - int red = Integer.parseInt(s.substring(1, 3), 16); - int grn = Integer.parseInt(s.substring(3, 5), 16); - int blu = Integer.parseInt(s.substring(5, 7), 16); - textColor = new Color(red, grn, blu); - } catch (Exception ex) { - LogManager.getLogger().error("", ex); - } - s = s.substring(7); - } - - if (s.length() > 0) { - AttributedString text = new AttributedString(s); - text.addAttribute(TextAttribute.FONT, new Font(FONT.getFontName(), Font.PLAIN, (int) (FONT.getSize() * GUIP.getGUIScale())), 0, s.length()); - - graph.setColor(SHADOW_COLOR); - graph.drawString(text.getIterator(), x + 1, y + 1); - graph.setColor(textColor); - graph.drawString(text.getIterator(), x, y); - } - } - - /** - * Activates or deactivates the overlay, fading it in or out. - * Also saves the visibility to the GUIPreferences so - * MegaMek remembers it. - * */ - public void setVisible(boolean vis) { - visible = vis; - GUIP.setValue(GUIPreferences.SHOW_KEYBINDS_OVERLAY, vis); - if (vis) { - fadingIn = true; - fadingOut = false; - } else { - fadingIn = false; - fadingOut = true; - } + @Override + protected void setVisibilityGUIPreference(boolean value) { + GUIP.setValue(GUIPreferences.SHOW_KEYBINDS_OVERLAY, value); } - - public boolean isVisible() { - return visible; + @Override + protected boolean getVisibilityGUIPreference() { + return GUIP.getShowKeybindsOverlay(); } - @Override - public boolean isSliding() { - return fadingOut || fadingIn; + protected Color getTextColorGUIPreference() { + // FIXME there is currently no custom option for key bindings + return GUIP.getPlanetaryConditionsColorText(); } @Override - public boolean slide() { - if (fadingIn) { - alpha += FADE_SPEED; - if (alpha > 1) { - alpha = 1; - fadingIn = false; - } - return true; - } else if (fadingOut) { - alpha -= FADE_SPEED; - if (alpha < 0) { - alpha = 0; - fadingOut = false; - } - return true; - } - return false; + protected int getDistTop(Rectangle clipBounds, int overlayHeight) { + return 30; } - /** Detects phase and turn changes to display only relevant keybinds. */ - private GameListener gameListener = new GameListenerAdapter() { - @Override - public void gamePhaseChange(GamePhaseChangeEvent e) { - currentPhase = e.getNewPhase(); - changed = true; - } - - @Override - public void gameTurnChange(GameTurnChangeEvent e) { - // The active player has changed - changed = true; - } - }; - @Override - public void preferenceChange(PreferenceChangeEvent e) { - // change on any preference change - changed = true; + protected int getDistSide(Rectangle clipBounds, int overlayWidth) { + return 30; } - } diff --git a/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java index 968ffd42bd..35b423370a 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java @@ -1,88 +1,38 @@ -/* -* MegaMek - Copyright (C) 2020 - The MegaMek Team -* -* This program 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 2 of the License, or (at your option) any later -* version. -* -* This program 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. -*/ +/* +* MegaMek - Copyright (C) 2020 - The MegaMek Team +* +* This program 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 2 of the License, or (at your option) any later +* version. +* +* This program 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. +*/ package megamek.client.ui.swing.boardview; -import megamek.client.ui.IDisplayable; import megamek.client.ui.Messages; import megamek.client.ui.swing.ClientGUI; import megamek.client.ui.swing.GUIPreferences; import megamek.client.ui.swing.util.KeyCommandBind; -import megamek.client.ui.swing.util.UIUtil; import megamek.common.Game; -import megamek.common.KeyBindParser; import megamek.common.PlanetaryConditions; -import megamek.common.enums.GamePhase; -import megamek.common.event.GameListener; -import megamek.common.event.GameListenerAdapter; -import megamek.common.event.GamePhaseChangeEvent; -import megamek.common.event.GameTurnChangeEvent; -import megamek.common.preference.IPreferenceChangeListener; -import megamek.common.preference.PreferenceChangeEvent; -import megamek.common.util.ImageUtil; -import org.apache.logging.log4j.LogManager; -import java.awt.font.TextAttribute; -import java.text.AttributedString; import java.text.MessageFormat; import java.util.ArrayList; -import java.awt.AlphaComposite; import java.awt.Color; -import java.awt.Composite; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Image; import java.awt.Rectangle; import java.util.List; -/** +/** * An overlay for the Boardview that displays a selection of Planetary Conditions - * for the current game situation - * + * for the current game situation + * * */ -public class PlanetaryConditionsOverlay implements IDisplayable, IPreferenceChangeListener { - private static final Font FONT = new Font("SansSerif", Font.PLAIN, 13); - private static final int DIST_TOP = 30; - private int distSide = 500; - private int overlayWidth = 500; - private static final int PADDING_X = 10; - private static final int PADDING_Y = 5; - private static final Color SHADOW_COLOR = Color.DARK_GRAY; - private static final float FADE_SPEED = 0.2f; - - ClientGUI clientGui; - private static final GUIPreferences GUIP = GUIPreferences.getInstance(); - - /** True when the overlay is displayed or fading in. */ - private boolean visible; - /** True indicates the strings should be redrawn. */ - private boolean changed = true; - /** The cached image for this Display. */ - Image displayImage; - /** The current game phase. */ - GamePhase currentPhase; - - Game currentGame; - /** True while fading in this overlay. */ - private boolean fadingIn = false; - /** True while fading out this overlay. */ - private boolean fadingOut = false; - /** The transparency of the overlay. Only used while fading in/out. */ - private float alpha = 1; - +public class PlanetaryConditionsOverlay extends AbstractBoardViewOverlay { private static final String MSG_HEADING = Messages.getString("PlanetaryConditionsOverlay.heading"); private static final String MSG_TEMPERATURE = Messages.getString("PlanetaryConditionsOverlay.Temperature"); private static final String MSG_GRAVITY = Messages.getString("PlanetaryConditionsOverlay.Gravity"); @@ -95,98 +45,17 @@ public class PlanetaryConditionsOverlay implements IDisplayable, IPreferenceChan private static final String MSG_FOG = Messages.getString("PlanetaryConditionsOverlay.Fog"); private static final String MSG_BLOWINGSAND = Messages.getString("PlanetaryConditionsOverlay.BlowingSand"); - /** + /** * An overlay for the Boardview that displays a selection of Planetary Conditions - * for the current game situation. + * for the current game situation. */ public PlanetaryConditionsOverlay(Game game, ClientGUI cg) { - visible = GUIP.getShowPlanetaryConditionsOverlay(); - currentGame = game; - currentPhase = game.getPhase(); - game.addGameListener(gameListener); - clientGui = cg; - KeyBindParser.addPreferenceChangeListener(this); - GUIP.addPreferenceChangeListener(this); + super(game, cg); } + /** @return an ArrayList of all text lines to be shown. */ @Override - public void draw(Graphics graph, Rectangle clipBounds) { - if (!visible && !isSliding()) { - return; - } - - if ((clientGui == null) || (currentGame == null)) { - return; - } - - // At startup, phase and turn change and when the Planetary Conditions change, - // the cached image is (re)created - if (changed) { - changed = false; - - // calculate the size from the text lines, font and padding - Font newFont = FONT.deriveFont(FONT.getSize() * GUIP.getGUIScale()); - graph.setFont(newFont); - FontMetrics fm = graph.getFontMetrics(newFont); - List allLines = assembleTextLines(); - Rectangle r = getSize(graph, allLines, fm); - r = new Rectangle(r.width + 2 * PADDING_X, r.height + 2 * PADDING_Y); - overlayWidth = r.width; - - displayImage = ImageUtil.createAcceleratedImage(r.width, r.height); - Graphics intGraph = displayImage.getGraphics(); - UIUtil.setHighQualityRendering(intGraph); - - // draw a semi-transparent background rectangle - Color colorBG = GUIP.getPlanetaryConditionsColorBackground(); - intGraph.setColor(new Color(colorBG.getRed(), colorBG.getGreen(), colorBG.getBlue(), GUIP.getPlanetaryConditionsBackgroundTransparency())); - intGraph.fillRoundRect(0, 0, r.width, r.height, PADDING_X, PADDING_X); - - // The coordinates to write the texts to - int x = PADDING_X; - int y = PADDING_Y + fm.getAscent(); - - // write the strings - for (String line: allLines) { - drawShadowedString(intGraph, line, x, y); - y += fm.getHeight(); - } - } - - distSide = clipBounds.width - (overlayWidth + 100); - - // draw the cached image to the boardview - // uses Composite to draw the image with variable transparency - if (alpha < 1) { - // Save the former composite and set an alpha blending composite - Composite saveComp = ((Graphics2D) graph).getComposite(); - int type = AlphaComposite.SRC_OVER; - ((Graphics2D) graph).setComposite(AlphaComposite.getInstance(type, alpha)); - graph.drawImage(displayImage, clipBounds.x + distSide, clipBounds.y + DIST_TOP, null); - ((Graphics2D) graph).setComposite(saveComp); - } else { - graph.drawImage(displayImage, clipBounds.x + distSide, clipBounds.y + DIST_TOP, null); - } - } - - /** Calculates the pixel size of the display from the necessary text lines. */ - private Rectangle getSize(Graphics graph, List lines, FontMetrics fm) { - int width = 0; - for (String line: lines) { - if (fm.stringWidth(line) > width) { - if (line.startsWith("#") && line.length() > 7) { - line = line.substring(7); - } - - width = fm.stringWidth(line); - } - } - int height = fm.getHeight() * lines.size(); - return new Rectangle(width, height); - } - - /** Returns an ArrayList of all text lines to be shown. */ - private List assembleTextLines() { + protected List assembleTextLines() { List result = new ArrayList<>(); Color colorTitle = GUIP.getPlanetaryConditionsColorTitle(); @@ -203,7 +72,7 @@ private List assembleTextLines() { if (tmpStr.length() > 0) { result.add(tmpStr); } - + if (clientGui != null && !currentGame.getBoard().inSpace()) { // In a game, not the Board Editor @@ -295,104 +164,26 @@ private List assembleTextLines() { return result; } - /** - * Draws the String s to the Graphics graph at position x, y - * with a shadow. If the string starts with #789ABC then 789ABC - * is converted to a color to write the rest of the text, - * otherwise TEXT_COLOR is used. - */ - private void drawShadowedString(Graphics graph, String s, int x, int y) { - Color textColor = GUIP.getPlanetaryConditionsColorText(); - // Extract a color code from the start of the string - // used to display headlines if it's there - if (s.startsWith("#") && s.length() > 7) { - try { - int red = Integer.parseInt(s.substring(1, 3), 16); - int grn = Integer.parseInt(s.substring(3, 5), 16); - int blu = Integer.parseInt(s.substring(5, 7), 16); - textColor = new Color(red, grn, blu); - } catch (Exception e) { - LogManager.getLogger().error("", e); - } - s = s.substring(7); - } - - if (s.length() > 0) { - AttributedString text = new AttributedString(s); - text.addAttribute(TextAttribute.FONT, new Font(FONT.getFontName(), Font.PLAIN, (int) (FONT.getSize() * GUIP.getGUIScale())), 0, s.length()); - - graph.setColor(SHADOW_COLOR); - graph.drawString(text.getIterator(), x + 1, y + 1); - graph.setColor(textColor); - graph.drawString(text.getIterator(), x, y); - } - } - - /** - * Activates or deactivates the overlay, fading it in or out. - * Also saves the visibility to the GUIPreferences so - * MegaMek remembers it. - * */ - public void setVisible(boolean vis) { - visible = vis; - - if (vis) { - fadingIn = true; - fadingOut = false; - } else { - fadingIn = false; - fadingOut = true; - } - } - - public boolean isVisible() { - return visible; + @Override + protected void setVisibilityGUIPreference(boolean value) { + GUIP.setValue(GUIPreferences.SHOW_PLANETARYCONDITIONS_OVERLAY, value); } - @Override - public boolean isSliding() { - return fadingOut || fadingIn; + protected boolean getVisibilityGUIPreference() { + return GUIP.getShowPlanetaryConditionsOverlay(); } - @Override - public boolean slide() { - if (fadingIn) { - alpha += FADE_SPEED; - if (alpha > 1) { - alpha = 1; - fadingIn = false; - } - return true; - } else if (fadingOut) { - alpha -= FADE_SPEED; - if (alpha < 0) { - alpha = 0; - fadingOut = false; - } - return true; - } - return false; + protected Color getTextColorGUIPreference() { + return GUIP.getPlanetaryConditionsColorText(); } - - /** Detects phase and turn changes to display Planetary Conditions. */ - private GameListener gameListener = new GameListenerAdapter() { - @Override - public void gamePhaseChange(GamePhaseChangeEvent e) { - currentPhase = e.getNewPhase(); - changed = true; - } - - @Override - public void gameTurnChange(GameTurnChangeEvent e) { - // The active player has changed - changed = true; - } - }; @Override - public void preferenceChange(PreferenceChangeEvent e) { - // change on any preference change - changed = true; + protected int getDistTop(Rectangle clipBounds, int overlayHeight) { + return 30; } + @Override + protected int getDistSide(Rectangle clipBounds, int overlayWidth) { + return clipBounds.width - (overlayWidth + 100); + } } diff --git a/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java index a2d2fa9014..69c6f4f072 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java @@ -1,4 +1,69 @@ +/* + * MegaMek - Copyright (C) 2023 - The MegaMek Team + * + * This program 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 2 of the License, or (at your option) any later + * version. + * + * This program 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. + */ package megamek.client.ui.swing.boardview; -public class TurnDetailsOverlay { +import megamek.client.ui.swing.ClientGUI; +import megamek.client.ui.swing.GUIPreferences; +import megamek.common.Game; + +import java.util.ArrayList; +import java.awt.Color; +import java.awt.Rectangle; +import java.util.List; + +/** + * An overlay for the Boardview that displays info about the users turn, such as list of users Move or Attack commands + * for the current game situation + * + * + */ +public class TurnDetailsOverlay extends AbstractBoardViewOverlay { + /** + * An overlay for the Boardview that shows details about a players turn + */ + public TurnDetailsOverlay(Game game, ClientGUI cg) { + super(game, cg); + } + + + /** @return an ArrayList of all text lines to be shown. */ + @Override + protected List assembleTextLines() { + List result = new ArrayList<>(); + return result; + } + + @Override + protected void setVisibilityGUIPreference(boolean value) { + GUIP.setValue(GUIPreferences.SHOW_PLANETARYCONDITIONS_OVERLAY, value); + } + @Override + protected boolean getVisibilityGUIPreference() { + return GUIP.getShowPlanetaryConditionsOverlay(); + } + @Override + protected Color getTextColorGUIPreference() { + return GUIP.getPlanetaryConditionsColorText(); + } + + @Override + protected int getDistTop(Rectangle clipBounds, int overlayHeight) { + return clipBounds.height - (overlayHeight + 100); + } + + @Override + protected int getDistSide(Rectangle clipBounds, int overlayWidth) { + return clipBounds.width - (overlayWidth + 100); + } } From ec6e4a34d7ae96135f1612d4e30bd7da2b83eb55 Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Tue, 6 Jun 2023 12:59:50 -0700 Subject: [PATCH 04/13] TurnDetailsOverLay updates on move/fire --- .../client/ui/swing/ActionPhaseDisplay.java | 12 +- .../client/ui/swing/AttackPhaseDisplay.java | 12 +- .../client/ui/swing/MovementDisplay.java | 52 ++++--- .../boardview/AbstractBoardViewOverlay.java | 132 +++++++++++------- .../client/ui/swing/boardview/BoardView.java | 10 +- .../swing/boardview/KeyBindingsOverlay.java | 16 +-- .../boardview/PlanetaryConditionsOverlay.java | 20 +-- .../swing/boardview/TurnDetailsOverlay.java | 33 ++++- 8 files changed, 166 insertions(+), 121 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java index 60ec0a87b7..7a1450a57a 100644 --- a/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/ActionPhaseDisplay.java @@ -136,7 +136,8 @@ protected boolean needNagForNoAction() { * @param skipButtonLabel * @param isDoingAction true if user has entered actions for this turn, false if not. */ - protected void updateDonePanelButtons(final String doneButtonLabel, final String skipButtonLabel, final boolean isDoingAction, @Nullable final String tooltip) { + protected void updateDonePanelButtons(final String doneButtonLabel, final String skipButtonLabel, final boolean isDoingAction, + @Nullable java.util.List turnDetails) { this.isDoingAction = isDoingAction; if (GUIP.getNagForNoAction()) { butDone.setText("" + doneButtonLabel + ""); @@ -163,13 +164,6 @@ protected void updateDonePanelButtons(final String doneButtonLabel, final String butSkipTurn.setEnabled(true); } - if (tooltip == null || tooltip.isEmpty()) { - String f = guiScaledFontHTML(UIUtil.uiLightViolet()) + KeyCommandBind.getDesc(KeyCommandBind.DONE) + ""; - butDone.setToolTipText("" + f + ""); - } else { - String f = guiScaledFontHTML(UIUtil.uiLightViolet()) + KeyCommandBind.getDesc(KeyCommandBind.DONE) + ""; - butDone.setToolTipText("" + f + "
" + tooltip + ""); - } - + clientgui.getBoardView().turnDetailsOverlay.setLines(turnDetails); } } diff --git a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java index 078907fef8..9b92784e07 100644 --- a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java @@ -54,12 +54,14 @@ protected void updateDonePanel() // a torso twist alone should not trigger Done button updateDonePanelButtons(getDoneButtonLabel(), getSkipTurnButtonLabel(), false, null); } else { - StringBuilder tooltip = new StringBuilder(); - for (var a : attacks) { - tooltip.append(a.toDisplayableString(clientgui.getClient())); - tooltip.append("
"); + ArrayList turnDetails = new ArrayList(); + for (EntityAction a : attacks) { +// turnDetails.add( String.format("%12s %8s (%2d%%)"), +// a.toString() +// ); + turnDetails.add(toString()); } - updateDonePanelButtons(getDoneButtonLabel(), getSkipTurnButtonLabel(), true, tooltip.toString()); + updateDonePanelButtons(getDoneButtonLabel(), getSkipTurnButtonLabel(), true, turnDetails); } } diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index 8c466d4bd4..dfe43bfd7a 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -1062,38 +1062,46 @@ protected void updateDonePanel() { boolean psrCheck = (!SharedUtility.doPSRCheck(cmd.clone()).isBlank()) || (!SharedUtility.doThrustCheck(cmd.clone(), clientgui.getClient()).isBlank()); boolean damageCheck = cmd.shouldMechanicalJumpCauseFallDamage() || cmd.hasActiveMASC() || (!(ce() instanceof VTOL) && cmd.hasActiveSupercharger()) || cmd.willCrushBuildings(); - StringBuilder tooltip = new StringBuilder(); - MoveStepType lastType = null; - int lastTypeCount = 0; - int lastTypeMP = 0; - boolean lastIsDanger = false; - final String format = "%s x%d (%dMP)%s"; +// final String format = "%-12s x%2d (%2dMP)%s"; + final String format = "%-3s %-12s %2dMP%s"; + + MoveStepType accumType = null; + int accumTypeCount = 0; + int accumMP = 0; + int accumDanger = 0; + ArrayList turnDetails = new ArrayList(); for (final Enumeration step = cmd.getSteps(); step.hasMoreElements(); ) { - MoveStep thisStep = step.nextElement(); - MoveStepType thisType = thisStep.getType(); - boolean thisIsDanger = thisStep.isDanger(); - - if (lastTypeCount != 0 && lastType == thisType && lastIsDanger == thisIsDanger) { - lastTypeMP += thisStep.getMp(); - lastTypeCount++; + MoveStep currentStep = step.nextElement(); + MoveStepType currentType = currentStep.getType(); + int currentDanger = currentStep.isDanger() ? 1 : 0; + + if (accumTypeCount != 0 && accumType == currentType) { + accumTypeCount++; + accumMP += currentStep.getMp(); + accumDanger += currentDanger; continue; } - if (lastTypeCount != 0) { - tooltip.append(String.format(format, lastType, lastTypeCount, lastTypeMP, lastIsDanger ? "*" : "")); - tooltip.append("
"); + if (accumTypeCount != 0) { + turnDetails.add(String.format(format, + accumTypeCount == 1 ? "" : "x"+accumTypeCount, + accumType, accumMP, "*".repeat(accumDanger))); } - lastType = thisType; - lastTypeCount = 1; - lastTypeMP = thisStep.getMp(); - lastIsDanger = thisIsDanger; + accumType = currentType; + accumTypeCount = 1; + accumMP = currentStep.getMp(); + accumDanger = currentDanger; } - tooltip.append(String.format(format, lastType, lastTypeCount, lastTypeMP, lastIsDanger ? "*" : "")); + + turnDetails.add(String.format(format, + accumTypeCount == 1 ? "" : "x"+accumTypeCount, + accumType, accumMP, "*".repeat(accumDanger))); + String moveMsg = Messages.getString("MovementDisplay.Move") + " (" + mp + "MP)" + (psrCheck ? "*" : "") + (psrCheck ? "*" : "") + (damageCheck ? "!" : ""); - updateDonePanelButtons(moveMsg, Messages.getString("MovementDisplay.Skip"), true, tooltip.toString()); + updateDonePanelButtons(moveMsg, Messages.getString("MovementDisplay.Skip"), true, turnDetails); } } } diff --git a/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java index a24205b43b..f5b0a3ff20 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java @@ -14,8 +14,10 @@ package megamek.client.ui.swing.boardview; import megamek.client.ui.IDisplayable; +import megamek.client.ui.Messages; import megamek.client.ui.swing.ClientGUI; import megamek.client.ui.swing.GUIPreferences; +import megamek.client.ui.swing.util.KeyCommandBind; import megamek.client.ui.swing.util.UIUtil; import megamek.common.Game; import megamek.common.KeyBindParser; @@ -49,7 +51,6 @@ * */ public abstract class AbstractBoardViewOverlay implements IDisplayable, IPreferenceChangeListener { - private static final Font FONT = new Font("SansSerif", Font.PLAIN, 13); private static final int PADDING_X = 10; private static final int PADDING_Y = 5; private static final Color SHADOW_COLOR = Color.DARK_GRAY; @@ -60,12 +61,16 @@ public abstract class AbstractBoardViewOverlay implements IDisplayable, IPrefere /** True when the overlay is displayed or fading in. */ private boolean visible; /** True indicates the strings should be redrawn. */ - private boolean changed = true; + private boolean dirty = true; + private boolean hasContents = false; /** The cached image for this Display. */ - Image displayImage; + private Image displayImage; /** The current game phase. */ - GamePhase currentPhase; - Game currentGame; + protected GamePhase currentPhase; + protected final Game currentGame; + + protected final Font font; + /** True while fading in this overlay. */ private boolean fadingIn = false; /** True while fading out this overlay. */ @@ -74,10 +79,13 @@ public abstract class AbstractBoardViewOverlay implements IDisplayable, IPrefere private float alpha = 1; private int overlayWidth = 500; private int overlayHeight = 500; + + private final String header; /** * An overlay for the Boardview */ - public AbstractBoardViewOverlay(Game game, ClientGUI cg) { + public AbstractBoardViewOverlay(Game game, ClientGUI cg, Font font, String headerText ) { + this.font = font; this.visible = getVisibilityGUIPreference(); currentGame = game; currentPhase = game.getPhase(); @@ -85,6 +93,16 @@ public AbstractBoardViewOverlay(Game game, ClientGUI cg) { clientGui = cg; KeyBindParser.addPreferenceChangeListener(this); GUIP.addPreferenceChangeListener(this); + + Color colorTitle = GUIP.getPlanetaryConditionsColorTitle(); + header = String.format("#%02X%02X%02X %s", + colorTitle.getRed(), colorTitle.getGreen(), colorTitle.getBlue(), headerText); + } + + protected void addHeader(List lines) { + if (GUIP.getPlanetaryConditionsShowHeader()) { + lines.add(header); + } } @Override @@ -99,54 +117,60 @@ public void draw(Graphics graph, Rectangle clipBounds) { // At startup, phase and turn change and when the Planetary Conditions change, // the cached image is (re)created - if (changed) { - changed = false; + if (dirty) { + dirty = false; // calculate the size from the text lines, font and padding - Font newFont = FONT.deriveFont(FONT.getSize() * GUIP.getGUIScale()); + Font newFont = font.deriveFont(font.getSize() * GUIP.getGUIScale()); graph.setFont(newFont); FontMetrics fm = graph.getFontMetrics(newFont); List allLines = assembleTextLines(); - Rectangle r = getSize(graph, allLines, fm); - r = new Rectangle(r.width + 2 * PADDING_X, r.height + 2 * PADDING_Y); - overlayWidth = r.width; - overlayHeight = r.height; - - displayImage = ImageUtil.createAcceleratedImage(r.width, r.height); - Graphics intGraph = displayImage.getGraphics(); - UIUtil.setHighQualityRendering(intGraph); - - // draw a semi-transparent background rectangle - Color colorBG = GUIP.getPlanetaryConditionsColorBackground(); - intGraph.setColor(new Color(colorBG.getRed(), colorBG.getGreen(), colorBG.getBlue(), GUIP.getPlanetaryConditionsBackgroundTransparency())); - intGraph.fillRoundRect(0, 0, r.width, r.height, PADDING_X, PADDING_Y); - - // The coordinates to write the texts to - int x = PADDING_X; - int y = PADDING_Y + fm.getAscent(); - - // write the strings - for (String line: allLines) { - drawShadowedString(intGraph, line, x, y); - y += fm.getHeight(); + if (allLines.size() == 0) { + hasContents = false; + } else { + hasContents = true; + Rectangle r = getSize(graph, allLines, fm); + r = new Rectangle(r.width + 2 * PADDING_X, r.height + 2 * PADDING_Y); + overlayWidth = r.width; + overlayHeight = r.height; + + displayImage = ImageUtil.createAcceleratedImage(r.width, r.height); + Graphics intGraph = displayImage.getGraphics(); + UIUtil.setHighQualityRendering(intGraph); + + // draw a semi-transparent background rectangle + Color colorBG = GUIP.getPlanetaryConditionsColorBackground(); + intGraph.setColor(new Color(colorBG.getRed(), colorBG.getGreen(), colorBG.getBlue(), GUIP.getPlanetaryConditionsBackgroundTransparency())); + intGraph.fillRoundRect(0, 0, r.width, r.height, PADDING_X, PADDING_Y); + + // The coordinates to write the texts to + int x = PADDING_X; + int y = PADDING_Y + fm.getAscent(); + + // write the strings + for (String line : allLines) { + drawShadowedString(intGraph, line, x, y); + y += fm.getHeight(); + } } } - // draw the cached image to the boardview - // uses Composite to draw the image with variable transparency - int distSide = getDistSide(clipBounds, overlayWidth); - int distTop = getDistTop(clipBounds, overlayHeight); - + if (hasContents) { + // draw the cached image to the boardview + // uses Composite to draw the image with variable transparency + int distSide = getDistSide(clipBounds, overlayWidth); + int distTop = getDistTop(clipBounds, overlayHeight); - if (alpha < 1) { - // Save the former composite and set an alpha blending composite - Composite saveComp = ((Graphics2D) graph).getComposite(); - int type = AlphaComposite.SRC_OVER; - ((Graphics2D) graph).setComposite(AlphaComposite.getInstance(type, alpha)); - graph.drawImage(displayImage, clipBounds.x + distSide, clipBounds.y + distTop, null); - ((Graphics2D) graph).setComposite(saveComp); - } else { - graph.drawImage(displayImage, clipBounds.x + distSide, clipBounds.y + distTop, null); + if (alpha < 1) { + // Save the former composite and set an alpha blending composite + Composite saveComp = ((Graphics2D) graph).getComposite(); + int type = AlphaComposite.SRC_OVER; + ((Graphics2D) graph).setComposite(AlphaComposite.getInstance(type, alpha)); + graph.drawImage(displayImage, clipBounds.x + distSide, clipBounds.y + distTop, null); + ((Graphics2D) graph).setComposite(saveComp); + } else { + graph.drawImage(displayImage, clipBounds.x + distSide, clipBounds.y + distTop, null); + } } } @@ -193,7 +217,7 @@ private void drawShadowedString(Graphics graph, String s, int x, int y) { if (s.length() > 0) { AttributedString text = new AttributedString(s); - text.addAttribute(TextAttribute.FONT, new Font(FONT.getFontName(), Font.PLAIN, (int) (FONT.getSize() * GUIP.getGUIScale())), 0, s.length()); + text.addAttribute(TextAttribute.FONT, new Font(font.getFontName(), Font.PLAIN, (int) (font.getSize() * GUIP.getGUIScale())), 0, s.length()); graph.setColor(SHADOW_COLOR); graph.drawString(text.getIterator(), x + 1, y + 1); @@ -249,25 +273,32 @@ public boolean slide() { return false; } - /** Detects phase and turn changes to display Planetary Conditions. */ + /** Detects phase and turn changes to display*/ private GameListener gameListener = new GameListenerAdapter() { @Override public void gamePhaseChange(GamePhaseChangeEvent e) { currentPhase = e.getNewPhase(); - changed = true; + gameTurnOrPhaseChange(); } @Override public void gameTurnChange(GameTurnChangeEvent e) { // The active player has changed - changed = true; + gameTurnOrPhaseChange(); } }; @Override public void preferenceChange(PreferenceChangeEvent e) { // change on any preference change - changed = true; + } + + protected void setDirty() { + dirty = true; + } + + protected void gameTurnOrPhaseChange() { + setDirty(); } protected abstract void setVisibilityGUIPreference(boolean value); @@ -275,4 +306,5 @@ public void preferenceChange(PreferenceChangeEvent e) { protected abstract Color getTextColorGUIPreference(); protected abstract int getDistTop(Rectangle clipBounds, int overlayHeight); protected abstract int getDistSide(Rectangle clipBounds, int overlayWidth); + } diff --git a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java index c6f17d6e9f..a2f1e0871a 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java +++ b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java @@ -401,10 +401,10 @@ public class BoardView extends JPanel implements Scrollable, BoardListener, Mous /** Stores the correct tooltip dismiss delay so it can be restored when exiting the boardview */ private int dismissDelay = ToolTipManager.sharedInstance().getDismissDelay(); - /** A map overlay showing some important keybinds. */ + /** map overlays */ KeyBindingsOverlay keybindOverlay; - PlanetaryConditionsOverlay planetaryConditionsOverlay; + public TurnDetailsOverlay turnDetailsOverlay; /** The coords where the mouse was last. */ Coords lastCoords; @@ -443,6 +443,12 @@ public BoardView(final Game game, final MegaMekController controller, ClientGUI addDisplayable(planetaryConditionsOverlay); } + turnDetailsOverlay = new TurnDetailsOverlay(game, clientgui); + // Avoid showing the planetary Conditions when they can't be used (in the lobby map preview) + if (controller != null) { + addDisplayable(turnDetailsOverlay); + } + ourTask = scheduleRedrawTimer(); // call only once clearSprites(); addMouseListener(this); diff --git a/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java index 5b41f49bc3..04897fd4cd 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java @@ -84,25 +84,15 @@ public class KeyBindingsOverlay extends AbstractBoardViewOverlay { * for the current game situation. */ public KeyBindingsOverlay(Game game, ClientGUI cg) { - super(game, cg); + super(game, cg, new Font("SansSerif", Font.PLAIN, 13), + Messages.getString("KeyBindingsDisplay.heading", KeyCommandBind.getDesc(KeyCommandBind.KEY_BINDS)) ); } /** @return an ArrayList of all text lines to be shown. */ @Override protected List assembleTextLines() { List result = new ArrayList<>(); - - Color colorTitle = GUIP.getPlanetaryConditionsColorTitle(); - String toggleKey = KeyCommandBind.getDesc(KeyCommandBind.KEY_BINDS); - - String tmpStr = ""; - Boolean showHeading = GUIP.getPlanetaryConditionsShowHeader(); - String titleColor = String.format("#%02X%02X%02X", colorTitle.getRed(), colorTitle.getGreen(), colorTitle.getBlue()); - tmpStr = (showHeading ? titleColor + Messages.getString("KeyBindingsDisplay.heading", toggleKey) : ""); - - if (tmpStr.length() > 0) { - result.add(tmpStr); - } + addHeader(result); if (clientGui != null) { // In a game, not the Board Editor diff --git a/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java index 35b423370a..49a4c388ea 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java @@ -20,10 +20,9 @@ import megamek.common.Game; import megamek.common.PlanetaryConditions; +import java.awt.*; import java.text.MessageFormat; import java.util.ArrayList; -import java.awt.Color; -import java.awt.Rectangle; import java.util.List; /** @@ -50,29 +49,21 @@ public class PlanetaryConditionsOverlay extends AbstractBoardViewOverlay { * for the current game situation. */ public PlanetaryConditionsOverlay(Game game, ClientGUI cg) { - super(game, cg); + super(game, cg, new Font("SansSerif", Font.PLAIN, 13), + Messages.getString(MSG_HEADING, KeyCommandBind.getDesc(KeyCommandBind.PLANETARY_CONDITIONS)) ); } /** @return an ArrayList of all text lines to be shown. */ @Override protected List assembleTextLines() { List result = new ArrayList<>(); - - Color colorTitle = GUIP.getPlanetaryConditionsColorTitle(); + addHeader(result); +// Color colorTitle = GUIP.getPlanetaryConditionsColorTitle(); Color colorHot = GUIP.getPlanetaryConditionsColorHot(); Color colorCold = GUIP.getPlanetaryConditionsColorCold(); String toggleKey = KeyCommandBind.getDesc(KeyCommandBind.PLANETARY_CONDITIONS); - String tmpStr = ""; - Boolean showHeading = GUIP.getPlanetaryConditionsShowHeader(); - String titleColor = String.format("#%02X%02X%02X", colorTitle.getRed(), colorTitle.getGreen(), colorTitle.getBlue()); - tmpStr = (showHeading ? titleColor + MessageFormat.format(MSG_HEADING, toggleKey) : ""); - - if (tmpStr.length() > 0) { - result.add(tmpStr); - } - if (clientGui != null && !currentGame.getBoard().inSpace()) { // In a game, not the Board Editor @@ -91,6 +82,7 @@ protected List assembleTextLines() { Boolean showValue = GUIP.getPlanetaryConditionsShowValues(); Boolean showIndicator = GUIP.getPlanetaryConditionsShowIndicators(); + String tmpStr; if (((showDefaultConditions) || ((!showDefaultConditions) && (currentGame.getPlanetaryConditions().isExtremeTemperature())))) { tmpStr = (showLabel ? MSG_TEMPERATURE + " " : ""); diff --git a/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java index 69c6f4f072..69696a13b2 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java @@ -13,13 +13,14 @@ */ package megamek.client.ui.swing.boardview; +import megamek.client.ui.Messages; import megamek.client.ui.swing.ClientGUI; import megamek.client.ui.swing.GUIPreferences; +import megamek.client.ui.swing.util.KeyCommandBind; import megamek.common.Game; +import java.awt.*; import java.util.ArrayList; -import java.awt.Color; -import java.awt.Rectangle; import java.util.List; /** @@ -32,16 +33,36 @@ public class TurnDetailsOverlay extends AbstractBoardViewOverlay { /** * An overlay for the Boardview that shows details about a players turn */ + + List lines = new ArrayList<>(); + public TurnDetailsOverlay(Game game, ClientGUI cg) { - super(game, cg); + super(game, cg, new Font(Font.MONOSPACED, Font.PLAIN, 12), + Messages.getString("KeyBindingsDisplay.heading", KeyCommandBind.getDesc(KeyCommandBind.KEY_BINDS)) ); } /** @return an ArrayList of all text lines to be shown. */ @Override protected List assembleTextLines() { - List result = new ArrayList<>(); - return result; + return lines; + } + + public void setLines(List value) { + lines.clear();; + if (value != null) { +// addHeader(lines); + lines.addAll(value); + } + setDirty(); + } + + @Override + protected void gameTurnOrPhaseChange() { + if (!clientGui.getClient().isMyTurn()) { + lines.clear(); + } + super.gameTurnOrPhaseChange(); } @Override @@ -59,7 +80,7 @@ protected Color getTextColorGUIPreference() { @Override protected int getDistTop(Rectangle clipBounds, int overlayHeight) { - return clipBounds.height - (overlayHeight + 100); + return clipBounds.height - (overlayHeight + 5); } @Override From 795406f22ace596b101b913b0fb70b96bfbd536a Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Wed, 7 Jun 2023 20:35:22 -0700 Subject: [PATCH 05/13] added EntityActionLog --- .../i18n/megamek/client/messages.properties | 1 + .../client/ui/swing/AttackPhaseDisplay.java | 15 +- .../client/ui/swing/FiringDisplay.java | 7 +- .../client/ui/swing/PhysicalDisplay.java | 4 +- .../ui/swing/TargetingPhaseDisplay.java | 9 +- .../ui/swing/tooltip/EntityActionLog.java | 368 ++++++++++++++++++ 6 files changed, 378 insertions(+), 26 deletions(-) create mode 100644 megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index a090d80055..50ad87371e 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -392,6 +392,7 @@ BoardView1.push=Pushes. Needs {0} BoardView1.Rubble=Rubble BoardView1.Searchlight=Illuminates with searchlight. BoardView1.Smoke={0} smoke hex(es) in line of sight.\n +BoardView1.Spot=Spot {0} BoardView1.STUCK=STUCK BoardView1.STUNNED=STUNNED:{0} BoardView1.SWARMED=SWARMED diff --git a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java index 9b92784e07..87b7794d9b 100644 --- a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java @@ -18,6 +18,7 @@ */ package megamek.client.ui.swing; +import megamek.client.ui.swing.tooltip.EntityActionLog; import megamek.common.actions.*; import java.util.*; @@ -26,10 +27,11 @@ public abstract class AttackPhaseDisplay extends ActionPhaseDisplay { // client list of attacks user has input - protected Vector attacks; + protected EntityActionLog attacks; protected AttackPhaseDisplay(ClientGUI cg) { super(cg); + attacks = new EntityActionLog(clientgui.getClient()); } /** @@ -54,14 +56,7 @@ protected void updateDonePanel() // a torso twist alone should not trigger Done button updateDonePanelButtons(getDoneButtonLabel(), getSkipTurnButtonLabel(), false, null); } else { - ArrayList turnDetails = new ArrayList(); - for (EntityAction a : attacks) { -// turnDetails.add( String.format("%12s %8s (%2d%%)"), -// a.toString() -// ); - turnDetails.add(toString()); - } - updateDonePanelButtons(getDoneButtonLabel(), getSkipTurnButtonLabel(), true, turnDetails); + updateDonePanelButtons(getDoneButtonLabel(), getSkipTurnButtonLabel(), true, attacks.getDescriptions()); } } @@ -74,7 +69,7 @@ protected void removeAttack(Object o) /** removes all elements from the local temporary attack list */ protected void removeAllAttacks() { - attacks.removeAllElements(); + attacks.clear(); updateDonePanel(); } diff --git a/megamek/src/megamek/client/ui/swing/FiringDisplay.java b/megamek/src/megamek/client/ui/swing/FiringDisplay.java index c8e1bcef37..55a2a5d602 100644 --- a/megamek/src/megamek/client/ui/swing/FiringDisplay.java +++ b/megamek/src/megamek/client/ui/swing/FiringDisplay.java @@ -204,9 +204,6 @@ public FiringDisplay(final ClientGUI clientgui) { shiftheld = false; - // fire - attacks = new Vector<>(); - setupStatusBar(Messages.getString("FiringDisplay.waitingForFiringPhase")); setButtons(); @@ -1793,9 +1790,7 @@ protected void clearAttacks() { } // remove attacks, set weapons available again - Enumeration i = attacks.elements(); - while (i.hasMoreElements()) { - Object o = i.nextElement(); + for( EntityAction o : attacks) { if (o instanceof WeaponAttackAction) { WeaponAttackAction waa = (WeaponAttackAction) o; ce().getEquipment(waa.getWeaponId()).setUsedThisRound(false); diff --git a/megamek/src/megamek/client/ui/swing/PhysicalDisplay.java b/megamek/src/megamek/client/ui/swing/PhysicalDisplay.java index caa6e356f3..f72cb0ee3c 100644 --- a/megamek/src/megamek/client/ui/swing/PhysicalDisplay.java +++ b/megamek/src/megamek/client/ui/swing/PhysicalDisplay.java @@ -126,8 +126,6 @@ public PhysicalDisplay(ClientGUI clientgui) { clientgui.getBoardView().addBoardViewListener(this); setupStatusBar(Messages.getString("PhysicalDisplay.waitingForPhysicalAttackPhase")); - attacks = new Vector<>(); - setButtons(); setButtonsTooltips(); @@ -347,7 +345,7 @@ public void ready() { } } disableButtons(); - clientgui.getClient().sendAttackData(cen, attacks); + clientgui.getClient().sendAttackData(cen, attacks.toVector()); removeAllAttacks(); // close aimed shot display, if any ash.closeDialog(); diff --git a/megamek/src/megamek/client/ui/swing/TargetingPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/TargetingPhaseDisplay.java index e4ec9fca26..0c796aa414 100644 --- a/megamek/src/megamek/client/ui/swing/TargetingPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/TargetingPhaseDisplay.java @@ -182,9 +182,6 @@ public TargetingPhaseDisplay(final ClientGUI clientgui, boolean offboard) { phase = offboard ? GamePhase.OFFBOARD : GamePhase.TARGETING; shiftheld = false; - // fire - attacks = new Vector<>(); - setupStatusBar(Messages.getString("TargetingPhaseDisplay.waitingForTargetingPhase")); setButtons(); @@ -776,7 +773,7 @@ public void ready() { removeTempAttacks(); // send out attacks - clientgui.getClient().sendAttackData(cen, attacks); + clientgui.getClient().sendAttackData(cen, attacks.toVector()); // clear queue removeAllAttacks(); @@ -949,9 +946,7 @@ private void clearAttacks() { } // remove attacks, set weapons available again - Enumeration i = attacks.elements(); - while (i.hasMoreElements()) { - Object o = i.nextElement(); + for( EntityAction o : attacks) { if (o instanceof WeaponAttackAction) { WeaponAttackAction waa = (WeaponAttackAction) o; ce().getEquipment(waa.getWeaponId()).setUsedThisRound(false); diff --git a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java new file mode 100644 index 0000000000..2e7107f43f --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java @@ -0,0 +1,368 @@ +package megamek.client.ui.swing.tooltip; + +import megamek.client.Client; +import megamek.client.ui.Messages; +import megamek.common.*; +import megamek.common.actions.*; + +import java.util.*; +import java.util.stream.Stream; + +/** + * An ordered List-like collection of EntityActions with a cached description of each action, + * created as the action is added. + */ +public class EntityActionLog implements Collection { + Game game; + Client client; + + protected ArrayList actions = new ArrayList(); + // need to cache to hit values since it goes to IMPOSSIBLE after action placed in attacks list + HashMap cache = new HashMap(); + ArrayList descriptions = new ArrayList<>(); + + public EntityActionLog(Client client) { + this.client = client; + this.game = client.getGame(); + } + + /** @return a list of descrition strings. Note that there may be fewer than the number of actions + * as similar actions are summarized in a single entry + */ + public List getDescriptions() { + return descriptions; + } + + void rebuild() { + descriptions.clear(); + for (EntityAction entityAction : cache.keySet()) { + addDescription(entityAction); + } + } + + /** + * @return a clone of the internal Vector of EntityActions + */ + public Vector toVector() { + return new Vector<>(actions); + } + + /** + * remove all items from collection + */ + @Override + public void clear() { + cache.clear(); + descriptions.clear(); + } + + @Override + public boolean add(EntityAction entityAction) { + if (!actions.add(entityAction)) { + return false; + } + addDescription(entityAction); + return true; + } + + public void add(int index, EntityAction entityAction) { + actions.add(index, entityAction); + addDescription(entityAction); + } + + /** + * Remove an item and its description cache + * @param entityAction + */ + @Override + public boolean remove(Object o) { + if (!actions.remove(o)) { + return false; + } + cache.remove(o); + rebuild(); + return true; + } + + @Override + public boolean containsAll(Collection c) { + return actions.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + if (!actions.addAll(c)) { + return false; + } + for (var a : c) { + addDescription(a); + } + return true; + } + + @Override + public boolean removeAll(Collection c) { + if (!actions.removeAll(c)) { + return false; + } + for (var a : c) { + cache.remove(a); + } + rebuild(); + return true; + } + + @Override + public boolean retainAll(Collection c) { + return actions.retainAll(c); + } + + public EntityAction firstElement() { + return actions.isEmpty() ? null : actions.get(0); + } + + public EntityAction lastElement() { + return actions.isEmpty() ? null : actions.get(actions.size()-1); + } + + @Override + public boolean isEmpty() { + return actions.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return actions.contains(o); + } + + void addDescription(EntityAction entityAction) { + if (entityAction instanceof WeaponAttackAction) { + addWeapon((WeaponAttackAction) entityAction); + } else if (entityAction instanceof KickAttackAction) { + addWeapon((KickAttackAction) entityAction); + } else if (entityAction instanceof PunchAttackAction) { + addWeapon((PunchAttackAction) entityAction); + } else if (entityAction instanceof PushAttackAction) { + addWeapon((PushAttackAction) entityAction); + } else if (entityAction instanceof ClubAttackAction) { + addWeapon((ClubAttackAction) entityAction); + } else if (entityAction instanceof ChargeAttackAction) { + addWeapon((ChargeAttackAction) entityAction); + } else if (entityAction instanceof DfaAttackAction) { + addWeapon((DfaAttackAction) entityAction); + } else if (entityAction instanceof ProtomechPhysicalAttackAction) { + addWeapon((ProtomechPhysicalAttackAction) entityAction); + } else if (entityAction instanceof SearchlightAttackAction) { + addWeapon((SearchlightAttackAction) entityAction); + } else if (entityAction instanceof SpotAction) { + addWeapon((SpotAction) entityAction); + } else { + cache.put(entityAction, ""); + descriptions.add(entityAction.toDisplayableString(client)); + } + } + + /** + * Adds a weapon to this attack + */ + void addWeapon(WeaponAttackAction attack) { + final Entity entity = game.getEntity(attack.getEntityId()); + final WeaponType wtype = (WeaponType) entity.getEquipment(attack.getWeaponId()).getType(); + + final String roll; + if (cache.containsKey(attack)) { + roll = cache.get(attack); + } else { + roll = attack.toHit(game).getValueAsString(); + cache.put(attack, roll); + } + + String table = attack.toHit(game).getTableDesc(); + if (!table.isEmpty()) { + table = " " + table; + } + boolean b = false; + ListIterator i = descriptions.listIterator(); + while (i.hasNext()) { + String s = i.next(); + if (s.contains(wtype.getName())) { + i.set(s + ", " + roll + table); + b = true; + } + } + + if (!b) { + descriptions.add(wtype.getName() + Messages.getString("BoardView1.needs") + roll + table); + } + } + + void addWeapon(KickAttackAction attack) { + String buffer; + if (cache.containsKey(attack)) { + buffer = cache.get(attack); + } else { + String rollLeft; + String rollRight; + final int leg = attack.getLeg(); + switch (leg) { + case KickAttackAction.BOTH: + rollLeft = KickAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), KickAttackAction.LEFT).getValueAsString(); + rollRight = KickAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), KickAttackAction.RIGHT).getValueAsString(); + buffer = Messages.getString("BoardView1.kickBoth", rollLeft, rollRight); + break; + case KickAttackAction.LEFT: + rollLeft = KickAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), KickAttackAction.LEFT).getValueAsString(); + buffer = Messages.getString("BoardView1.kickLeft", rollLeft); + break; + case KickAttackAction.RIGHT: + rollRight = KickAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), KickAttackAction.RIGHT).getValueAsString(); + buffer = Messages.getString("BoardView1.kickRight", rollRight); + break; + default: + buffer = "Error on kick action"; + } + cache.put(attack, buffer); + } + descriptions.add(buffer); + } + + void addWeapon(PunchAttackAction attack) { + String buffer; + if (cache.containsKey(attack)) { + buffer = cache.get(attack); + } else { + String rollLeft; + String rollRight; + final int arm = attack.getArm(); + switch (arm) { + case PunchAttackAction.BOTH: + rollLeft = PunchAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), PunchAttackAction.LEFT, false).getValueAsString(); + rollRight = PunchAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), PunchAttackAction.RIGHT, false).getValueAsString(); + buffer = Messages.getString("BoardView1.punchBoth", rollLeft, rollRight); + break; + case PunchAttackAction.LEFT: + rollLeft = PunchAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), PunchAttackAction.LEFT, false).getValueAsString(); + buffer = Messages.getString("BoardView1.punchLeft", rollLeft); + break; + case PunchAttackAction.RIGHT: + rollRight = PunchAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), PunchAttackAction.RIGHT, false).getValueAsString(); + buffer = Messages.getString("BoardView1.punchRight", rollRight); + break; + default: + buffer = "Error on punch action"; + } + cache.put(attack, buffer); + } + descriptions.add(buffer); + } + + void addWeapon(PushAttackAction attack) { + String buffer; + if (cache.containsKey(attack)) { + buffer = cache.get(attack); + } else { + final String roll = attack.toHit(game).getValueAsString(); + buffer = Messages.getString("BoardView1.push", roll); + cache.put(attack, buffer); + } + descriptions.add(buffer); + } + + void addWeapon(ClubAttackAction attack) { + String buffer; + if (cache.containsKey(attack)) { + buffer = cache.get(attack); + } else { + final String roll = attack.toHit(game).getValueAsString(); + final String club = attack.getClub().getName(); + buffer = Messages.getString("BoardView1.hit", club, roll); + cache.put(attack, buffer); + } + descriptions.add(buffer); + } + + void addWeapon(ChargeAttackAction attack) { + String buffer; + if (cache.containsKey(attack)) { + buffer = cache.get(attack); + } else { + final String roll = attack.toHit(game).getValueAsString(); + buffer = Messages.getString("BoardView1.charge", roll); + cache.put(attack, buffer); + } + descriptions.add(buffer); + } + + void addWeapon(DfaAttackAction attack) { + String buffer; + if (cache.containsKey(attack)) { + buffer = cache.get(attack); + } else { + final String roll = attack.toHit(game).getValueAsString(); + buffer = Messages.getString("BoardView1.DFA", roll); + cache.put(attack, buffer); + } + descriptions.add(buffer); + } + + void addWeapon(ProtomechPhysicalAttackAction attack) { + String buffer; + if (cache.containsKey(attack)) { + buffer = cache.get(attack); + } else { + final String roll = attack.toHit(game).getValueAsString(); + buffer = Messages.getString("BoardView1.proto", roll); + cache.put(attack, buffer); + } + descriptions.add(buffer); + } + + void addWeapon(SearchlightAttackAction attack) { + String buffer; + if (cache.containsKey(attack)) { + buffer = cache.get(attack); + } else { + buffer = Messages.getString("BoardView1.Searchlight"); + cache.put(attack, buffer); + } + descriptions.add(buffer); + } + + void addWeapon(SpotAction attack) { + String buffer; + Entity target = game.getEntity(attack.getTargetId()); + + if (cache.containsKey(attack)) { + buffer = cache.get(attack); + } else { + buffer = Messages.getString("BoardView1.Spot", (target != null) ? target.getShortName() : "" ); + cache.put(attack, buffer); + } + descriptions.add(buffer); + } + + @Override + public int size() { + return actions.size(); + } + + @Override + public Iterator iterator() { + return actions.iterator(); + } + + @Override + public Object[] toArray() { + return new Object[0]; + } + + @Override + public T[] toArray(T[] a) { + return actions.toArray(a); + } + + @Override + public Stream stream() { + return actions.stream(); + } +} From ea3dc0fe555bd0d5d6828985a8b803d58effaa0d Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Thu, 8 Jun 2023 10:59:19 -0700 Subject: [PATCH 06/13] use EntityActionLog in AttackSprite --- .../client/ui/swing/AttackPhaseDisplay.java | 2 +- .../ui/swing/boardview/AttackSprite.java | 188 +----------------- .../client/ui/swing/boardview/BoardView.java | 41 +--- .../ui/swing/tooltip/EntityActionLog.java | 100 +++++----- 4 files changed, 67 insertions(+), 264 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java index 87b7794d9b..a791be2ada 100644 --- a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java @@ -31,7 +31,7 @@ public abstract class AttackPhaseDisplay extends ActionPhaseDisplay { protected AttackPhaseDisplay(ClientGUI cg) { super(cg); - attacks = new EntityActionLog(clientgui.getClient()); + attacks = new EntityActionLog(clientgui.getClient(), true); } /** diff --git a/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java b/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java index 01d3c1fde9..abb1195ab3 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java +++ b/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java @@ -11,18 +11,10 @@ import java.util.ListIterator; import megamek.client.ui.Messages; +import megamek.client.ui.swing.tooltip.EntityActionLog; import megamek.client.ui.swing.util.StraightArrowPolygon; import megamek.common.*; -import megamek.common.actions.AttackAction; -import megamek.common.actions.ChargeAttackAction; -import megamek.common.actions.ClubAttackAction; -import megamek.common.actions.DfaAttackAction; -import megamek.common.actions.KickAttackAction; -import megamek.common.actions.ProtomechPhysicalAttackAction; -import megamek.common.actions.PunchAttackAction; -import megamek.common.actions.PushAttackAction; -import megamek.common.actions.SearchlightAttackAction; -import megamek.common.actions.WeaponAttackAction; +import megamek.common.actions.*; import megamek.common.enums.GamePhase; import static megamek.client.ui.swing.util.UIUtil.guiScaledFontHTML; @@ -56,7 +48,7 @@ class AttackSprite extends Sprite { private String targetDesc; - ArrayList weaponDescs = new ArrayList<>(); + EntityActionLog weaponDescs; private final Entity ae; @@ -70,6 +62,7 @@ class AttackSprite extends Sprite { public AttackSprite(BoardView boardView1, final AttackAction attack) { super(boardView1); + weaponDescs = new EntityActionLog(boardView1.clientgui.getClient(), false); this.boardView1 = boardView1; entityId = attack.getEntityId(); targetType = attack.getTargetType(); @@ -106,38 +99,16 @@ public AttackSprite(BoardView boardView1, final AttackAction attack) { // set names & stuff attackerDesc = ae.getDisplayName(); targetDesc = target.getDisplayName(); - if (attack instanceof WeaponAttackAction) { - addWeapon((WeaponAttackAction) attack); - } - if (attack instanceof KickAttackAction) { - addWeapon((KickAttackAction) attack); - } - if (attack instanceof PunchAttackAction) { - addWeapon((PunchAttackAction) attack); - } - if (attack instanceof PushAttackAction) { - addWeapon((PushAttackAction) attack); - } - if (attack instanceof ClubAttackAction) { - addWeapon((ClubAttackAction) attack); - } - if (attack instanceof ChargeAttackAction) { - addWeapon((ChargeAttackAction) attack); - } - if (attack instanceof DfaAttackAction) { - addWeapon((DfaAttackAction) attack); - } - if (attack instanceof ProtomechPhysicalAttackAction) { - addWeapon((ProtomechPhysicalAttackAction) attack); - } - if (attack instanceof SearchlightAttackAction) { - addWeapon((SearchlightAttackAction) attack); - } + addEntityAction(attack); // nullify image image = null; } + public void addEntityAction(EntityAction entityAction) { + weaponDescs.add(entityAction); + } + private void makePoly() { // make a polygon a = this.boardView1.getHexLocation(ae.getPosition()); @@ -265,145 +236,6 @@ public int getTargetId() { return targetId; } - /** - * Adds a weapon to this attack - */ - public void addWeapon(WeaponAttackAction attack) { - final Entity entity = this.boardView1.game.getEntity(attack.getEntityId()); - final WeaponType wtype = (WeaponType) entity.getEquipment( - attack.getWeaponId()).getType(); - final String roll = attack.toHit(this.boardView1.game).getValueAsString(); - String table = attack.toHit(this.boardView1.game).getTableDesc(); - if (!table.isEmpty()) { - table = " " + table; - } - boolean b = false; - ListIterator i = weaponDescs.listIterator(); - while (i.hasNext()) { - String s = i.next(); - if (s.contains(wtype.getName())) { - i.set(s + ", " + roll + table); - b = true; - } - } - - if (!b) { - weaponDescs.add(wtype.getName() + Messages.getString("BoardView1.needs") + roll + table); - } - } - - public void addWeapon(KickAttackAction attack) { - String bufer = ""; - String rollLeft = ""; - String rollRight = ""; - final int leg = attack.getLeg(); - switch (leg) { - case KickAttackAction.BOTH: - rollLeft = KickAttackAction.toHit( - this.boardView1.game, - attack.getEntityId(), - this.boardView1.game.getTarget(attack.getTargetType(), - attack.getTargetId()), - KickAttackAction.LEFT).getValueAsString(); - rollRight = KickAttackAction.toHit( - this.boardView1.game, - attack.getEntityId(), - this.boardView1.game.getTarget(attack.getTargetType(), - attack.getTargetId()), - KickAttackAction.RIGHT).getValueAsString(); - bufer = Messages.getString("BoardView1.kickBoth", rollLeft, rollRight); - break; - case KickAttackAction.LEFT: - rollLeft = KickAttackAction.toHit( - this.boardView1.game, - attack.getEntityId(), - this.boardView1.game.getTarget(attack.getTargetType(), - attack.getTargetId()), - KickAttackAction.LEFT).getValueAsString(); - bufer = Messages.getString("BoardView1.kickLeft", rollLeft); - break; - case KickAttackAction.RIGHT: - rollRight = KickAttackAction.toHit( - this.boardView1.game, - attack.getEntityId(), - this.boardView1.game.getTarget(attack.getTargetType(), - attack.getTargetId()), - KickAttackAction.RIGHT).getValueAsString(); - bufer = Messages.getString("BoardView1.kickRight", rollRight); - break; - } - weaponDescs.add(bufer); - } - - public void addWeapon(PunchAttackAction attack) { - String bufer = ""; - String rollLeft; - String rollRight; - final int arm = attack.getArm(); - switch (arm) { - case PunchAttackAction.BOTH: - rollLeft = PunchAttackAction.toHit( - this.boardView1.game, - attack.getEntityId(), - this.boardView1.game.getTarget(attack.getTargetType(), attack.getTargetId()), - PunchAttackAction.LEFT, false).getValueAsString(); - rollRight = PunchAttackAction.toHit( - this.boardView1.game, - attack.getEntityId(), - this.boardView1.game.getTarget(attack.getTargetType(), attack.getTargetId()), - PunchAttackAction.RIGHT, false).getValueAsString(); - bufer = Messages.getString("BoardView1.punchBoth", rollLeft, rollRight); - break; - case PunchAttackAction.LEFT: - rollLeft = PunchAttackAction.toHit( - this.boardView1.game, - attack.getEntityId(), - this.boardView1.game.getTarget(attack.getTargetType(), attack.getTargetId()), - PunchAttackAction.LEFT, false).getValueAsString(); - bufer = Messages.getString("BoardView1.punchLeft", rollLeft); - break; - case PunchAttackAction.RIGHT: - rollRight = PunchAttackAction.toHit( - this.boardView1.game, - attack.getEntityId(), - this.boardView1.game.getTarget(attack.getTargetType(), attack.getTargetId()), - PunchAttackAction.RIGHT, false).getValueAsString(); - bufer = Messages.getString("BoardView1.punchRight", rollRight); - break; - } - weaponDescs.add(bufer); - } - - public void addWeapon(PushAttackAction attack) { - final String roll = attack.toHit(this.boardView1.game).getValueAsString(); - weaponDescs.add(Messages.getString("BoardView1.push", roll)); - } - - public void addWeapon(ClubAttackAction attack) { - final String roll = attack.toHit(this.boardView1.game).getValueAsString(); - final String club = attack.getClub().getName(); - weaponDescs.add(Messages.getString("BoardView1.hit", club, roll)); - } - - public void addWeapon(ChargeAttackAction attack) { - final String roll = attack.toHit(this.boardView1.game).getValueAsString(); - weaponDescs.add(Messages.getString("BoardView1.charge", roll)); - } - - public void addWeapon(DfaAttackAction attack) { - final String roll = attack.toHit(this.boardView1.game).getValueAsString(); - weaponDescs.add(Messages.getString("BoardView1.DFA", roll)); - } - - public void addWeapon(ProtomechPhysicalAttackAction attack) { - final String roll = attack.toHit(this.boardView1.game).getValueAsString(); - weaponDescs.add(Messages.getString("BoardView1.proto", roll)); - } - - public void addWeapon(SearchlightAttackAction attack) { - weaponDescs.add(Messages.getString("BoardView1.Searchlight")); - } - @Override public StringBuffer getTooltip() { GamePhase phase = this.boardView1.game.getPhase(); @@ -414,7 +246,7 @@ public StringBuffer getTooltip() { result = guiScaledFontHTML(attackColor) + sAttacherDesc + ""; String sAttacks = ""; if ((phase.isFiring()) || (phase.isPhysical())) { - for (String wpD : weaponDescs) { + for (String wpD : weaponDescs.getDescriptions()) { sAttacks += "
" + wpD; } result += guiScaledFontHTML(uiBlack()) + sAttacks + ""; diff --git a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java index a2f1e0871a..db1aab896c 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java +++ b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java @@ -4197,46 +4197,7 @@ public synchronized void addAttack(AttackAction aa) { if ((sprite.getEntityId() == aa.getEntityId()) && (sprite.getTargetId() == aa.getTargetId())) { // use existing attack, but add this weapon - if (aa instanceof WeaponAttackAction) { - WeaponAttackAction waa = (WeaponAttackAction) aa; - if (aa.getTargetType() != Targetable.TYPE_HEX_ARTILLERY) { - sprite.addWeapon(waa); - } else if (waa.getEntity(game).getOwner().getId() == localPlayer.getId()) { - sprite.addWeapon(waa); - } - } - - if (aa instanceof KickAttackAction) { - sprite.addWeapon((KickAttackAction) aa); - } - - if (aa instanceof PunchAttackAction) { - sprite.addWeapon((PunchAttackAction) aa); - } - - if (aa instanceof PushAttackAction) { - sprite.addWeapon((PushAttackAction) aa); - } - - if (aa instanceof ClubAttackAction) { - sprite.addWeapon((ClubAttackAction) aa); - } - - if (aa instanceof ChargeAttackAction) { - sprite.addWeapon((ChargeAttackAction) aa); - } - - if (aa instanceof DfaAttackAction) { - sprite.addWeapon((DfaAttackAction) aa); - } - - if (aa instanceof ProtomechPhysicalAttackAction) { - sprite.addWeapon((ProtomechPhysicalAttackAction) aa); - } - - if (aa instanceof SearchlightAttackAction) { - sprite.addWeapon((SearchlightAttackAction) aa); - } + sprite.addEntityAction(aa); return; } } diff --git a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java index 2e7107f43f..9c1d5dac12 100644 --- a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java +++ b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java @@ -13,17 +13,22 @@ * created as the action is added. */ public class EntityActionLog implements Collection { - Game game; - Client client; + final Game game; + final Client client; - protected ArrayList actions = new ArrayList(); + final protected ArrayList actions = new ArrayList(); // need to cache to hit values since it goes to IMPOSSIBLE after action placed in attacks list - HashMap cache = new HashMap(); - ArrayList descriptions = new ArrayList<>(); + final HashMap infoCache = new HashMap(); + final HashMap targetCache = new HashMap(); - public EntityActionLog(Client client) { + final ArrayList descriptions = new ArrayList<>(); + + final boolean showTarget; + + public EntityActionLog(Client client, boolean showTarget) { this.client = client; this.game = client.getGame(); + this.showTarget = showTarget; } /** @return a list of descrition strings. Note that there may be fewer than the number of actions @@ -35,7 +40,7 @@ public List getDescriptions() { void rebuild() { descriptions.clear(); - for (EntityAction entityAction : cache.keySet()) { + for (EntityAction entityAction : infoCache.keySet()) { addDescription(entityAction); } } @@ -52,7 +57,7 @@ public Vector toVector() { */ @Override public void clear() { - cache.clear(); + infoCache.clear(); descriptions.clear(); } @@ -79,7 +84,7 @@ public boolean remove(Object o) { if (!actions.remove(o)) { return false; } - cache.remove(o); + infoCache.remove(o); rebuild(); return true; } @@ -106,7 +111,7 @@ public boolean removeAll(Collection c) { return false; } for (var a : c) { - cache.remove(a); + infoCache.remove(a); } rebuild(); return true; @@ -157,7 +162,7 @@ void addDescription(EntityAction entityAction) { } else if (entityAction instanceof SpotAction) { addWeapon((SpotAction) entityAction); } else { - cache.put(entityAction, ""); + infoCache.put(entityAction, ""); descriptions.add(entityAction.toDisplayableString(client)); } } @@ -168,38 +173,43 @@ void addDescription(EntityAction entityAction) { void addWeapon(WeaponAttackAction attack) { final Entity entity = game.getEntity(attack.getEntityId()); final WeaponType wtype = (WeaponType) entity.getEquipment(attack.getWeaponId()).getType(); + final Targetable target = attack.getTarget(game); final String roll; - if (cache.containsKey(attack)) { - roll = cache.get(attack); + if (infoCache.containsKey(attack)) { + roll = infoCache.get(attack); } else { roll = attack.toHit(game).getValueAsString(); - cache.put(attack, roll); + infoCache.put(attack, roll); + targetCache.put(attack, target); } String table = attack.toHit(game).getTableDesc(); if (!table.isEmpty()) { table = " " + table; } - boolean b = false; + + //add to an existing entry if possible + boolean found = false; ListIterator i = descriptions.listIterator(); while (i.hasNext()) { String s = i.next(); - if (s.contains(wtype.getName())) { + if (s.startsWith(wtype.getName())) { i.set(s + ", " + roll + table); - b = true; + found = true; + break; } } - if (!b) { + if (!found) { descriptions.add(wtype.getName() + Messages.getString("BoardView1.needs") + roll + table); } } void addWeapon(KickAttackAction attack) { String buffer; - if (cache.containsKey(attack)) { - buffer = cache.get(attack); + if (infoCache.containsKey(attack)) { + buffer = infoCache.get(attack); } else { String rollLeft; String rollRight; @@ -221,15 +231,15 @@ void addWeapon(KickAttackAction attack) { default: buffer = "Error on kick action"; } - cache.put(attack, buffer); + infoCache.put(attack, buffer); } descriptions.add(buffer); } void addWeapon(PunchAttackAction attack) { String buffer; - if (cache.containsKey(attack)) { - buffer = cache.get(attack); + if (infoCache.containsKey(attack)) { + buffer = infoCache.get(attack); } else { String rollLeft; String rollRight; @@ -251,79 +261,79 @@ void addWeapon(PunchAttackAction attack) { default: buffer = "Error on punch action"; } - cache.put(attack, buffer); + infoCache.put(attack, buffer); } descriptions.add(buffer); } void addWeapon(PushAttackAction attack) { String buffer; - if (cache.containsKey(attack)) { - buffer = cache.get(attack); + if (infoCache.containsKey(attack)) { + buffer = infoCache.get(attack); } else { final String roll = attack.toHit(game).getValueAsString(); buffer = Messages.getString("BoardView1.push", roll); - cache.put(attack, buffer); + infoCache.put(attack, buffer); } descriptions.add(buffer); } void addWeapon(ClubAttackAction attack) { String buffer; - if (cache.containsKey(attack)) { - buffer = cache.get(attack); + if (infoCache.containsKey(attack)) { + buffer = infoCache.get(attack); } else { final String roll = attack.toHit(game).getValueAsString(); final String club = attack.getClub().getName(); buffer = Messages.getString("BoardView1.hit", club, roll); - cache.put(attack, buffer); + infoCache.put(attack, buffer); } descriptions.add(buffer); } void addWeapon(ChargeAttackAction attack) { String buffer; - if (cache.containsKey(attack)) { - buffer = cache.get(attack); + if (infoCache.containsKey(attack)) { + buffer = infoCache.get(attack); } else { final String roll = attack.toHit(game).getValueAsString(); buffer = Messages.getString("BoardView1.charge", roll); - cache.put(attack, buffer); + infoCache.put(attack, buffer); } descriptions.add(buffer); } void addWeapon(DfaAttackAction attack) { String buffer; - if (cache.containsKey(attack)) { - buffer = cache.get(attack); + if (infoCache.containsKey(attack)) { + buffer = infoCache.get(attack); } else { final String roll = attack.toHit(game).getValueAsString(); buffer = Messages.getString("BoardView1.DFA", roll); - cache.put(attack, buffer); + infoCache.put(attack, buffer); } descriptions.add(buffer); } void addWeapon(ProtomechPhysicalAttackAction attack) { String buffer; - if (cache.containsKey(attack)) { - buffer = cache.get(attack); + if (infoCache.containsKey(attack)) { + buffer = infoCache.get(attack); } else { final String roll = attack.toHit(game).getValueAsString(); buffer = Messages.getString("BoardView1.proto", roll); - cache.put(attack, buffer); + infoCache.put(attack, buffer); } descriptions.add(buffer); } void addWeapon(SearchlightAttackAction attack) { String buffer; - if (cache.containsKey(attack)) { - buffer = cache.get(attack); + if (infoCache.containsKey(attack)) { + buffer = infoCache.get(attack); } else { buffer = Messages.getString("BoardView1.Searchlight"); - cache.put(attack, buffer); + infoCache.put(attack, buffer); } descriptions.add(buffer); } @@ -332,11 +342,11 @@ void addWeapon(SpotAction attack) { String buffer; Entity target = game.getEntity(attack.getTargetId()); - if (cache.containsKey(attack)) { - buffer = cache.get(attack); + if (infoCache.containsKey(attack)) { + buffer = infoCache.get(attack); } else { buffer = Messages.getString("BoardView1.Spot", (target != null) ? target.getShortName() : "" ); - cache.put(attack, buffer); + infoCache.put(attack, buffer); } descriptions.add(buffer); } From 92d131ca4fe9f6b977a570fe993c0bd0c9f38b45 Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Fri, 9 Jun 2023 17:30:36 -0700 Subject: [PATCH 07/13] Handle unknonw action with clean type name. rename properties to avoid some confusion --- .../i18n/megamek/client/messages.properties | 18 ++--- .../megamek/client/messages_de.properties | 14 ++-- .../megamek/client/messages_es.properties | 16 ++-- .../megamek/client/messages_ru.properties | 16 ++-- .../ui/swing/tooltip/EntityActionLog.java | 73 +++++++++++-------- 5 files changed, 75 insertions(+), 62 deletions(-) diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 0fc972f5d6..0ef26d2461 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -311,17 +311,17 @@ BoardView1.ConversionMode.TRACKED=Tracked BoardView1.ConversionMode.WHEELED=Wheeled BoardView1.ConversionMode.WIGE=AirMech BoardView1.ConversionMode.AERODYNE=Fighter -BoardView1.charge=Charges. Needs {0} -BoardView1.charge1=(Charging) +BoardView1.ChargeAttackAction=Charges. Needs {0} +BoardView1.ChargeAttackAction1=(Charging) BoardView1.CrewDead=CREW DEAD -BoardView1.DFA1=(Executing DFA) +BoardView1.DfaAttackAction1=(Executing DFA) BoardView1.Drop=Drop BoardView1.Command-=Command- BoardView1.Command=Command BoardView1.Conventional=Conv ( BoardView1.DEPTH=DEPTH BoardView1.detonated=detonated ( -BoardView1.DFA=DFA. Needs {0} +BoardView1.DfaAttackAction=DFA. Needs {0} BoardView1.Disconnect=Disconnect BoardView1.ecmSource=ECM Source BoardView1.eccmSource=ECCM Source @@ -340,7 +340,7 @@ BoardView1.Height=Height BoardView1.HEIGHT=HEIGHT BoardView1.Hex=Hex BoardView1.HIDDEN=HIDDEN -BoardView1.hit=Hits with {0}. Needs {1} +BoardView1.ClubAttackAction=Hits with {0}. Needs {1} BoardView1.Hover=Hover BoardView1.ID=\ ID: BoardView1.IMMOBILE=IMMOBILE @@ -384,15 +384,15 @@ BoardView1.on=on BoardView1.pilot=\ Pilot BoardView1.PRONE=PRONE BoardView1.NO_GYRO=NO GYRO -BoardView1.proto=Makes a protomech frenzy attack. Needs {0} +BoardView1.ProtomechPhysicalAttackAction=Makes a protomech frenzy attack. Needs {0} BoardView1.punchBoth=Punches with both arms. Left needs {0}; Right needs {1} BoardView1.punchLeft=Punches with left arm. Needs {0} BoardView1.punchRight=Punches with right arm. Needs {0} -BoardView1.push=Pushes. Needs {0} +BoardView1.PushAttackAction=Pushes. Needs {0} BoardView1.Rubble=Rubble -BoardView1.Searchlight=Illuminates with searchlight. +BoardView1.SearchlightAttackAction=Illuminates with searchlight. BoardView1.Smoke={0} smoke hex(es) in line of sight.\n -BoardView1.Spot=Spot {0} +BoardView1.SpotAction=Spot {0} BoardView1.STUCK=STUCK BoardView1.STUNNED=STUNNED:{0} BoardView1.SWARMED=SWARMED diff --git a/megamek/i18n/megamek/client/messages_de.properties b/megamek/i18n/megamek/client/messages_de.properties index 5574ecce8a..a1706ad949 100644 --- a/megamek/i18n/megamek/client/messages_de.properties +++ b/megamek/i18n/megamek/client/messages_de.properties @@ -100,21 +100,21 @@ BoardView1.Armor=Panzerung BoardView1.Attacker=Angreifer({0}) hex ist {1}.\n BoardView1.AttackerPartialCover=Angreifer hat {0} Teilweise Deckung.\n BoardView1.CF=, KF: -BoardView1.charge=Rammt. Benötigt {0} -BoardView1.charge1=(Rammangriff) -BoardView1.DFA1=(Todessprung) +BoardView1.ChargeAttackAction=Rammt. Benötigt {0} +BoardView1.ChargeAttackAction1=(Rammangriff) +BoardView1.DfaAttackAction1=(Todessprung) BoardView1.Command-=Kommando- BoardView1.Conventional=Konventionell BoardView1.DEPTH=TIEFE BoardView1.detonated=detoniert -BoardView1.DFA=Todessprung. Benötigt {0} +BoardView1.DfaAttackAction=Todessprung. Benötigt {0} BoardView1.Heat=\ Hitze BoardView1.HeavySmoke={0} Feld(er) mit dichtem Rauch in der Sichtlinie.\n BoardView1.HeavyWoods={0} Feld(er) mit dichtem Wald in der Sichtlinie.\n BoardView1.Height=Höhe BoardView1.HEIGHT=H\u00D6HE BoardView1.Hex=Hex -BoardView1.hit=Trifft mit {0}. Benötigt {1} +BoardView1.ClubAttackAction=Trifft mit {0}. Benötigt {1} BoardView1.ID=\ ID: BoardView1.IMMOBILE=IMMOBIL BoardView1.internal=; Intern @@ -140,11 +140,11 @@ BoardView1.NonMech=Nicht-Mech BoardView1.on=auf BoardView1.pilot=\ Pilot BoardView1.PRONE=LIEGT -BoardView1.proto=Führt Protomech Nahkampfattacke durch. Benötigt {0} +BoardView1.ProtomechPhysicalAttackAction=Führt Protomech Nahkampfattacke durch. Benötigt {0} BoardView1.punchBoth=Schlägt mit beiden Armen. Linker Arm benötigt {0}; Rechter Arm benötigt {1} BoardView1.punchLeft=Schlägt mit dem linken Arm. Benötigt {0} BoardView1.punchRight=Schlägt mit dem rechten Arm. Benötigt {0} -BoardView1.push=Schubst. Benötigt {0} +BoardView1.PushAttackAction=Schubst. Benötigt {0} BoardView1.Rubble=Unrat BoardView1.Smoke={0} Rauchfeld(er) in der Sichtlinie\n BoardView1.STUCK=STECKT diff --git a/megamek/i18n/megamek/client/messages_es.properties b/megamek/i18n/megamek/client/messages_es.properties index 284272a96b..527594bdfc 100644 --- a/megamek/i18n/megamek/client/messages_es.properties +++ b/megamek/i18n/megamek/client/messages_es.properties @@ -381,17 +381,17 @@ BoardView1.ConversionMode.TRACKED=Con orugas BoardView1.ConversionMode.WHEELED=Con ruedas BoardView1.ConversionMode.WIGE=AirMech BoardView1.ConversionMode.AERODYNE=Caza -BoardView1.charge=Cargas. Necesita {0} -BoardView1.charge1=(Cargando) +BoardView1.ChargeAttackAction=Cargas. Necesita {0} +BoardView1.ChargeAttackAction1=(Cargando) BoardView1.CrewDead=EQUIPO MUERTO -BoardView1.DFA1=(Ejecutando MDC) +BoardView1.DfaAttackAction1=(Ejecutando MDC) BoardView1.Drop=Soltar BoardView1.Command-=Comando- BoardView1.Command=Comando BoardView1.Conventional=Conv ( BoardView1.DEPTH=PROF. BoardView1.detonated=detonado ( -BoardView1.DFA=MDC. Necesita {0} +BoardView1.DfaAttackAction=MDC. Necesita {0} BoardView1.Disconnect=Desconectar BoardView1.ecmSource=Fuente ECM BoardView1.eccmSource=Fuente ECCM @@ -410,7 +410,7 @@ BoardView1.Height=Altura BoardView1.HEIGHT=ALTURA BoardView1.Hex=Hex BoardView1.HIDDEN=OCULTO -BoardView1.hit=Golpes con {0}. Necesita {1} +BoardView1.ClubAttackAction=Golpes con {0}. Necesita {1} BoardView1.Hover=Flotar BoardView1.ID=\ ID: BoardView1.IMMOBILE=INMÓVIL @@ -454,13 +454,13 @@ BoardView1.on=activado BoardView1.pilot=\ Piloto BoardView1.PRONE=CAIDO BoardView1.NO_GYRO=NO GYRO -BoardView1.proto=Hace un ataque frenético de protomech. Necesita {0} +BoardView1.ProtomechPhysicalAttackAction=Hace un ataque frenético de protomech. Necesita {0} BoardView1.punchBoth=Golpea con ambos brazos. Izquierdo necesita {0}; Derecho necesita {1} BoardView1.punchLeft=Golpea con el brazo izquierdo. Necesita {0} BoardView1.punchRight=Golpea con el brazo derecho. Necesita {0} -BoardView1.push=Empuja. Necesita {0} +BoardView1.PushAttackAction=Empuja. Necesita {0} BoardView1.Rubble=Escombros -BoardView1.Searchlight=Se ilumina con un reflector. +BoardView1.SearchlightAttackAction=Se ilumina con un reflector. BoardView1.Smoke={0} hex de humo en el campo visual.\n BoardView1.STUCK=ATASCADO BoardView1.STUNNED=ATURDIDO:{0} diff --git a/megamek/i18n/megamek/client/messages_ru.properties b/megamek/i18n/megamek/client/messages_ru.properties index 56cd6e9484..82fb6da107 100644 --- a/megamek/i18n/megamek/client/messages_ru.properties +++ b/megamek/i18n/megamek/client/messages_ru.properties @@ -103,17 +103,17 @@ BoardView1.Climb=ЗабратьÑÑ Ð½Ð°Ð²ÐµÑ€Ñ… BoardView1.ClimbOff=Пройти через BoardView1.WIGEClimb=Держать выÑоту BoardView1.WIGEClimbOff=ПереÑтать держать выÑоту -BoardView1.charge=Таранит. Ðужно {0} -BoardView1.charge1=(Таранит) +BoardView1.ChargeAttackAction=Таранит. Ðужно {0} +BoardView1.ChargeAttackAction1=(Таранит) BoardView1.CrewDead=ЭКИПÐЖ МЕРТВ -BoardView1.DFA1=(ВыполнÑет DFA) +BoardView1.DfaAttackAction1=(ВыполнÑет DFA) BoardView1.Drop=Drop BoardView1.Command-=Командование- BoardView1.Command=Командование BoardView1.Conventional=Обчн BoardView1.DEPTH=ГЛУБИÐÐ BoardView1.detonated=взорваны ( -BoardView1.DFA=DFA. Ðужно {0} +BoardView1.DfaAttackAction=DFA. Ðужно {0} BoardView1.ecmSource=ИÑточник ECM BoardView1.eccmSource=ИÑточник ECCM BoardView1.Evade=\ (Уход) @@ -125,7 +125,7 @@ BoardView1.HeavyWoods={0} гекÑов гуÑтого леÑа на линии BoardView1.Height=Ð’Ñ‹Ñота BoardView1.HEIGHT=ВЫСОТРBoardView1.Hex=Ð“ÐµÐºÑ -BoardView1.hit=Попадание при {0}. Ðужно {1} +BoardView1.ClubAttackAction=Попадание при {0}. Ðужно {1} BoardView1.Hover=Парить BoardView1.ID=\ ID: BoardView1.IMMOBILE=ОБЕЗДВИЖЕР@@ -166,13 +166,13 @@ BoardView1.NonMech=не Мех BoardView1.on=на BoardView1.pilot=\ Пилот BoardView1.PRONE=ЛЕЖИТ -BoardView1.proto=ВыполнÑет ÑроÑтную атаку протомеха. Ðужно {0} +BoardView1.ProtomechPhysicalAttackAction=ВыполнÑет ÑроÑтную атаку протомеха. Ðужно {0} BoardView1.punchBoth=Бьет обеими руками. Левой нужно {0}; Правой нужно {1} BoardView1.punchLeft=Бьет левой рукой. Ðужно {0} BoardView1.punchRight=Бьет правой рукой. Ðужно {0} -BoardView1.push=Толкает. Ðужно {0} +BoardView1.PushAttackAction=Толкает. Ðужно {0} BoardView1.Rubble=МуÑор -BoardView1.Searchlight=Светит поиÑковым прожектором. +BoardView1.SearchlightAttackAction=Светит поиÑковым прожектором. BoardView1.Smoke={0} дымных гекÑа(ов) на линии обзора.\n BoardView1.STUCK=ЗÐСТРЯЛ BoardView1.STUNNED=ОГЛУШЕÐ:{0} diff --git a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java index 9c1d5dac12..f708d43aa7 100644 --- a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java +++ b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java @@ -142,35 +142,34 @@ public boolean contains(Object o) { void addDescription(EntityAction entityAction) { if (entityAction instanceof WeaponAttackAction) { - addWeapon((WeaponAttackAction) entityAction); + addEntityAction((WeaponAttackAction) entityAction); } else if (entityAction instanceof KickAttackAction) { - addWeapon((KickAttackAction) entityAction); + addEntityAction((KickAttackAction) entityAction); } else if (entityAction instanceof PunchAttackAction) { - addWeapon((PunchAttackAction) entityAction); + addEntityAction((PunchAttackAction) entityAction); } else if (entityAction instanceof PushAttackAction) { - addWeapon((PushAttackAction) entityAction); + addEntityAction((PushAttackAction) entityAction); } else if (entityAction instanceof ClubAttackAction) { - addWeapon((ClubAttackAction) entityAction); + addEntityAction((ClubAttackAction) entityAction); } else if (entityAction instanceof ChargeAttackAction) { - addWeapon((ChargeAttackAction) entityAction); + addEntityAction((ChargeAttackAction) entityAction); } else if (entityAction instanceof DfaAttackAction) { - addWeapon((DfaAttackAction) entityAction); + addEntityAction((DfaAttackAction) entityAction); } else if (entityAction instanceof ProtomechPhysicalAttackAction) { - addWeapon((ProtomechPhysicalAttackAction) entityAction); + addEntityAction((ProtomechPhysicalAttackAction) entityAction); } else if (entityAction instanceof SearchlightAttackAction) { - addWeapon((SearchlightAttackAction) entityAction); + addEntityAction((SearchlightAttackAction) entityAction); } else if (entityAction instanceof SpotAction) { - addWeapon((SpotAction) entityAction); + addEntityAction((SpotAction) entityAction); } else { - infoCache.put(entityAction, ""); - descriptions.add(entityAction.toDisplayableString(client)); + addEntityAction(entityAction); } } /** * Adds a weapon to this attack */ - void addWeapon(WeaponAttackAction attack) { + void addEntityAction(WeaponAttackAction attack) { final Entity entity = game.getEntity(attack.getEntityId()); final WeaponType wtype = (WeaponType) entity.getEquipment(attack.getWeaponId()).getType(); final Targetable target = attack.getTarget(game); @@ -206,7 +205,7 @@ void addWeapon(WeaponAttackAction attack) { } } - void addWeapon(KickAttackAction attack) { + void addEntityAction(KickAttackAction attack) { String buffer; if (infoCache.containsKey(attack)) { buffer = infoCache.get(attack); @@ -236,7 +235,7 @@ void addWeapon(KickAttackAction attack) { descriptions.add(buffer); } - void addWeapon(PunchAttackAction attack) { + void addEntityAction(PunchAttackAction attack) { String buffer; if (infoCache.containsKey(attack)) { buffer = infoCache.get(attack); @@ -266,91 +265,105 @@ void addWeapon(PunchAttackAction attack) { descriptions.add(buffer); } - void addWeapon(PushAttackAction attack) { + void addEntityAction(PushAttackAction attack) { String buffer; if (infoCache.containsKey(attack)) { buffer = infoCache.get(attack); } else { final String roll = attack.toHit(game).getValueAsString(); - buffer = Messages.getString("BoardView1.push", roll); + buffer = Messages.getString("BoardView1.PushAttackAction", roll); infoCache.put(attack, buffer); } descriptions.add(buffer); } - void addWeapon(ClubAttackAction attack) { + void addEntityAction(ClubAttackAction attack) { String buffer; if (infoCache.containsKey(attack)) { buffer = infoCache.get(attack); } else { final String roll = attack.toHit(game).getValueAsString(); final String club = attack.getClub().getName(); - buffer = Messages.getString("BoardView1.hit", club, roll); + buffer = Messages.getString("BoardView1.ClubAttackAction", club, roll); infoCache.put(attack, buffer); } descriptions.add(buffer); } - void addWeapon(ChargeAttackAction attack) { + void addEntityAction(ChargeAttackAction attack) { String buffer; if (infoCache.containsKey(attack)) { buffer = infoCache.get(attack); } else { final String roll = attack.toHit(game).getValueAsString(); - buffer = Messages.getString("BoardView1.charge", roll); + buffer = Messages.getString("BoardView1.ChargeAttackAction", roll); infoCache.put(attack, buffer); } descriptions.add(buffer); } - void addWeapon(DfaAttackAction attack) { + void addEntityAction(DfaAttackAction attack) { String buffer; if (infoCache.containsKey(attack)) { buffer = infoCache.get(attack); } else { final String roll = attack.toHit(game).getValueAsString(); - buffer = Messages.getString("BoardView1.DFA", roll); + buffer = Messages.getString("BoardView1.DfaAttackAction", roll); infoCache.put(attack, buffer); } descriptions.add(buffer); } - void addWeapon(ProtomechPhysicalAttackAction attack) { + void addEntityAction(ProtomechPhysicalAttackAction attack) { String buffer; if (infoCache.containsKey(attack)) { buffer = infoCache.get(attack); } else { final String roll = attack.toHit(game).getValueAsString(); - buffer = Messages.getString("BoardView1.proto", roll); + buffer = Messages.getString("BoardView1.ProtomechPhysicalAttackAction", roll); infoCache.put(attack, buffer); } descriptions.add(buffer); } - void addWeapon(SearchlightAttackAction attack) { + void addEntityAction(SearchlightAttackAction attack) { String buffer; if (infoCache.containsKey(attack)) { buffer = infoCache.get(attack); } else { - buffer = Messages.getString("BoardView1.Searchlight"); + Entity target = game.getEntity(attack.getTargetId()); + buffer = Messages.getString("BoardView1.SearchlightAttackAction") + ((target != null) ? ' ' +target.getShortName() : ""); infoCache.put(attack, buffer); } descriptions.add(buffer); } - void addWeapon(SpotAction attack) { + void addEntityAction(SpotAction attack) { String buffer; - Entity target = game.getEntity(attack.getTargetId()); if (infoCache.containsKey(attack)) { buffer = infoCache.get(attack); } else { - buffer = Messages.getString("BoardView1.Spot", (target != null) ? target.getShortName() : "" ); + Entity target = game.getEntity(attack.getTargetId()); + buffer = Messages.getString("BoardView1.SpotAction", (target != null) ? target.getShortName() : "" ); infoCache.put(attack, buffer); } descriptions.add(buffer); } + void addEntityAction(EntityAction entityAction) { + String buffer; + + if (infoCache.containsKey(entityAction)) { + buffer = infoCache.get(entityAction); + } else { + String typeName = entityAction.getClass().getTypeName(); + buffer = typeName.substring(typeName.lastIndexOf('.') + 1); + infoCache.put(entityAction, buffer); + } + descriptions.add(buffer); + } + @Override public int size() { return actions.size(); From d8fbad08b2bfa1a3ab842d2d3bfe66232bf8bf30 Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Sat, 10 Jun 2023 20:03:43 -0700 Subject: [PATCH 08/13] TurnDetailOverlay visibility menu item and GUIP --- .../i18n/megamek/client/messages.properties | 5 ++ megamek/mmconf/defaultKeyBinds.xml | 7 ++ .../client/ui/swing/AttackPhaseDisplay.java | 2 +- .../megamek/client/ui/swing/ClientGUI.java | 6 ++ .../client/ui/swing/CommonMenuBar.java | 71 ++++++++++--------- .../client/ui/swing/GUIPreferences.java | 8 +++ .../boardview/AbstractBoardViewOverlay.java | 14 +++- .../ui/swing/boardview/AttackSprite.java | 2 +- .../swing/boardview/KeyBindingsOverlay.java | 5 -- .../boardview/PlanetaryConditionsOverlay.java | 7 -- .../swing/boardview/TurnDetailsOverlay.java | 20 +++--- .../ui/swing/tooltip/EntityActionLog.java | 42 +++++------ .../client/ui/swing/util/KeyCommandBind.java | 1 + 13 files changed, 104 insertions(+), 86 deletions(-) diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 0ef26d2461..cd02d3d463 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -1052,6 +1052,7 @@ CommonMenuBar.physicalPush=Push CommonMenuBar.physicalThrash=Thrash CommonMenuBar.physicalVibro=Vibro Claw CommonMenuBar.PrintMenu=Print +CommonMenuBar.turnDetailsOverlay=Show turn details overlay CommonMenuBar.SpecialMenu=Special CommonMenuBar.UnitListMenu=Units CommonMenuBar.Vibrabomb=Vibrabomb({0}) @@ -1805,6 +1806,8 @@ KeyBinds.cmdNames.toggleKeybinds=Keybindings Display KeyBinds.cmdDesc.toggleKeybinds=Toggles the map overlay showing various key shortcuts KeyBinds.cmdNames.togglePlanetaryConditions=Planetary Conditions Display KeyBinds.cmdDesc.togglePlanetaryConditions=Toggles the map overlay showing Planetary Conditions +KeyBinds.cmdNames.toggleTurnDetails=Turn Details Display +KeyBinds.cmdDesc.toggleTurnDetails=Toggles the map overlay showing Turn Details KeyBinds.cmdNames.toggleHexCoords=Toggle Hex Coords KeyBinds.cmdDesc.toggleHexCoords=Toggles the map showing the coords on each hex KeyBinds.cmdNames.toggleMinimap=Toggle Minimap @@ -3428,6 +3431,8 @@ TriggerBPodDialog.title=Trigger ABA Pods TriggerBPodDialog.ChooseTargetDialog.message=Hex {0} contains the following targets.\n\nWhich target do you want to attack?" TriggerBPodDialog.ChooseTargetDialog.title=Choose Target +TurnDetailsOverlay.heading=Turn Details - {0} to toggle + UnitDisplay.SwitchLocation=switch location UnitDisplay.SwitchView=switch view diff --git a/megamek/mmconf/defaultKeyBinds.xml b/megamek/mmconf/defaultKeyBinds.xml index 1f7cde6cf9..9b8ba56015 100644 --- a/megamek/mmconf/defaultKeyBinds.xml +++ b/megamek/mmconf/defaultKeyBinds.xml @@ -371,6 +371,13 @@ false + + toggleTurnDetails + 84 + 128 + false + + clientSettings 67 diff --git a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java index a791be2ada..87b7794d9b 100644 --- a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java @@ -31,7 +31,7 @@ public abstract class AttackPhaseDisplay extends ActionPhaseDisplay { protected AttackPhaseDisplay(ClientGUI cg) { super(cg); - attacks = new EntityActionLog(clientgui.getClient(), true); + attacks = new EntityActionLog(clientgui.getClient()); } /** diff --git a/megamek/src/megamek/client/ui/swing/ClientGUI.java b/megamek/src/megamek/client/ui/swing/ClientGUI.java index 83e9b7e07d..35ae5fad36 100644 --- a/megamek/src/megamek/client/ui/swing/ClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java @@ -162,6 +162,7 @@ public class ClientGUI extends JPanel implements BoardViewListener, public static final String VIEW_TOGGLE_FOV_HIGHLIGHT = "viewToggleFovHighlight"; public static final String VIEW_TOGGLE_FIRING_SOLUTIONS = "viewToggleFiringSolutions"; public static final String VIEW_MOVE_ENV = "viewMovementEnvelope"; + public static final String VIEW_TURN_DETAILS_OVERLAY = "viewTurnDetailsOverlay"; public static final String VIEW_MOVE_MOD_ENV = "viewMovModEnvelope"; public static final String VIEW_CHANGE_THEME = "viewChangeTheme"; public static final String VIEW_ROUND_REPORT = "viewRoundReport"; @@ -963,6 +964,11 @@ public void actionPerformed(ActionEvent event) { ((MovementDisplay) curPanel).computeMovementEnvelope(getUnitDisplay().getCurrentEntity()); } break; + case VIEW_TURN_DETAILS_OVERLAY: + if (curPanel instanceof ActionPhaseDisplay) { + GUIP.setTurnDetailsOverlay(!GUIP.getTurnDetailsOverlay()); + } + break; case VIEW_MOVE_MOD_ENV: if (curPanel instanceof MovementDisplay) { ((MovementDisplay) curPanel).computeModifierEnvelope(); diff --git a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java index 8bf2b63401..302a444a8e 100644 --- a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java +++ b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java @@ -45,12 +45,12 @@ /** * The menu bar that is used across MM, i.e. in the main menu, the board editor and - * the lobby and game. + * the lobby and game. */ public class CommonMenuBar extends JMenuBar implements ActionListener, IPreferenceChangeListener { private static final GUIPreferences GUIP = GUIPreferences.getInstance(); - + /** True when this menu is attached to the board editor. */ private boolean isBoardEditor = false; /** True when this menu is attached to the game's main menu. */ @@ -63,7 +63,7 @@ public class CommonMenuBar extends JMenuBar implements ActionListener, IPreferen // The Game menu private JMenuItem gameLoad = new JMenuItem(getString("CommonMenuBar.fileGameLoad")); private JMenuItem gameSave = new JMenuItem(getString("CommonMenuBar.fileGameSave")); - private JMenuItem gameQSave = new JMenuItem(getString("CommonMenuBar.fileGameQuickSave")); + private JMenuItem gameQSave = new JMenuItem(getString("CommonMenuBar.fileGameQuickSave")); private JMenuItem gameQLoad = new JMenuItem(getString("CommonMenuBar.fileGameQuickLoad")); private JMenuItem gameSaveServer = new JMenuItem(getString("CommonMenuBar.fileGameSaveServer")); private JCheckBoxMenuItem gameRoundReport = new JCheckBoxMenuItem(getString("CommonMenuBar.viewRoundReport")); @@ -71,7 +71,7 @@ public class CommonMenuBar extends JMenuBar implements ActionListener, IPreferen private JCheckBoxMenuItem gamePlayerList = new JCheckBoxMenuItem(getString("CommonMenuBar.viewPlayerList")); private JMenuItem gameGameOptions = new JMenuItem(getString("CommonMenuBar.viewGameOptions")); private JMenuItem gamePlayerSettings = new JMenuItem(getString("CommonMenuBar.viewPlayerSettings")); - + // The Units menu private JMenuItem fileUnitsReinforce = new JMenuItem(getString("CommonMenuBar.fileUnitsReinforce")); private JMenuItem fileUnitsReinforceRAT = new JMenuItem(getString("CommonMenuBar.fileUnitsReinforceRAT")); @@ -122,25 +122,26 @@ public class CommonMenuBar extends JMenuBar implements ActionListener, IPreferen private JCheckBoxMenuItem toggleFovDarken = new JCheckBoxMenuItem(getString("CommonMenuBar.viewToggleFovDarken")); private JCheckBoxMenuItem toggleFiringSolutions = new JCheckBoxMenuItem(getString("CommonMenuBar.viewToggleFiringSolutions")); private JCheckBoxMenuItem viewMovementEnvelope = new JCheckBoxMenuItem(getString("CommonMenuBar.movementEnvelope")); + private JCheckBoxMenuItem viewTurnDetailsOverlay = new JCheckBoxMenuItem(getString("CommonMenuBar.turnDetailsOverlay")); private JMenuItem viewMovModEnvelope = new JMenuItem(getString("CommonMenuBar.movementModEnvelope")); private JMenuItem viewLOSSetting = new JMenuItem(getString("CommonMenuBar.viewLOSSetting")); private JCheckBoxMenuItem viewUnitOverview = new JCheckBoxMenuItem(getString("CommonMenuBar.viewUnitOverview")); private JMenuItem viewClientSettings = new JMenuItem(getString("CommonMenuBar.viewClientSettings")); private JMenuItem viewIncGUIScale = new JMenuItem(getString("CommonMenuBar.viewIncGUIScale")); private JMenuItem viewDecGUIScale = new JMenuItem(getString("CommonMenuBar.viewDecGUIScale")); - + // The Help menu private JMenuItem helpContents = new JMenuItem(getString("CommonMenuBar.helpContents")); private JMenuItem helpSkinning = new JMenuItem(getString("CommonMenuBar.helpSkinning")); private JMenuItem helpAbout = new JMenuItem(getString("CommonMenuBar.helpAbout")); private JMenuItem helpResetNags = new JMenuItem(getString("CommonMenuBar.helpResetNags")); - + // The Firing Action menu private JMenuItem fireSaveWeaponOrder = new JMenuItem(getString("CommonMenuBar.fireSaveWeaponOrder")); - + /** Contains all ActionListeners that have registered themselves with this menu bar. */ private final List actionListeners = new ArrayList<>(); - + /** Maps the Action Command to the respective MenuItem. */ private final Map itemMap = new HashMap<>(); @@ -150,14 +151,14 @@ public CommonMenuBar(Client parent) { isGame = true; updateEnabledStates(); } - + /** Creates a MegaMek menu bar for the board editor. */ public CommonMenuBar(BoardEditor be) { this(); isBoardEditor = true; updateEnabledStates(); } - + /** Creates a MegaMek menu bar for the main menu. */ public CommonMenuBar(MegaMekGUI mmg) { this(); @@ -181,10 +182,10 @@ public CommonMenuBar() { menu = new JMenu(Messages.getString("CommonMenuBar.GameMenu")); add(menu); menu.setMnemonic(VK_G); - + initMenuItem(gameReplacePlayer, menu, FILE_GAME_REPLACE_PLAYER, VK_R); menu.addSeparator(); - + initMenuItem(gameGameOptions, menu, VIEW_GAME_OPTIONS, VK_O); initMenuItem(gamePlayerSettings, menu, VIEW_PLAYER_SETTINGS); initMenuItem(fileUnitsCopy, menu, FILE_UNITS_COPY); @@ -195,11 +196,11 @@ public CommonMenuBar() { initMenuItem(fileUnitsReinforceRAT, menu, FILE_UNITS_REINFORCE_RAT); initMenuItem(fileUnitsSave, menu, FILE_UNITS_SAVE); menu.addSeparator(); - + initMenuItem(fileRefreshCache, menu, FILE_REFRESH_CACHE); initMenuItem(fileUnitsBrowse, menu, FILE_UNITS_BROWSE); menu.addSeparator(); - + initMenuItem(fireSaveWeaponOrder, menu, FIRE_SAVE_WEAPON_ORDER); // Create the Board sub-menu. @@ -213,17 +214,17 @@ public CommonMenuBar() { initMenuItem(boardValidate, menu, BOARD_VALIDATE); initMenuItem(boardSourceFile, menu, BOARD_SOURCEFILE); menu.addSeparator(); - + initMenuItem(boardSaveAsImage, menu, BOARD_SAVE_AS_IMAGE); boardSaveAsImage.setToolTipText(Messages.getString("CommonMenuBar.fileBoardSaveAsImage.tooltip")); initMenuItem(boardSaveAsImageUnits, menu, BOARD_SAVE_AS_IMAGE_UNITS); boardSaveAsImageUnits.setToolTipText(Messages.getString("CommonMenuBar.fileBoardSaveAsImageUnits.tooltip")); menu.addSeparator(); - + initMenuItem(boardUndo, menu, BOARD_UNDO); initMenuItem(boardRedo, menu, BOARD_REDO); menu.addSeparator(); - + initMenuItem(boardResize, menu, BOARD_RESIZE); initMenuItem(boardChangeTheme, menu, VIEW_CHANGE_THEME); initMenuItem(boardRaise, menu, BOARD_RAISE); @@ -236,7 +237,7 @@ public CommonMenuBar() { initMenuItem(boardRemoveWater, boardRemove, BOARD_REMOVE_WATER); initMenuItem(boardRemoveRoads, boardRemove, BOARD_REMOVE_ROADS); initMenuItem(boardRemoveBuildings, boardRemove, BOARD_REMOVE_BUILDINGS); - + // Create the view menu. menu = new JMenu(Messages.getString("CommonMenuBar.ViewMenu")); menu.setMnemonic(VK_V); @@ -266,7 +267,7 @@ public CommonMenuBar() { viewPlanetaryConditionsOverlay.setSelected(GUIP.getShowPlanetaryConditionsOverlay()); initMenuItem(viewUnitOverview, menu, VIEW_UNIT_OVERVIEW); menu.addSeparator(); - + initMenuItem(viewResetWindowPositions, menu, VIEW_RESET_WINDOW_POSITIONS); initMenuItem(viewAccessibilityWindow, menu, VIEW_ACCESSIBILITY_WINDOW, VK_A); viewAccessibilityWindow.setMnemonic(KeyEvent.VK_A); @@ -280,15 +281,17 @@ public CommonMenuBar() { initMenuItem(toggleHexCoords, menu, VIEW_TOGGLE_HEXCOORDS, VK_G); initMenuItem(viewLabels, menu, VIEW_LABELS); menu.addSeparator(); - + initMenuItem(toggleFovDarken, menu, VIEW_TOGGLE_FOV_DARKEN); - toggleFovDarken.setSelected(GUIP.getFovDarken()); + toggleFovDarken.setSelected(GUIP.getFovDarken()); toggleFovDarken.setToolTipText(Messages.getString("CommonMenuBar.viewToggleFovDarkenTooltip")); initMenuItem(viewLOSSetting, menu, VIEW_LOS_SETTING); initMenuItem(toggleFovHighlight, menu, VIEW_TOGGLE_FOV_HIGHLIGHT); toggleFovHighlight.setSelected(GUIP.getFovHighlight()); initMenuItem(viewMovementEnvelope, menu, VIEW_MOVE_ENV); viewMovementEnvelope.setSelected(GUIP.getMoveEnvelope()); + initMenuItem(viewTurnDetailsOverlay, menu, VIEW_TURN_DETAILS_OVERLAY); + viewTurnDetailsOverlay.setSelected(GUIP.getTurnDetailsOverlay()); initMenuItem(viewMovModEnvelope, menu, VIEW_MOVE_MOD_ENV); menu.addSeparator(); @@ -301,7 +304,7 @@ public CommonMenuBar() { initMenuItem(toggleFiringSolutions, menu, VIEW_TOGGLE_FIRING_SOLUTIONS); toggleFiringSolutions.setToolTipText(Messages.getString("CommonMenuBar.viewToggleFiringSolutionsToolTip")); toggleFiringSolutions.setSelected(GUIP.getFiringSolutions()); - + /* TODO: moveTraitor = createMenuItem(menu, getString("CommonMenuBar.moveTraitor"), MovementDisplay.MOVE_TRAITOR); */ // Create the Help menu @@ -319,7 +322,7 @@ public CommonMenuBar() { GUIP.addPreferenceChangeListener(this); KeyBindParser.addPreferenceChangeListener(this); } - + /** Sets/updates the accelerators from the KeyCommandBinds preferences. */ private void setKeyBinds() { toggleSensorRange.setAccelerator(KeyCommandBind.keyStroke(KeyCommandBind.SENSOR_RANGE)); @@ -332,6 +335,7 @@ private void setKeyBinds() { viewMovModEnvelope.setAccelerator(KeyCommandBind.keyStroke(KeyCommandBind.MOD_ENVELOPE)); viewKeybindsOverlay.setAccelerator(KeyCommandBind.keyStroke(KeyCommandBind.KEY_BINDS)); viewPlanetaryConditionsOverlay.setAccelerator(KeyCommandBind.keyStroke(KeyCommandBind.PLANETARY_CONDITIONS)); + viewTurnDetailsOverlay.setAccelerator(KeyCommandBind.keyStroke(KeyCommandBind.TURN_DETAILS)); viewMekDisplay.setAccelerator(KeyCommandBind.keyStroke(KeyCommandBind.UNIT_DISPLAY)); viewUnitOverview.setAccelerator(KeyCommandBind.keyStroke(KeyCommandBind.UNIT_OVERVIEW)); viewLOSSetting.setAccelerator(KeyCommandBind.keyStroke(KeyCommandBind.LOS_SETTING)); @@ -411,9 +415,9 @@ private synchronized void updateEnabledStates() { boolean canSave = !phase.isUnknown() && !phase.isSelection() && !phase.isExchange() && !phase.isVictory() && !phase.isStartingScenario(); boolean isNotVictory = !phase.isVictory(); - + viewAccessibilityWindow.setEnabled(false); - + boardChangeTheme.setEnabled(isBoardEditor); boardUndo.setEnabled(isBoardEditor); boardRedo.setEnabled(isBoardEditor); @@ -431,7 +435,7 @@ private synchronized void updateEnabledStates() { boardSourceFile.setEnabled(isBoardEditor); gameQLoad.setEnabled(isMainMenu); gameLoad.setEnabled(isMainMenu); - + gameSave.setEnabled(isLobby || (isInGame && canSave)); gameSaveServer.setEnabled(isLobby || (isInGame && canSave)); gameQSave.setEnabled(isLobby || (isInGame && canSave)); @@ -450,10 +454,10 @@ private synchronized void updateEnabledStates() { boardSaveAsImageUnits.setEnabled(isInGame); gamePlayerList.setEnabled(isInGame); viewLabels.setEnabled(isInGameBoardView); - + gameGameOptions.setEnabled(isInGame || isLobby); gamePlayerSettings.setEnabled(isInGame); - + viewMinimap.setEnabled(isBoardView); viewZoomIn.setEnabled(isBoardView); viewZoomOut.setEnabled(isBoardView); @@ -461,7 +465,7 @@ private synchronized void updateEnabledStates() { viewKeybindsOverlay.setEnabled(isBoardView); viewPlanetaryConditionsOverlay.setEnabled(isBoardView); toggleHexCoords.setEnabled(isBoardView); - + viewLOSSetting.setEnabled(isInGameBoardView); viewUnitOverview.setEnabled(isInGameBoardView); toggleSensorRange.setEnabled(isInGameBoardView); @@ -470,6 +474,7 @@ private synchronized void updateEnabledStates() { toggleFovDarken.setEnabled(isInGameBoardView); toggleFiringSolutions.setEnabled(isInGameBoardView); viewMovementEnvelope.setEnabled(isInGameBoardView); + viewTurnDetailsOverlay.setEnabled(isInGameBoardView); viewMovModEnvelope.setEnabled(isInGameBoardView); gameRoundReport.setEnabled((isInGame)); viewMekDisplay.setEnabled(isInGameBoardView); @@ -487,7 +492,7 @@ public synchronized void setPhase(GamePhase current) { phase = current; updateEnabledStates(); } - + public synchronized void setEnabled(String command, boolean enabled) { if (itemMap.containsKey(command)) { itemMap.get(command).setEnabled(enabled); @@ -525,7 +530,7 @@ public void preferenceChange(PreferenceChangeEvent e) { gamePlayerList.setSelected(GUIP.getPlayerListEnabled()); } } - + /** Adapts the menu (the font size) to the current GUI scale. */ private void adaptToGUIScale() { UIUtil.scaleMenu(this); @@ -536,14 +541,14 @@ public void die() { GUIP.removePreferenceChangeListener(this); KeyBindParser.removePreferenceChangeListener(this); } - + private void initMenuItem(JMenuItem item, JMenu menu, String command) { item.addActionListener(this); item.setActionCommand(command); itemMap.put(command, item); menu.add(item); } - + private void initMenuItem(JMenuItem item, JMenu menu, String command, int mnemonic) { initMenuItem(item, menu, command); item.setMnemonic(mnemonic); diff --git a/megamek/src/megamek/client/ui/swing/GUIPreferences.java b/megamek/src/megamek/client/ui/swing/GUIPreferences.java index 22c4ad6639..20750abea7 100644 --- a/megamek/src/megamek/client/ui/swing/GUIPreferences.java +++ b/megamek/src/megamek/client/ui/swing/GUIPreferences.java @@ -187,6 +187,7 @@ public class GUIPreferences extends PreferenceStoreProxy { public static final String FOCUS = "Focus"; public static final String FIRING_SOLUTIONS = "FiringSolutions"; public static final String MOVE_ENVELOPE = "MoveEnvelope"; + public static final String TURN_DETAILS_OVERLAY = "TurnDetailsOverlay"; public static final String FOV_HIGHLIGHT = "FovHighlight"; public static final String FOV_HIGHLIGHT_ALPHA = "FovHighlightAlpha"; //Rings' sizes (measured in distance to center) separated by whitespace. @@ -967,6 +968,9 @@ public boolean getMoveEnvelope() { return store.getBoolean(MOVE_ENVELOPE); } + public boolean getTurnDetailsOverlay() { + return store.getBoolean(TURN_DETAILS_OVERLAY); + } public boolean getFovHighlight() { return store.getBoolean(FOV_HIGHLIGHT); } @@ -1732,6 +1736,10 @@ public void setMoveEnvelope(boolean state) { store.setValue(MOVE_ENVELOPE, state); } + public void setTurnDetailsOverlay(boolean state) { + store.setValue(TURN_DETAILS_OVERLAY, state); + } + public void setFovHighlight(boolean state) { store.setValue(FOV_HIGHLIGHT, state); } diff --git a/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java index f5b0a3ff20..3dc3617c3e 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java @@ -290,20 +290,30 @@ public void gameTurnChange(GameTurnChangeEvent e) { @Override public void preferenceChange(PreferenceChangeEvent e) { - // change on any preference change + String name = e.getName(); + if (getVisibilityGUIPreference() != visible) { + visible = getVisibilityGUIPreference(); + setDirty(); + } } protected void setDirty() { dirty = true; + //TODO force boardview redraw + clientGui.getBoardView().boardChanged(); } protected void gameTurnOrPhaseChange() { setDirty(); } + protected Color getTextColorGUIPreference() { + // FIXME there is currently no custom option for key bindings + return GUIP.getPlanetaryConditionsColorText(); + } + protected abstract void setVisibilityGUIPreference(boolean value); protected abstract boolean getVisibilityGUIPreference(); - protected abstract Color getTextColorGUIPreference(); protected abstract int getDistTop(Rectangle clipBounds, int overlayHeight); protected abstract int getDistSide(Rectangle clipBounds, int overlayWidth); diff --git a/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java b/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java index abb1195ab3..a7c36144f4 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java +++ b/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java @@ -62,7 +62,7 @@ class AttackSprite extends Sprite { public AttackSprite(BoardView boardView1, final AttackAction attack) { super(boardView1); - weaponDescs = new EntityActionLog(boardView1.clientgui.getClient(), false); + weaponDescs = new EntityActionLog(boardView1.clientgui.getClient()); this.boardView1 = boardView1; entityId = attack.getEntityId(); targetType = attack.getTargetType(); diff --git a/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java index 04897fd4cd..439bfa53fa 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/KeyBindingsOverlay.java @@ -145,11 +145,6 @@ protected void setVisibilityGUIPreference(boolean value) { protected boolean getVisibilityGUIPreference() { return GUIP.getShowKeybindsOverlay(); } - @Override - protected Color getTextColorGUIPreference() { - // FIXME there is currently no custom option for key bindings - return GUIP.getPlanetaryConditionsColorText(); - } @Override protected int getDistTop(Rectangle clipBounds, int overlayHeight) { diff --git a/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java index 49a4c388ea..d3f97eba42 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java @@ -58,12 +58,9 @@ public PlanetaryConditionsOverlay(Game game, ClientGUI cg) { protected List assembleTextLines() { List result = new ArrayList<>(); addHeader(result); -// Color colorTitle = GUIP.getPlanetaryConditionsColorTitle(); Color colorHot = GUIP.getPlanetaryConditionsColorHot(); Color colorCold = GUIP.getPlanetaryConditionsColorCold(); - String toggleKey = KeyCommandBind.getDesc(KeyCommandBind.PLANETARY_CONDITIONS); - if (clientGui != null && !currentGame.getBoard().inSpace()) { // In a game, not the Board Editor @@ -164,10 +161,6 @@ protected void setVisibilityGUIPreference(boolean value) { protected boolean getVisibilityGUIPreference() { return GUIP.getShowPlanetaryConditionsOverlay(); } - @Override - protected Color getTextColorGUIPreference() { - return GUIP.getPlanetaryConditionsColorText(); - } @Override protected int getDistTop(Rectangle clipBounds, int overlayHeight) { diff --git a/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java index 69696a13b2..8a384daede 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java @@ -38,7 +38,7 @@ public class TurnDetailsOverlay extends AbstractBoardViewOverlay { public TurnDetailsOverlay(Game game, ClientGUI cg) { super(game, cg, new Font(Font.MONOSPACED, Font.PLAIN, 12), - Messages.getString("KeyBindingsDisplay.heading", KeyCommandBind.getDesc(KeyCommandBind.KEY_BINDS)) ); + Messages.getString("TurnDetailsOverlay.heading", KeyCommandBind.getDesc(KeyCommandBind.TURN_DETAILS)) ); } @@ -48,11 +48,11 @@ protected List assembleTextLines() { return lines; } - public void setLines(List value) { - lines.clear();; - if (value != null) { -// addHeader(lines); - lines.addAll(value); + public void setLines(List newLines) { + lines.clear(); + if (newLines != null && newLines.size() != 0) { + addHeader(lines); + lines.addAll(newLines); } setDirty(); } @@ -67,15 +67,11 @@ protected void gameTurnOrPhaseChange() { @Override protected void setVisibilityGUIPreference(boolean value) { - GUIP.setValue(GUIPreferences.SHOW_PLANETARYCONDITIONS_OVERLAY, value); + GUIP.setTurnDetailsOverlay(value); } @Override protected boolean getVisibilityGUIPreference() { - return GUIP.getShowPlanetaryConditionsOverlay(); - } - @Override - protected Color getTextColorGUIPreference() { - return GUIP.getPlanetaryConditionsColorText(); + return GUIP.getTurnDetailsOverlay(); } @Override diff --git a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java index f708d43aa7..150229f5fc 100644 --- a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java +++ b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java @@ -15,23 +15,17 @@ public class EntityActionLog implements Collection { final Game game; final Client client; - final protected ArrayList actions = new ArrayList(); - // need to cache to hit values since it goes to IMPOSSIBLE after action placed in attacks list + // cache to prevent regeneration of info if action already processed final HashMap infoCache = new HashMap(); - final HashMap targetCache = new HashMap(); - final ArrayList descriptions = new ArrayList<>(); - final boolean showTarget; - - public EntityActionLog(Client client, boolean showTarget) { + public EntityActionLog(Client client) { this.client = client; this.game = client.getGame(); - this.showTarget = showTarget; } - /** @return a list of descrition strings. Note that there may be fewer than the number of actions + /** @return a list of description strings. Note that there may be fewer than the number of actions * as similar actions are summarized in a single entry */ public List getDescriptions() { @@ -142,7 +136,12 @@ public boolean contains(Object o) { void addDescription(EntityAction entityAction) { if (entityAction instanceof WeaponAttackAction) { + // WeaponAttacks are merged summarized in as few lines as possible, + // so always need a certain level of reormatting addEntityAction((WeaponAttackAction) entityAction); + } else if (infoCache.containsKey(entityAction)) { + //reuse previous description + descriptions.add(infoCache.get(entityAction)); } else if (entityAction instanceof KickAttackAction) { addEntityAction((KickAttackAction) entityAction); } else if (entityAction instanceof PunchAttackAction) { @@ -170,23 +169,16 @@ void addDescription(EntityAction entityAction) { * Adds a weapon to this attack */ void addEntityAction(WeaponAttackAction attack) { - final Entity entity = game.getEntity(attack.getEntityId()); - final WeaponType wtype = (WeaponType) entity.getEquipment(attack.getWeaponId()).getType(); - final Targetable target = attack.getTarget(game); - - final String roll; + final String buffer; if (infoCache.containsKey(attack)) { - roll = infoCache.get(attack); + buffer = infoCache.get(attack); } else { - roll = attack.toHit(game).getValueAsString(); - infoCache.put(attack, roll); - targetCache.put(attack, target); - } - - String table = attack.toHit(game).getTableDesc(); - if (!table.isEmpty()) { - table = " " + table; + String table = attack.toHit(game).getTableDesc(); + buffer = attack.toHit(game).getValueAsString() + ((!table.isEmpty()) ? ' '+table : ""); + infoCache.put(attack, buffer); } + final Entity entity = game.getEntity(attack.getEntityId()); + final WeaponType wtype = (WeaponType) entity.getEquipment(attack.getWeaponId()).getType(); //add to an existing entry if possible boolean found = false; @@ -194,14 +186,14 @@ void addEntityAction(WeaponAttackAction attack) { while (i.hasNext()) { String s = i.next(); if (s.startsWith(wtype.getName())) { - i.set(s + ", " + roll + table); + i.set(s + ", " + buffer); found = true; break; } } if (!found) { - descriptions.add(wtype.getName() + Messages.getString("BoardView1.needs") + roll + table); + descriptions.add(wtype.getName() + Messages.getString("BoardView1.needs") + buffer); } } diff --git a/megamek/src/megamek/client/ui/swing/util/KeyCommandBind.java b/megamek/src/megamek/client/ui/swing/util/KeyCommandBind.java index eb5d9cfd7f..98a51fdb5e 100644 --- a/megamek/src/megamek/client/ui/swing/util/KeyCommandBind.java +++ b/megamek/src/megamek/client/ui/swing/util/KeyCommandBind.java @@ -104,6 +104,7 @@ public enum KeyCommandBind { UNIT_OVERVIEW(true, "toggleUnitOverview", VK_U, CTRL_DOWN_MASK), KEY_BINDS(true, "toggleKeybinds", VK_K, CTRL_DOWN_MASK), PLANETARY_CONDITIONS(true, "togglePlanetaryConditions", VK_P, CTRL_DOWN_MASK), + TURN_DETAILS(true, "toggleTurnDetails", VK_T, CTRL_DOWN_MASK), CLIENT_SETTINGS(true, "clientSettings", VK_C, ALT_DOWN_MASK), INC_GUISCALE(true, "incGuiScale", VK_ADD, CTRL_DOWN_MASK), DEC_GUISCALE(true, "decGuiScale", VK_SUBTRACT, CTRL_DOWN_MASK), From 456e2e79719a43e92ca1638f862c78d490865b27 Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Sun, 11 Jun 2023 21:04:27 -0700 Subject: [PATCH 09/13] EntityActionLog correctly reevals ToHit when actions change it --- .../client/ui/swing/FiringDisplay.java | 10 +- .../ui/swing/boardview/AttackSprite.java | 17 +- .../client/ui/swing/boardview/BoardView.java | 19 +- .../ui/swing/tooltip/EntityActionLog.java | 45 +- megamek/src/megamek/common/Mounted.java | 100 +- .../common/actions/WeaponAttackAction.java | 949 +++++++++--------- 6 files changed, 600 insertions(+), 540 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/FiringDisplay.java b/megamek/src/megamek/client/ui/swing/FiringDisplay.java index dcaaf7acdd..ff1696afaa 100644 --- a/megamek/src/megamek/client/ui/swing/FiringDisplay.java +++ b/megamek/src/megamek/client/ui/swing/FiringDisplay.java @@ -1663,12 +1663,14 @@ void fire() { waa.setStrafingFirstShot(firstShot); firstShot = false; - // add the attack to our temporary queue - addAttack(waa); - - // and add it into the game, temporarily + // Temporarily add attack into the game. On turn done + // this will be recomputed from the local + // @attacks EntityAttackLog, but Game actions + // must be populated to calculate ToHit mods etc. game.addAction(waa); + // add the attack to our temporary queue + addAttack(waa); } // set the weapon as used mounted.setUsedThisRound(true); diff --git a/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java b/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java index a7c36144f4..bce56322b5 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java +++ b/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java @@ -6,9 +6,6 @@ import java.awt.Polygon; import java.awt.Rectangle; import java.awt.image.ImageObserver; -import java.util.ArrayList; -import java.util.List; -import java.util.ListIterator; import megamek.client.ui.Messages; import megamek.client.ui.swing.tooltip.EntityActionLog; @@ -48,7 +45,7 @@ class AttackSprite extends Sprite { private String targetDesc; - EntityActionLog weaponDescs; + EntityActionLog attacks; private final Entity ae; @@ -62,7 +59,7 @@ class AttackSprite extends Sprite { public AttackSprite(BoardView boardView1, final AttackAction attack) { super(boardView1); - weaponDescs = new EntityActionLog(boardView1.clientgui.getClient()); + attacks = new EntityActionLog(boardView1.clientgui.getClient()); this.boardView1 = boardView1; entityId = attack.getEntityId(); targetType = attack.getTargetType(); @@ -106,7 +103,13 @@ public AttackSprite(BoardView boardView1, final AttackAction attack) { } public void addEntityAction(EntityAction entityAction) { - weaponDescs.add(entityAction); + attacks.add(entityAction); + } + + /** reuild the text descriptions to reflect changes in ToHits from adding or removing other attacks such as secondaryTarget */ + public void rebuildDescriptions() + { + attacks.rebuildDescriptions(); } private void makePoly() { @@ -246,7 +249,7 @@ public StringBuffer getTooltip() { result = guiScaledFontHTML(attackColor) + sAttacherDesc + ""; String sAttacks = ""; if ((phase.isFiring()) || (phase.isPhysical())) { - for (String wpD : weaponDescs.getDescriptions()) { + for (String wpD : attacks.getDescriptions()) { sAttacks += "
" + wpD; } result += guiScaledFontHTML(uiBlack()) + sAttacks + ""; diff --git a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java index ab4c93c953..9fdf72479f 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java +++ b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java @@ -4193,12 +4193,14 @@ public synchronized void addAttack(AttackAction aa) { } repaint(100); + int attackerId = aa.getEntityId(); for (AttackSprite sprite : attackSprites) { // can we just add this attack to an existing one? - if ((sprite.getEntityId() == aa.getEntityId()) + if ((sprite.getEntityId() == attackerId) && (sprite.getTargetId() == aa.getTargetId())) { // use existing attack, but add this weapon sprite.addEntityAction(aa); + rebuildAllSpriteDescriptions(attackerId); return; } } @@ -4214,8 +4216,23 @@ public synchronized void addAttack(AttackAction aa) { } else { attackSprites.add(new AttackSprite(this, aa)); } + rebuildAllSpriteDescriptions(attackerId); } + /** adding a new EntityAction may affect the ToHits of other attacks + * so rebuild. The underlying data is cached when possible, so the should + * o the minumum amount of work needed + */ + void rebuildAllSpriteDescriptions(int attackerId) { + for (AttackSprite sprite : attackSprites) { + if (sprite.getEntityId() == attackerId) { + sprite.rebuildDescriptions(); + } + } + + } + + /** * Removes all attack sprites from a certain entity */ diff --git a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java index 150229f5fc..b14511a92c 100644 --- a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java +++ b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java @@ -16,6 +16,7 @@ public class EntityActionLog implements Collection { final Game game; final Client client; final protected ArrayList actions = new ArrayList(); +// final protected ArrayList weaponAttackActions = new ArrayList(); // cache to prevent regeneration of info if action already processed final HashMap infoCache = new HashMap(); final ArrayList descriptions = new ArrayList<>(); @@ -32,25 +33,26 @@ public List getDescriptions() { return descriptions; } - void rebuild() { + public void rebuildDescriptions() { descriptions.clear(); - for (EntityAction entityAction : infoCache.keySet()) { + for (EntityAction entityAction : actions) { addDescription(entityAction); } } /** - * @return a clone of the internal Vector of EntityActions + * @return a clone of the internal List of EntityActions */ public Vector toVector() { return new Vector<>(actions); } /** - * remove all items from collection + * remove all items from all internal collections */ @Override public void clear() { + actions.clear(); infoCache.clear(); descriptions.clear(); } @@ -60,13 +62,13 @@ public boolean add(EntityAction entityAction) { if (!actions.add(entityAction)) { return false; } - addDescription(entityAction); + rebuildDescriptions(); return true; } public void add(int index, EntityAction entityAction) { actions.add(index, entityAction); - addDescription(entityAction); + rebuildDescriptions(); } /** @@ -79,7 +81,7 @@ public boolean remove(Object o) { return false; } infoCache.remove(o); - rebuild(); + rebuildDescriptions(); return true; } @@ -93,9 +95,7 @@ public boolean addAll(Collection c) { if (!actions.addAll(c)) { return false; } - for (var a : c) { - addDescription(a); - } + rebuildDescriptions(); return true; } @@ -107,7 +107,7 @@ public boolean removeAll(Collection c) { for (var a : c) { infoCache.remove(a); } - rebuild(); + rebuildDescriptions(); return true; } @@ -136,11 +136,11 @@ public boolean contains(Object o) { void addDescription(EntityAction entityAction) { if (entityAction instanceof WeaponAttackAction) { - // WeaponAttacks are merged summarized in as few lines as possible, - // so always need a certain level of reormatting + // ToHit may change as other actions are added, + // so always re-evaluate all WeaponAttack Actions addEntityAction((WeaponAttackAction) entityAction); } else if (infoCache.containsKey(entityAction)) { - //reuse previous description + // reuse previous description descriptions.add(infoCache.get(entityAction)); } else if (entityAction instanceof KickAttackAction) { addEntityAction((KickAttackAction) entityAction); @@ -169,23 +169,18 @@ void addDescription(EntityAction entityAction) { * Adds a weapon to this attack */ void addEntityAction(WeaponAttackAction attack) { - final String buffer; - if (infoCache.containsKey(attack)) { - buffer = infoCache.get(attack); - } else { - String table = attack.toHit(game).getTableDesc(); - buffer = attack.toHit(game).getValueAsString() + ((!table.isEmpty()) ? ' '+table : ""); - infoCache.put(attack, buffer); - } + ToHitData toHit = attack.toHit(game, true); + String table = toHit.getTableDesc(); + final String buffer = toHit.getValueAsString() + ((!table.isEmpty()) ? ' '+table : ""); final Entity entity = game.getEntity(attack.getEntityId()); - final WeaponType wtype = (WeaponType) entity.getEquipment(attack.getWeaponId()).getType(); + final String weaponName = ((WeaponType) entity.getEquipment(attack.getWeaponId()).getType()).getName(); //add to an existing entry if possible boolean found = false; ListIterator i = descriptions.listIterator(); while (i.hasNext()) { String s = i.next(); - if (s.startsWith(wtype.getName())) { + if (s.startsWith(weaponName)) { i.set(s + ", " + buffer); found = true; break; @@ -193,7 +188,7 @@ void addEntityAction(WeaponAttackAction attack) { } if (!found) { - descriptions.add(wtype.getName() + Messages.getString("BoardView1.needs") + buffer); + descriptions.add(weaponName + Messages.getString("BoardView1.needs") + buffer); } } diff --git a/megamek/src/megamek/common/Mounted.java b/megamek/src/megamek/common/Mounted.java index 78dc0f79a3..fb3d16aa7e 100644 --- a/megamek/src/megamek/common/Mounted.java +++ b/megamek/src/megamek/common/Mounted.java @@ -58,7 +58,7 @@ public class Mounted implements Serializable, RoundUpdated, PhaseUpdated { // Fourshot, etc private int pendingMode = -1; // if mode changes happen at end of turn private boolean modeSwitchable = true; // disallow mode switching - + private int location; private boolean rearMounted; @@ -90,7 +90,7 @@ public class Mounted implements Serializable, RoundUpdated, PhaseUpdated { // and now Machine Gun Arrays too! private Vector bayWeapons = new Vector<>(); private Vector bayAmmo = new Vector<>(); - + // on capital fighters and squadrons some weapon mounts actually represent // multiple weapons of the same type // provide a boolean indicating this type of mount and the number of weapons @@ -155,7 +155,7 @@ public class Mounted implements Serializable, RoundUpdated, PhaseUpdated { * weapon mount? */ private boolean isAPMMounted = false; - + /** * Does this Mounted represent equipmented that is pod mounted in an omni unit? */ @@ -253,7 +253,7 @@ public void restore() { public EquipmentType getType() { return (null != type) ? type : (type = EquipmentType.get(typeName)); } - + /** * @return the current mode of the equipment, or null if it's * not available. @@ -571,11 +571,11 @@ public double getCost() { } public boolean isReady() { - return isReady(false); + return isReady(false, false); } - public boolean isReady(boolean isStrafing) { - return (!usedThisRound || isStrafing) && !destroyed && !missing + public boolean isReady(boolean isStrafing, boolean evenIfAlreadyFired) { + return (!usedThisRound || evenIfAlreadyFired || isStrafing) && !destroyed && !missing && !jammed && !useless && !fired && (!isDWPMounted || (isDWPMounted && (getLinkedBy() != null))); } @@ -816,7 +816,7 @@ public static int getNumShots(WeaponType wtype, EquipmentMode mode, return 2; } // sets number of shots for AC rapid mode - else if (((wtype.getAmmoType() == AmmoType.T_AC) + else if (((wtype.getAmmoType() == AmmoType.T_AC) || (wtype.getAmmoType() == AmmoType.T_LAC) || (wtype.getAmmoType() == AmmoType.T_AC_IMP) || (wtype.getAmmoType() == AmmoType.T_PAC)) @@ -880,11 +880,11 @@ public double getSize() { public void setSize(double size) { this.size = size; } - + /** * The capacity of an ammo bin may be different than the weight of the original shots * in the case of AR10s due to variable missile weight. - * + * * @return The capacity of a mounted ammo bin in tons. * @deprecated Use {@link #getSize()} */ @@ -895,7 +895,7 @@ public double getAmmoCapacity() { /** * Sets the maximum tonnage of ammo for a mounted ammo bin. - * + * * @param capacity The capacity of the bin in tons. * @deprecated Use {@link #setSize(double)} */ @@ -911,8 +911,8 @@ public boolean isRapidfire() { public void setRapidfire(boolean rapidfire) { this.rapidfire = rapidfire; } - - + + /** * Checks to see if the current ammo for this weapon is hotloaded * @@ -982,7 +982,7 @@ public void setHotLoad(boolean hotload) { } } - + /** Returns true when m is a PPC Capacitor and not destroyed. */ private boolean isWorkingCapacitor(Mounted m) { return !m.isDestroyed() @@ -990,9 +990,9 @@ private boolean isWorkingCapacitor(Mounted m) { && ((MiscType) m.getType()).hasFlag(MiscType.F_PPC_CAPACITOR); } - /** + /** * Returns 1 or 2 if this Mounted has a linked - * and charged (= set to charge in an earlier turn) + * and charged (= set to charge in an earlier turn) * PPC Capacitor. */ public int hasChargedCapacitor() { @@ -1010,11 +1010,11 @@ && getLinkedBy().curMode().equals("Charge")) { return 0; } - /** + /** * Returns 1 or 2 if this Mounted has a linked - * and charged (= set to charge in an earlier turn) + * and charged (= set to charge in an earlier turn) * or charging (= set to charge this turn) - * PPC Capacitor. Used to determine heat. + * PPC Capacitor. Used to determine heat. */ public int hasChargedOrChargingCapacitor() { int isCharged = hasChargedCapacitor(); @@ -1192,18 +1192,18 @@ public int getExplosionDamage() { && (((BombType) type).getBombType() == BombType.B_ASEW)) { damagePerShot = 5; } - + //Capital missiles need a racksize for this if (type.hasFlag(AmmoType.F_CAP_MISSILE)) { rackSize = 1; } - + //Screen launchers need a racksize. Damage is 15 per TW p251 if (atype.getAmmoType() == AmmoType.T_SCREEN_LAUNCHER) { rackSize = 1; damagePerShot = 15; } - + long mType = atype.getMunitionType(); // both Dead-Fire and Tandem-charge SRM's do 3 points of damage per // shot when critted @@ -1325,13 +1325,16 @@ public void setTSEMPDowntime(boolean val) { * false otherwise. */ public boolean canFire() { - return canFire(false); + return canFire(false, false); } - public boolean canFire(boolean isStrafing) { + public boolean canFire(boolean isStrafing, boolean evenIfAlreadyFired) { + if (!evenIfAlreadyFired && isFired()) { + return false; + } // Equipment operational? - if (!isReady(isStrafing) || isBreached() || isMissing() || isFired()) { + if (!isReady(isStrafing, evenIfAlreadyFired) || isBreached() || isMissing()) { return false; } @@ -1345,6 +1348,23 @@ public boolean canFire(boolean isStrafing) { return true; } +// public boolean canFire(boolean isStrafing) { +// +// // Equipment operational? +// if (!isReady(isStrafing) || isBreached() || isMissing() || isFired()) { +// return false; +// } +// +// // Is the entity even active? +// if (entity.isShutDown() +// || ((null != entity.getCrew()) && !entity.getCrew().isActive())) { +// return false; +// } +// +// // Otherwise, the equipment can be fired. +// return true; +// } + /** * Determines whether this weapon should be considered crippled for damage * level purposes. @@ -1561,7 +1581,7 @@ public int getDamageTaken() { public void addWeaponToBay(int w) { bayWeapons.add(w); } - + public Vector getBayWeapons() { return bayWeapons; } @@ -1594,13 +1614,13 @@ public void setBombMounted(boolean b) { /** * Convenience "property" to reduce typing, which returns true if the current * piece of equipment is a bomb capable of striking ground targets. - * @return True if + * @return True if */ public boolean isGroundBomb() { return getType().hasFlag(WeaponType.F_DIVE_BOMB) || getType().hasFlag(WeaponType.F_ALT_BOMB) || getType().hasFlag(AmmoType.F_GROUND_BOMB); } - + // is ammo in the same bay as the weapon public boolean ammoInBay(int mAmmoId) { for (int nextAmmoId : bayAmmo) { @@ -1688,11 +1708,11 @@ public boolean isAPMMounted() { public void setAPMMounted(boolean apmMounted) { isAPMMounted = apmMounted; } - + public boolean isOmniPodMounted() { return omniPodMounted; } - + public void setOmniPodMounted(boolean omniPodMounted) { this.omniPodMounted = omniPodMounted; } @@ -1860,7 +1880,7 @@ public boolean isModeSwitchable() { public void setModeSwitchable(boolean b) { modeSwitchable = b; } - + /** * Method that checks to see if our capital missile bay is in bearings-only mode * Only available in space games @@ -1880,7 +1900,7 @@ && getEntity().isSpaceborne()) { } return false; } - + /** * Method that checks to see if our capital missile bay is in waypoint launch mode * Only available in space games @@ -1897,7 +1917,7 @@ && getEntity().isSpaceborne()) { } return false; } - + /** * Method that adds/removes available capital missile modes as we move between space and atmospheric maps * Called by Entity.setGameOptions(), which is in turn called during a mode change by server. @@ -1920,11 +1940,11 @@ public void setModesForMapType() { /* //Placeholder. This will be used to add the space modes back when we're able to switch maps. if (getEntity().isSpaceborne()) { - + } */ } - + public int getBaMountLoc() { return baMountLoc; } @@ -1942,7 +1962,7 @@ public void setBaMountLoc(int baMountLoc) { public boolean isOneShotWeapon() { return (getType() instanceof WeaponType) && getType().hasFlag(WeaponType.F_ONESHOT); } - + /** * Checks whether this mount is either one a one-shot weapon or ammo for a one-shot weapon. * @return @@ -1966,10 +1986,10 @@ public boolean isOneShot() { } return false; } - + /** * Check for whether this mount is linked by a one-shot weapon - * + * * @return {@code true} if this is one-shot ammo */ public boolean isOneShotAmmo() { @@ -2073,7 +2093,7 @@ && getType().getInternalName().equals("ISBAAPDS")) { return false; } } - + /** * Returns true if this Mounted is ammunition in homing mode. */ @@ -2081,7 +2101,7 @@ public boolean isHomingAmmoInHomingMode() { if (!(getType() instanceof AmmoType)) { return false; } - + AmmoType ammoType = (AmmoType) getType(); return ammoType.getMunitionType() == AmmoType.M_HOMING && curMode().equals("Homing"); diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 7b393e0c98..6d7453ab31 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -48,9 +48,9 @@ */ public class WeaponAttackAction extends AbstractAttackAction implements Serializable { private static final long serialVersionUID = -9096603813317359351L; - + public static final int STRATOPS_SENSOR_SHADOW_WEIGHT_DIFF = 100000; - + private int weaponId; private int ammoId = -1; private long ammoMunitionType; @@ -112,7 +112,7 @@ public class WeaponAttackAction extends AbstractAttackAction implements Serializ * not be modified for terrain or movement. See TW pg 260 */ protected boolean isPointblankShot = false; - + /** * Boolean flag that determines if this shot was fired using homing ammunition. * Can be checked to allow casting of attack handlers to the proper homing handler. @@ -141,7 +141,7 @@ public int getAmmoId() { public long getAmmoMunitionType() { return ammoMunitionType; } - + /** * Returns the entity id of the unit carrying the ammo used by this attack * @return @@ -169,7 +169,7 @@ public void setAmmoId(int ammoId) { public void setAmmoMunitionType(long ammoMunitionType) { this.ammoMunitionType = ammoMunitionType; } - + /** * Sets the entity id of the ammo carrier for this shot, if different than the firing entity * @param entityId @@ -245,12 +245,25 @@ public ToHitData toHit(Game game) { game.getTarget(getOriginalTargetType(), getOriginalTargetId()), isStrafing(), isPointblankShot()); } - public ToHitData toHit(Game game, List allECMInfo) { + /** + * + * @param game + * @param evenIfAlreadyFired true if evaluating hit change even if the weapon has already fired or is otherwise unable to fire + * @return + */ + public ToHitData toHit(Game game, boolean evenIfAlreadyFired) { return toHit(game, getEntityId(), game.getTarget(getTargetType(), getTargetId()), + getWeaponId(), getAimedLocation(), getAimingMode(), nemesisConfused, swarmingMissiles, + game.getTarget(getOldTargetType(), getOldTargetId()), + game.getTarget(getOriginalTargetType(), getOriginalTargetId()), isStrafing(), isPointblankShot(), evenIfAlreadyFired); + } + + public ToHitData toHit(Game game, List allECMInfo) { + return toHitCalc(game, getEntityId(), game.getTarget(getTargetType(), getTargetId()), getWeaponId(), getAimedLocation(), getAimingMode(), nemesisConfused, swarmingMissiles, game.getTarget(getOldTargetType(), getOldTargetId()), game.getTarget(getOriginalTargetType(), getOriginalTargetId()), isStrafing(), isPointblankShot(), - allECMInfo); + allECMInfo, false); } public static ToHitData toHit(Game game, int attackerId, Targetable target, int weaponId, boolean isStrafing) { @@ -268,18 +281,26 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target, int int aimingAt, AimingMode aimingMode, boolean isNemesisConfused, boolean exchangeSwarmTarget, Targetable oldTarget, Targetable originalTarget, boolean isStrafing, boolean isPointblankShot) { - return toHit(game, attackerId, target, weaponId, aimingAt, aimingMode, isNemesisConfused, - exchangeSwarmTarget, oldTarget, originalTarget, isStrafing, isPointblankShot, null); + return toHitCalc(game, attackerId, target, weaponId, aimingAt, aimingMode, isNemesisConfused, + exchangeSwarmTarget, oldTarget, originalTarget, isStrafing, isPointblankShot, null, false); + } + + public static ToHitData toHit(Game game, int attackerId, Targetable target, int weaponId, + int aimingAt, AimingMode aimingMode, boolean isNemesisConfused, + boolean exchangeSwarmTarget, Targetable oldTarget, + Targetable originalTarget, boolean isStrafing, boolean isPointblankShot, boolean evenIfAlreadyFired) { + return toHitCalc(game, attackerId, target, weaponId, aimingAt, aimingMode, isNemesisConfused, + exchangeSwarmTarget, oldTarget, originalTarget, isStrafing, isPointblankShot, null, evenIfAlreadyFired); } /** * To-hit number for attacker firing a weapon at the target. */ - private static ToHitData toHit(Game game, int attackerId, Targetable target, int weaponId, + private static ToHitData toHitCalc(Game game, int attackerId, Targetable target, int weaponId, int aimingAt, AimingMode aimingMode, boolean isNemesisConfused, boolean exchangeSwarmTarget, Targetable oldTarget, Targetable originalTarget, boolean isStrafing, - boolean isPointblankShot, List allECMInfo) { + boolean isPointblankShot, List allECMInfo, boolean evenIfAlreadyFired) { final Entity ae = game.getEntity(attackerId); final Mounted weapon = ae.getEquipment(weaponId); @@ -290,7 +311,7 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int LogManager.getLogger().error("Trying to make a weapon attack with " + weapon.getName() + " which has type " + type.getName()); return new ToHitData(TargetRoll.AUTOMATIC_FAIL, Messages.getString("WeaponAttackAction.NotAWeapon")); } - + if (target == null) { LogManager.getLogger().error(attackerId + "Attempting to attack null target"); return new ToHitData(TargetRoll.AUTOMATIC_FAIL, Messages.getString("MovementDisplay.NoTarget")); @@ -316,84 +337,84 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int te = (Entity) target; } boolean isAttackerInfantry = ae instanceof Infantry; - + boolean isWeaponInfantry = wtype.hasFlag(WeaponType.F_INFANTRY) && !ae.isSupportVehicle(); - + boolean isWeaponFieldGuns = isAttackerInfantry && (weapon.getLocation() == Infantry.LOC_FIELD_GUNS); // 2003-01-02 BattleArmor MG and Small Lasers have unlimited ammo. // 2002-09-16 Infantry weapons have unlimited ammo. - + final boolean usesAmmo = (wtype.getAmmoType() != AmmoType.T_NA) && !isWeaponInfantry; - + final Mounted ammo = usesAmmo ? weapon.getLinked() : null; - + final AmmoType atype = ammo == null ? null : (AmmoType) ammo.getType(); - + long munition = AmmoType.M_STANDARD; if (atype != null) { munition = atype.getMunitionType(); } - + final boolean targetInBuilding = Compute.isInBuilding(game, te); - + boolean bMekTankStealthActive = false; if ((ae instanceof Mech) || (ae instanceof Tank)) { bMekTankStealthActive = ae.isStealthActive(); } - + boolean isFlakAttack = !game.getBoard().inSpace() && (te != null) && (te.isAirborne() || te.isAirborneVTOLorWIGE()) && (atype != null) && ((((atype.getAmmoType() == AmmoType.T_AC_LBX) || (atype.getAmmoType() == AmmoType.T_AC_LBX_THB) || (atype.getAmmoType() == AmmoType.T_SBGAUSS)) && (munition == AmmoType.M_CLUSTER)) || (munition == AmmoType.M_FLAK) || (atype.getAmmoType() == AmmoType.T_HAG)); - + boolean isIndirect = (wtype.hasModes() && weapon.curMode().equals(Weapon.MODE_MISSILE_INDIRECT)); - + boolean isInferno = ((atype != null) && ((atype.getAmmoType() == AmmoType.T_SRM) || (atype.getAmmoType() == AmmoType.T_SRM_IMP) || (atype.getAmmoType() == AmmoType.T_MML)) && (atype.getMunitionType() == AmmoType.M_INFERNO)) || (isWeaponInfantry && (wtype.hasFlag(WeaponType.F_INFERNO))); - + boolean isArtilleryDirect = (wtype.hasFlag(WeaponType.F_ARTILLERY) || (wtype instanceof CapitalMissileWeapon && Compute.isGroundToGround(ae, target))) && game.getPhase().isFiring(); - + boolean isArtilleryIndirect = (wtype.hasFlag(WeaponType.F_ARTILLERY) || (wtype instanceof CapitalMissileWeapon && Compute.isGroundToGround(ae, target))) && (game.getPhase().isTargeting() || game.getPhase().isOffboard()); - + boolean isBearingsOnlyMissile = (weapon.isInBearingsOnlyMode()) && (game.getPhase().isTargeting() || game.getPhase().isFiring()); - + boolean isCruiseMissile = (weapon.getType().hasFlag(WeaponType.F_CRUISE_MISSILE) || (wtype instanceof CapitalMissileWeapon && Compute.isGroundToGround(ae, target))); - + // hack, otherwise when actually resolves shot labeled impossible. boolean isArtilleryFLAK = isArtilleryDirect && (te != null) && ((((te.getMovementMode() == EntityMovementMode.VTOL) || (te.getMovementMode() == EntityMovementMode.WIGE)) && te.isAirborneVTOLorWIGE()) || (te.isAirborne())) && (atype != null) && (usesAmmo && (atype.getMunitionType() == AmmoType.M_STANDARD)); - + boolean isHaywireINarced = ae.isINarcedWith(INarcPod.HAYWIRE); - + boolean isINarcGuided = false; - + // for attacks where ECM along flight path makes a difference boolean isECMAffected = ComputeECM.isAffectedByECM(ae, ae.getPosition(), target.getPosition(), allECMInfo); - + // for attacks where only ECM on the target hex makes a difference boolean isTargetECMAffected = ComputeECM.isAffectedByECM(ae, target.getPosition(), target.getPosition(), allECMInfo); - + boolean isTAG = wtype.hasFlag(WeaponType.F_TAG); - + // target type checked later because its different for // direct/indirect (BMRr p77 on board arrow IV) boolean isHoming = ammo != null && ammo.isHomingAmmoInHomingMode(); @@ -405,24 +426,24 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int || (atype.getAmmoType() == AmmoType.T_LRM) || (atype.getAmmoType() == AmmoType.T_LRM_IMP)) && (munition == AmmoType.M_HEAT_SEEKING); - + boolean bFTL = (atype != null) - && ((atype.getAmmoType() == AmmoType.T_MML) + && ((atype.getAmmoType() == AmmoType.T_MML) || (atype.getAmmoType() == AmmoType.T_LRM) || (atype.getAmmoType() == AmmoType.T_LRM_IMP)) && (munition == AmmoType.M_FOLLOW_THE_LEADER); Mounted mLinker = weapon.getLinkedBy(); - + boolean bApollo = ((mLinker != null) && (mLinker.getType() instanceof MiscType) && !mLinker.isDestroyed() && !mLinker.isMissing() && !mLinker.isBreached() && mLinker.getType().hasFlag(MiscType.F_APOLLO)) && (atype != null) && (atype.getAmmoType() == AmmoType.T_MRM); - + boolean bArtemisV = ((mLinker != null) && (mLinker.getType() instanceof MiscType) && !mLinker.isDestroyed() && !mLinker.isMissing() && !mLinker.isBreached() && mLinker.getType().hasFlag(MiscType.F_ARTEMIS_V) && !isECMAffected && !bMekTankStealthActive && (atype != null) && (munition == AmmoType.M_ARTEMIS_V_CAPABLE)); - + if (ae.usesWeaponBays()) { for (int wId : weapon.getBayWeapons()) { Mounted bayW = ae.getEquipment(wId); @@ -433,10 +454,10 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int continue; } AmmoType bAmmo = (AmmoType) bayWAmmo.getType(); - + //If we're using optional rules and firing Arrow Homing missiles from a bay... isHoming = bAmmo != null && bAmmo.getMunitionType() == AmmoType.M_HOMING; - + //If the artillery bay is firing cruise missiles, they have some special rules //It is possible to combine cruise missiles and other artillery in a bay, so //set this to true if any of the weapons are cruise missile launchers. @@ -448,22 +469,22 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int bApollo = ((mLinker != null) && (mLinker.getType() instanceof MiscType) && !mLinker.isDestroyed() && !mLinker.isMissing() && !mLinker.isBreached() && mLinker.getType().hasFlag(MiscType.F_APOLLO)) && (bAmmo != null) && (bAmmo.getAmmoType() == AmmoType.T_MRM); - + bArtemisV = ((mLinker != null) && (mLinker.getType() instanceof MiscType) && !mLinker.isDestroyed() && !mLinker.isMissing() && !mLinker.isBreached() && mLinker.getType().hasFlag(MiscType.F_ARTEMIS_V) && !isECMAffected && !bMekTankStealthActive && (atype != null) && (bAmmo != null) && (bAmmo.getMunitionType() == AmmoType.M_ARTEMIS_V_CAPABLE)); } } - + boolean inSameBuilding = Compute.isInSameBuilding(game, ae, te); - + //Set up the target's relative elevation/depth int targEl; if (te == null) { Hex hex = game.getBoard().getHex(target.getPosition()); - + targEl = hex == null ? 0 : -hex.depth(); } else { targEl = te.relHeight(); @@ -482,17 +503,17 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int || (atype.getAmmoType() == AmmoType.T_LRM_IMP) || (atype.getAmmoType() == AmmoType.T_MML) || (atype.getAmmoType() == AmmoType.T_SRM) - || (atype.getAmmoType() == AmmoType.T_SRM_IMP) + || (atype.getAmmoType() == AmmoType.T_SRM_IMP) || (atype.getAmmoType() == AmmoType.T_NLRM)) && (munition == AmmoType.M_NARC_CAPABLE)) { isINarcGuided = true; } } int toSubtract = 0; - + //Convenience variable to test the targetable type value final int ttype = target.getTargetType(); - + // if we're doing indirect fire, find a spotter Entity spotter = null; boolean narcSpotter = false; @@ -528,17 +549,17 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int int eistatus = 0; boolean mpMelevationHack = false; - if (usesAmmo - && ((wtype.getAmmoType() == AmmoType.T_LRM) || (wtype.getAmmoType() == AmmoType.T_LRM_IMP)) + if (usesAmmo + && ((wtype.getAmmoType() == AmmoType.T_LRM) || (wtype.getAmmoType() == AmmoType.T_LRM_IMP)) && (atype != null) - && (munition == AmmoType.M_MULTI_PURPOSE) + && (munition == AmmoType.M_MULTI_PURPOSE) && (ae.getElevation() == -1) && (ae.getLocationStatus(weapon.getLocation()) == ILocationExposureStatus.WET)) { mpMelevationHack = true; // surface to fire ae.setElevation(0); } - + // check LOS (indirect LOS is from the spotter) LosEffects los; ToHitData losMods; @@ -607,32 +628,32 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int // return to depth 1 ae.setElevation(-1); } - + // determine some more variables int aElev = ae.getElevation(); int tElev = target.getElevation(); int distance = Compute.effectiveDistance(game, ae, target); - + //Set up our initial toHit data ToHitData toHit = new ToHitData(); - + //Check to see if this attack is impossible and return the reason code String reasonImpossible = WeaponAttackAction.toHitIsImpossible(game, ae, attackerId, target, ttype, los, losMods, toHit, distance, spotter, wtype, weapon, weaponId, atype, ammo, munition, isArtilleryDirect, isArtilleryFLAK, isArtilleryIndirect, isAttackerInfantry, isBearingsOnlyMissile, - isCruiseMissile, exchangeSwarmTarget, isHoming, isInferno, isIndirect, isStrafing, isTAG, - targetInBuilding, usesAmmo, underWater); + isCruiseMissile, exchangeSwarmTarget, isHoming, isInferno, isIndirect, isStrafing, isTAG, + targetInBuilding, usesAmmo, underWater, evenIfAlreadyFired); if (reasonImpossible != null) { return new ToHitData(TargetRoll.IMPOSSIBLE, reasonImpossible); } - + //Check to see if this attack is automatically successful and return the reason code String reasonAutoHit = WeaponAttackAction.toHitIsAutomatic(game, ae, target, ttype, los, distance, wtype, weapon, isBearingsOnlyMissile); if (reasonAutoHit != null) { return new ToHitData(TargetRoll.AUTOMATIC_SUCCESS, reasonAutoHit); } - + SpecialResolutionTracker srt = new SpecialResolutionTracker(); srt.setSpecialResolution(false); //Is this an infantry leg/swarm attack? @@ -640,16 +661,16 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int if (srt.isSpecialResolution()) { return toHit; } - + //Check to see if this attack was made with a weapon that has special to-hit rules toHit = handleSpecialWeaponAttacks(game, ae, target, ttype, los, toHit, wtype, atype, srt); if (srt.isSpecialResolution()) { return toHit; } - + //This attack has now tested possible and doesn't follow any weird special rules, //so let's start adding up the to-hit numbers - + //Start with the attacker's weapon skill toHit = new ToHitData(ae.getCrew().getGunnery(), Messages.getString("WeaponAttackAction.GunSkill")); if (game.getOptions().booleanOption(OptionsConstants.RPG_RPG_GUNNERY)) { @@ -666,7 +687,7 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int if (wtype.hasFlag(WeaponType.F_ARTILLERY) && game.getOptions().booleanOption(OptionsConstants.RPG_ARTILLERY_SKILL)) { toHit = new ToHitData(ae.getCrew().getArtillery(), Messages.getString("WeaponAttackAction.ArtySkill")); } - + // Is this an Artillery attack? if (isArtilleryDirect || isArtilleryIndirect) { toHit = handleArtilleryAttacks(game, ae, target, ttype, losMods, toHit, wtype, weapon, atype, isArtilleryDirect, @@ -675,7 +696,7 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int if (srt.isSpecialResolution()) { return toHit; } - + //Mine launchers have their own base to-hit, but can still be affected by terrain and movement modifiers //thus, they don't qualify for special weapon handling if (BattleArmor.MINE_LAUNCHER.equals(wtype.getInternalName())) { @@ -688,16 +709,16 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int // the target hex or the elevation of the hex the attacker is // in, whichever is higher." // Ancient rules - have we implemented this per TW? - + // Store the thruBldg state, for later processing toHit.setThruBldg(los.getThruBldg()); - + // Collect the modifiers for the environment toHit = compileEnvironmentalToHitMods(game, ae, target, wtype, atype, toHit, isArtilleryIndirect); - + // Collect the modifiers for the crew/pilot toHit = compileCrewToHitMods(game, ae, te, toHit, wtype); - + // Collect the modifiers for the attacker's condition/actions if (ae != null) { //Conventional fighter, Aerospace and fighter LAM attackers @@ -712,7 +733,7 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int isWeaponFieldGuns, usesAmmo); } } - + // "hack" to cover the situation where the target is standing in a short // building which provides it partial cover. Unlike other partial cover situations, // this occurs regardless of other LOS consideration. @@ -724,21 +745,21 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int shortBuildingLos.setDamagableCoverTypePrimary(LosEffects.DAMAGABLE_COVER_BUILDING); shortBuildingLos.setCoverBuildingPrimary(currentBuilding); shortBuildingLos.setCoverLocPrimary(target.getPosition()); - + los.add(shortBuildingLos); toHit.append(shortBuildingLos.losModifiers(game)); } - - // Collect the modifiers for the target's condition/actions + + // Collect the modifiers for the target's condition/actions toHit = compileTargetToHitMods(game, ae, target, ttype, los, toHit, toSubtract, aimingAt, aimingMode, distance, wtype, weapon, atype, munition, isArtilleryDirect, isArtilleryIndirect, isAttackerInfantry, exchangeSwarmTarget, isIndirect, isPointblankShot, usesAmmo); - + // Collect the modifiers for terrain and line-of-sight. This includes any related to-hit table changes toHit = compileTerrainAndLosToHitMods(game, ae, target, ttype, aElev, tElev, targEl, distance, los, toHit, losMods, toSubtract, eistatus, wtype, weapon, weaponId, atype, munition, isAttackerInfantry, inSameBuilding, isIndirect, isPointblankShot, underWater); - + // If this is a swarm LRM secondary attack, remove old target movement and terrain mods, then // add those for new target. if (exchangeSwarmTarget) { @@ -750,11 +771,11 @@ private static ToHitData toHit(Game game, int attackerId, Targetable target, int // Collect the modifiers specific to the weapon the attacker is using toHit = compileWeaponToHitMods(game, ae, spotter, target, ttype, toHit, wtype, weapon, atype, munition, isFlakAttack, isIndirect, narcSpotter); - + // Collect the modifiers specific to the ammo the attacker is using toHit = compileAmmoToHitMods(game, ae, target, ttype, toHit, wtype, weapon, atype, munition, bApollo, bArtemisV, bFTL, bHeatSeeking, isECMAffected, isINarcGuided); - + // okay! return toHit; } @@ -783,20 +804,20 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target) { } else { targEl = te.relHeight(); } - + int toSubtract = 0; int distance = Compute.effectiveDistance(game, ae, target); - + // EI system // 0 if no EI (or switched off) // 1 if no intervening light woods // 2 if intervening light woods (because target in woods + intervening // woods is only +1 total) int eistatus = 0; - + // Bogus value, since this method doesn't account for weapons but some of its calls do int weaponId = WeaponType.WEAPON_NA; - + boolean isAttackerInfantry = ae instanceof Infantry; boolean inSameBuilding = Compute.isInSameBuilding(game, ae, te); @@ -813,13 +834,13 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target) { ToHitData losMods = los.losModifiers(game, eistatus, ae.isUnderwater()); ToHitData toHit = new ToHitData(0, Messages.getString("WeaponAttackAction.BaseToHit")); - + // Collect the modifiers for the environment toHit = compileEnvironmentalToHitMods(game, ae, target, null, null, toHit, false); - + // Collect the modifiers for the crew/pilot toHit = compileCrewToHitMods(game, ae, te, toHit, null); - + // Collect the modifiers for the attacker's condition/actions if (ae != null) { //Conventional fighter, Aerospace and fighter LAM attackers @@ -834,13 +855,13 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target) { false, false, false, false, false); } } - - // Collect the modifiers for the target's condition/actions + + // Collect the modifiers for the target's condition/actions toHit = compileTargetToHitMods(game, ae, target, ttype, los, toHit, toSubtract, Entity.LOC_NONE, AimingMode.NONE, distance, null, null, null, AmmoType.M_STANDARD, false, false, isAttackerInfantry, false, false, false, false); - + // Collect the modifiers for terrain and line-of-sight. This includes any related to-hit table changes toHit = compileTerrainAndLosToHitMods(game, ae, target, ttype, aElev, tElev, targEl, distance, los, toHit, losMods, toSubtract, eistatus, null, null, weaponId, null, AmmoType.M_STANDARD, isAttackerInfantry, @@ -849,13 +870,13 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target) { // okay! return toHit; } - - + + /** * Method that tests each attack to see if it's impossible. * If so, a reason string will be returned. A null return means we can continue * processing the attack - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param attackerId The ID number of the attacking entity @@ -864,16 +885,16 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target) { * @param los The calculated LOS between attacker and target * @param losMods ToHitData calculated from the spotter for indirect fire scenarios * @param toHit The running total ToHitData for this WeaponAttackAction - * + * * @param distance The distance in hexes from attacker to target * @param spotter The spotting entity for indirect fire, if present - * + * * @param wtype The WeaponType of the weapon being used * @param weapon The Mounted weapon being used * @param atype The AmmoType being used for this attack * @param ammo The Mounted ammo being used * @param munition Long indicating the munition type flag being used, if applicable - * + * * @param isArtilleryDirect flag that indicates whether this is a direct-fire artillery attack * @param isArtilleryFLAK flag that indicates whether or not this is an artillery flak attack against an entity * @param isArtilleryIndirect flag that indicates whether this is an indirect-fire artillery attack @@ -896,8 +917,8 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta boolean isArtilleryDirect, boolean isArtilleryFLAK, boolean isArtilleryIndirect, boolean isAttackerInfantry, boolean isBearingsOnlyMissile, boolean isCruiseMissile, boolean exchangeSwarmTarget, boolean isHoming, boolean isInferno, boolean isIndirect, boolean isStrafing, boolean isTAG, boolean targetInBuilding, - boolean usesAmmo, boolean underWater) { - + boolean usesAmmo, boolean underWater, boolean evenIfAlreadyFired) { + // Block the shot if the attacker is null if (ae == null) { return Messages.getString("WeaponAttackAction.NoAttacker"); @@ -910,30 +931,30 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if (toHit == null) { toHit = new ToHitData(); } - + Entity te = null; if (ttype == Targetable.TYPE_ENTITY) { //Some weapons only target valid entities te = (Entity) target; } - + // If the attacker and target are in the same building & hex, they can // always attack each other, TW pg 175. if ((los.getThruBldg() != null) && los.getTargetPosition().equals(ae.getPosition())) { return null; } - + // got ammo? if (usesAmmo && ((ammo == null) || (ammo.getUsableShotsLeft() == 0))) { return Messages.getString("WeaponAttackAction.OutOfAmmo"); } - + // are we bracing a location that's not where the weapon is located? if (ae.isBracing() && (ae.braceLocation() != weapon.getLocation())) { - return String.format(Messages.getString("WeaponAttackAction.BracingOtherLocation"), + return String.format(Messages.getString("WeaponAttackAction.BracingOtherLocation"), ae.getLocationName(ae.braceLocation()), ae.getLocationName(weapon.getLocation())); } - + // Ammo-specific Reasons if (atype != null) { // Are we dumping that ammo? @@ -945,14 +966,14 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } // make sure weapon can deliver flares if ((target.getTargetType() == Targetable.TYPE_FLARE_DELIVER) && !(usesAmmo - && ((atype.getAmmoType() == AmmoType.T_LRM) + && ((atype.getAmmoType() == AmmoType.T_LRM) || (atype.getAmmoType() == AmmoType.T_MML) || (atype.getAmmoType() == AmmoType.T_LRM_IMP) || (atype.getAmmoType() == AmmoType.T_MEK_MORTAR)) && (munition == AmmoType.M_FLARE))) { return Messages.getString("WeaponAttackAction.NoFlares"); } - + // These ammo types can only target hexes for flare delivery if (((atype.getAmmoType() == AmmoType.T_LRM) || (atype.getAmmoType() == AmmoType.T_LRM_IMP) @@ -968,7 +989,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta && (ae.getTotalAmmoOfType(ammo.getType()) < weapon.getCurrentShots())) { return Messages.getString("WeaponAttackAction.InsufficientAmmo"); } - + // Some Mek mortar ammo types can only be aimed at a hex if (wtype != null && wtype.hasFlag(WeaponType.F_MEK_MORTAR) && ((atype.getMunitionType() == AmmoType.M_AIRBURST) || (atype.getMunitionType() == AmmoType.M_FLARE) @@ -977,15 +998,15 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return String.format(Messages.getString("WeaponAttackAction.AmmoAtHexOnly"), atype.getSubMunitionName()); } } - + // make sure weapon can deliver minefield - if ((target.getTargetType() == Targetable.TYPE_MINEFIELD_DELIVER) + if ((target.getTargetType() == Targetable.TYPE_MINEFIELD_DELIVER) && !AmmoType.canDeliverMinefield(atype)) { return Messages.getString("WeaponAttackAction.NoMinefields"); } - + // These ammo types can only target hexes for minefield delivery - if (((atype.getAmmoType() == AmmoType.T_LRM) + if (((atype.getAmmoType() == AmmoType.T_LRM) || (atype.getAmmoType() == AmmoType.T_LRM_IMP) || (atype.getAmmoType() == AmmoType.T_MML) || (atype.getAmmoType() == AmmoType.T_MEK_MORTAR)) @@ -998,7 +1019,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.OnlyMinefields"); } } - + // Attacker Action Reasons // If the attacker is actively using a shield, weapons in the same location are blocked @@ -1010,7 +1031,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if (ae.isShutDown() || !ae.getCrew().isActive()) { return Messages.getString("WeaponAttackAction.AttackerNotReady"); } - + // If the attacker is involved in a grapple if (ae.getGrappled() != Entity.NONE) { int grapple = ae.getGrappled(); @@ -1035,7 +1056,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // Only large spacecraft can shoot while evading if (ae.isEvading() && !(ae instanceof Dropship) && !(ae instanceof Jumpship)) { return Messages.getString("WeaponAttackAction.AeEvading"); @@ -1045,13 +1066,13 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if (ae.isLayingMines()) { return Messages.getString("WeaponAttackAction.BusyLayingMines"); } - + // Attacker prone and unable to fire? ToHitData ProneMods = Compute.getProneMods(game, ae, weaponId); if ((ProneMods != null) && ProneMods.getValue() == ToHitData.IMPOSSIBLE) { return ProneMods.getDesc(); } - + // WiGE vehicles cannot fire at 0-range targets as they fly overhead if ((ae.getMovementMode() == EntityMovementMode.WIGE) && (ae.getPosition() == target.getPosition())) { return Messages.getString("WeaponAttackAction.ZeroRangeTarget"); @@ -1063,13 +1084,13 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if (ae instanceof Tank && ((Tank) ae).getStunnedTurns() > 0) { return Messages.getString("WeaponAttackAction.CrewStunned"); } - // Vehicles with a single crewman can't shoot and unjam a RAC in the same turn (like mechs...) - if (game.getOptions().booleanOption(OptionsConstants.ADVANCED_TACOPS_TANK_CREWS) + // Vehicles with a single crewman can't shoot and unjam a RAC in the same turn (like mechs...) + if (game.getOptions().booleanOption(OptionsConstants.ADVANCED_TACOPS_TANK_CREWS) && (ae instanceof Tank) && ae.isUnjammingRAC() && (ae.getCrew().getSize() == 1)) { return Messages.getString("WeaponAttackAction.VeeSingleCrew"); } - + // Critical Damage Reasons @@ -1111,7 +1132,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } // Invalid Target Reasons - + // a friendly unit can never be the target of a direct attack. // but we do allow vehicle flamers to cool. Also swarm missile secondary targets and strafing are exempt. if (!game.getOptions().booleanOption(OptionsConstants.BASE_FRIENDLY_FIRE) && !isStrafing && !exchangeSwarmTarget) { @@ -1126,7 +1147,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if ((target instanceof Entity) && ((Entity) target).isHidden()) { return Messages.getString("WeaponAttackAction.NoFireAtHidden"); } - + // Infantry can't clear woods. if (isAttackerInfantry && (Targetable.TYPE_HEX_CLEAR == target.getTargetType())) { Hex hexTarget = game.getBoard().getHex(target.getPosition()); @@ -1134,7 +1155,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.NoInfantryWoodsClearing"); } } - + // Can't target infantry with Inferno rounds (BMRr, pg. 141). // Also, enforce options for keeping vehicles and protos safe // if those options are checked. @@ -1144,22 +1165,22 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta && game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_PROTOS_SAFE_FROM_INFERNOS)))) { return Messages.getString("WeaponAttackAction.CantShootWithInferno"); } - + // Only weapons allowed to clear minefields can target a hex for minefield clearance - if ((target.getTargetType() == Targetable.TYPE_MINEFIELD_CLEAR) && + if ((target.getTargetType() == Targetable.TYPE_MINEFIELD_CLEAR) && ((atype == null) || !AmmoType.canClearMinefield(atype))) { return Messages.getString("WeaponAttackAction.CantClearMines"); } - + // Mine Clearance munitions can only target hexes for minefield clearance if (!(target instanceof HexTarget) && (atype != null) && (atype.getMunitionType() == AmmoType.M_MINE_CLEARANCE)) { return Messages.getString("WeaponAttackAction.MineClearHexOnly"); } - + // Only screen launchers may target a hex for screen launch if (Targetable.TYPE_HEX_SCREEN == target.getTargetType()) { - if (wtype != null && + if (wtype != null && (!((wtype.getAmmoType() == AmmoType.T_SCREEN_LAUNCHER) || (wtype instanceof ScreenLauncherBayWeapon)))) { return Messages.getString("WeaponAttackAction.ScreenLauncherOnly"); } @@ -1171,12 +1192,12 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta || (wtype instanceof ScreenLauncherBayWeapon)))) { return Messages.getString("WeaponAttackAction.ScreenHexOnly"); } - + // Can't target an entity conducting a swarm attack. if ((te != null) && (Entity.NONE != te.getSwarmTargetId())) { return Messages.getString("WeaponAttackAction.TargetSwarming"); } - + //Tasers must target units and can't target flying units if (wtype != null && wtype.hasFlag(WeaponType.F_TASER)) { if (te != null) { @@ -1187,12 +1208,12 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.TaserOnlyAtUnit"); } } - + // can't target yourself intentionally, but swarm missiles can come back to bite you if (!exchangeSwarmTarget && te != null && ae.equals(te)) { return Messages.getString("WeaponAttackAction.NoSelfTarget"); } - + // Line of Sight and Range Reasons @@ -1233,17 +1254,17 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // http://www.classicbattletech.com/forums/index.php/topic,47618.0.html // anything outside of visual range requires a "sensor lock" in order to // direct fire. Note that this is for ground combat with tacops sensors rules if (game.getOptions().booleanOption(OptionsConstants.ADVANCED_DOUBLE_BLIND) && !ae.isSpaceborne() && !Compute.inVisualRange(game, ae, target) - && !(Compute.inSensorRange(game, ae, target, null) + && !(Compute.inSensorRange(game, ae, target, null) // Can shoot at something in sensor range if it has // been spotted by another unit - && (te != null) && te.hasSeenEntity(ae.getOwner())) + && (te != null) && te.hasSeenEntity(ae.getOwner())) && !isArtilleryIndirect && !isIndirect && !isBearingsOnlyMissile) { boolean networkSee = false; if (ae.hasC3() || ae.hasC3i() || ae.hasActiveNovaCEWS()) { @@ -1305,9 +1326,9 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if (!game.getPhase().isOffboard() && isTAG) { return Messages.getString("WeaponAttackAction.TagOnlyInOffboard"); } - + // Unit-specific Reasons - + // Airborne units cannot tag and attack // http://bg.battletech.com/forums/index.php?topic=17613.new;topicseen#new if (ae.isAirborne() && ae.usedTag()) { @@ -1328,18 +1349,18 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } // hull down vees can't fire front weapons unless indirect - if ((ae instanceof Tank) && ae.isHullDown() && (weapon != null) && + if ((ae instanceof Tank) && ae.isHullDown() && (weapon != null) && (weapon.getLocation() == Tank.LOC_FRONT) && !isIndirect) { return Messages.getString("WeaponAttackAction.FrontBlockedByTerrain"); } // LAMs in fighter mode are restricted to only the ammo types that Aeros can use if ((ae instanceof LandAirMech) && (ae.getConversionMode() == LandAirMech.CONV_MODE_FIGHTER) - && usesAmmo && ammo != null + && usesAmmo && ammo != null && !((AmmoType) ammo.getType()).canAeroUse(game.getOptions().booleanOption(OptionsConstants.ADVAERORULES_AERO_ARTILLERY_MUNITIONS))) { return Messages.getString("WeaponAttackAction.InvalidAmmoForFighter"); } - + // LAMs carrying certain types of bombs that require a weapon have attacks that cannot // be used in mech mode. if ((ae instanceof LandAirMech) @@ -1350,7 +1371,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta && !wtype.hasFlag(WeaponType.F_TAG)) { return Messages.getString("WeaponAttackAction.NoBombInMechMode"); } - + // limit large craft to zero net heat and to heat by arc final int heatCapacity = ae.getHeatCapacity(); if (ae.usesWeaponBays() && (weapon != null) && !weapon.getBayWeapons().isEmpty()) { @@ -1482,11 +1503,11 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } // Protomechs can't fire energy weapons while charging EDP armor - if ((ae instanceof Protomech) && ((Protomech) ae).isEDPCharging() + if ((ae instanceof Protomech) && ((Protomech) ae).isEDPCharging() && wtype != null && wtype.hasFlag(WeaponType.F_ENERGY)) { return Messages.getString("WeaponAttackAction.ChargingEDP"); } - + // for spheroid dropships in atmosphere (and on ground), the rules about // firing arcs are more complicated // TW errata 2.1 @@ -1514,16 +1535,16 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.TooLowForFrontSide"); } // Aft-mounted weapons can only be fired at targets at least 1 altitude lower - // For grounded spheroids, weapons can only be fired at targets in occupied hexes, + // For grounded spheroids, weapons can only be fired at targets in occupied hexes, // but it's not actually possible for a unit to occupy the same hex as a grounded spheroid so // we simplify the calculation a bit if (weapon.getLocation() == Aero.LOC_AFT) { if (altDif > -1) { return Messages.getString("WeaponAttackAction.TooHighForAft"); - } - + } + // if both targets are on the ground - // and the target is below the attacker + // and the target is below the attacker // and the attacker is in one of the target's occupied hexes // then we can shoot aft weapons at it // note that this cannot actually happen in MegaMek currently but is left here for the possible eventuality @@ -1533,7 +1554,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta ae.getPosition().equals(target.getPosition()); boolean targetBelowAttacker = game.getBoard().getHex(ae.getPosition()).getLevel() > game.getBoard().getHex(target.getPosition()).getLevel() + target.getElevation(); - + if (!targetInAttackerHex || !targetBelowAttacker) { return Messages.getString("WeaponAttackAction.GroundedSpheroidDropshipAftWeaponRestriction"); } @@ -1557,29 +1578,29 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } - + // Weapon-specific Reasons - + if (weapon != null && wtype != null) { // Variable setup - + // "Cool" mode for vehicle flamer requires coolant ammo boolean vf_cool = false; if (atype != null && ammo != null && (((AmmoType) ammo.getType()).getMunitionType() == AmmoType.M_COOLANT)) { vf_cool = true; } - + // Anti-Infantry weapons can only target infantry if (wtype.hasFlag(WeaponType.F_INFANTRY_ONLY)) { if ((te != null) && !(te instanceof Infantry)) { return Messages.getString("WeaponAttackAction.TargetOnlyInf"); } } - + // Air-to-ground attacks if (Compute.isAirToGround(ae, target) && !isArtilleryIndirect && !ae.isDropping()) { // Can't strike from above altitude 5. Dive bombing uses a different test below - if ((ae.getAltitude() > 5) + if ((ae.getAltitude() > 5) && !wtype.hasFlag(WeaponType.F_DIVE_BOMB) && !wtype.hasFlag(WeaponType.F_ALT_BOMB)) { return Messages.getString("WeaponAttackAction.AttackerTooHigh"); } @@ -1709,15 +1730,15 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.InvalidStrafingArc"); } } - + // Artillery - + // Arty shots have to be with arty, non arty shots with non arty. if (wtype.hasFlag(WeaponType.F_ARTILLERY)) { // check artillery is targeted appropriately for its ammo // Artillery only targets hexes unless making a direct fire flak shot or using // homing ammo. - + if ((ttype != Targetable.TYPE_HEX_ARTILLERY) && (ttype != Targetable.TYPE_MINEFIELD_CLEAR) && !isArtilleryFLAK && !isHoming && !target.isOffBoard()) { return Messages.getString("WeaponAttackAction.ArtyAttacksOnly"); @@ -1765,7 +1786,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.NoArtyAttacks"); } } - + // Direct-fire artillery attacks. if (isArtilleryDirect) { // Cruise missiles cannot make direct-fire attacks @@ -1787,7 +1808,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // Indirect artillery attacks if (isArtilleryIndirect) { int boardRange = (int) Math.ceil(distance / 17f); @@ -1813,7 +1834,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } // Indirect shots cannot be made at less than 17 hexes range unless // the attacker is airborne or has no line-of-sight - if (((distance <= Board.DEFAULT_BOARD_HEIGHT) && !ae.isAirborne()) + if (((distance <= Board.DEFAULT_BOARD_HEIGHT) && !ae.isAirborne()) && !(losMods.getValue() == TargetRoll.IMPOSSIBLE)) { return Messages.getString("WeaponAttackAction.TooShortForIndirectArty"); } @@ -1824,7 +1845,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // Ballistic and Missile weapons are subject to wind conditions int windCond = game.getPlanetaryConditions().getWindStrength(); if ((windCond == PlanetaryConditions.WI_TORNADO_F13) && wtype.hasFlag(WeaponType.F_MISSILE) @@ -1836,9 +1857,9 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta && (wtype.hasFlag(WeaponType.F_MISSILE) || wtype.hasFlag(WeaponType.F_BALLISTIC))) { return Messages.getString("WeaponAttackAction.F4Tornado"); } - + // Battle Armor - + // BA can only make one AP attack if ((ae instanceof BattleArmor) && wtype.hasFlag(WeaponType.F_INFANTRY)) { final int weapId = ae.getEquipmentNum(weapon); @@ -1859,7 +1880,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // BA compact narc: we have one weapon for each trooper, but you // can fire only at one target at a time if (wtype.getName().equals("Compact Narc")) { @@ -1879,14 +1900,14 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // BA Mine launchers can not hit infantry if (BattleArmor.MINE_LAUNCHER.equals(wtype.getInternalName())) { if (te instanceof Infantry) { return Messages.getString("WeaponAttackAction.CantShootInfantry"); } } - + // BA NARCs and Tasers can only fire at one target in a round if ((ae instanceof BattleArmor) && (wtype.hasFlag(WeaponType.F_TASER) || wtype.getAmmoType() == AmmoType.T_NARC)) { @@ -1912,27 +1933,27 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // BA squad support weapons require that Trooper 1 be alive to use if (weapon.isSquadSupportWeapon() && (ae instanceof BattleArmor)) { if (!((BattleArmor) ae).isTrooperActive(BattleArmor.LOC_TROOPER_1)) { return Messages.getString("WeaponAttackAction.NoSquadSupport"); } } - + // Bombs and such - + // Anti ship missiles can't be launched from altitude 3 or lower if (wtype.hasFlag(WeaponType.F_ANTI_SHIP) && !game.getBoard().inSpace() && (ae.getAltitude() < 4)) { return Messages.getString("WeaponAttackAction.TooLowForASM"); } - + // ASEW Missiles cannot be launched in an atmosphere if ((wtype.getAmmoType() == AmmoType.T_ASEW_MISSILE) && !ae.isSpaceborne()) { return Messages.getString("WeaponAttackAction.ASEWAtmo"); } - + if (ae.isAero()) { // Can't mix bombing with other attack types // also for altitude bombing, the target hex must either be the first in a line @@ -1977,7 +1998,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.BombNotInLine"); } } - + // Altitude and dive bombing attacks... if (wtype.hasFlag(WeaponType.F_DIVE_BOMB) || wtype.hasFlag(WeaponType.F_ALT_BOMB)) { // Can't fire if the unit is out of bombs @@ -2013,13 +2034,13 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // Can't attack bomb hex targets with weapons other than alt/dive bombs if ((target.getTargetType() == Targetable.TYPE_HEX_AERO_BOMB) && !wtype.hasFlag(WeaponType.F_DIVE_BOMB) && !wtype.hasFlag(WeaponType.F_ALT_BOMB)) { return Messages.getString("WeaponAttackAction.InvalidForBombing"); } - + // BA Micro bombs only when flying if ((atype != null) && (atype.getAmmoType() == AmmoType.T_BA_MICRO_BOMB)) { if (!ae.isAirborneVTOLorWIGE()) { @@ -2032,13 +2053,13 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.ExactlyAlt1"); } } - + // Can't attack a Micro Bomb hex target with other weapons if ((target.getTargetType() == Targetable.TYPE_HEX_BOMB) && !(usesAmmo && atype != null && (atype.getAmmoType() == AmmoType.T_BA_MICRO_BOMB))) { return Messages.getString("WeaponAttackAction.InvalidForBombing"); } - + // Space bombing attacks if (wtype.hasFlag(WeaponType.F_SPACE_BOMB) && te != null) { toHit = Compute.getSpaceBombBaseToHit(ae, te, game); @@ -2047,7 +2068,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return toHit.getDesc(); } } - + // B-Pods if (wtype.hasFlag(WeaponType.F_B_POD)) { @@ -2068,7 +2089,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // Called shots if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_CALLED_SHOTS)) { String reason = weapon.getCalledShot().isValid(target); @@ -2076,7 +2097,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return reason; } } - + // Capital Mass Drivers can only fire at targets directly in front of the attacker if ((target.getTargetType() == Targetable.TYPE_ENTITY) && wtype.hasFlag(WeaponType.F_MASS_DRIVER) && (ae instanceof SpaceStation)) { @@ -2084,7 +2105,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.MassDriverFrontOnly"); } } - + // Capital missiles in bearings-only mode if (isBearingsOnlyMissile) { //Can't target anything beyond max range of 5,000 hexes @@ -2095,13 +2116,13 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta // Can't fire in bearings-only mode within direct-fire range (50 hexes) if (game.getPhase().isTargeting() && distance < RangeType.RANGE_BEARINGS_ONLY_MINIMUM) { return Messages.getString("WeaponAttackAction.BoMissileMinRange"); - } + } // Can't target anything but hexes if (ttype != Targetable.TYPE_HEX_ARTILLERY) { return Messages.getString("WeaponAttackAction.BOHexOnly"); } } - + // Capital weapons fire by grounded units if (wtype.isSubCapital() || wtype.isCapital()) { // Can't fire any but capital/subcapital missiles surface to surface @@ -2110,15 +2131,15 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.NoS2SCapWeapons"); } } - + // Causing Fires - + // Some weapons can't cause fires, but Infernos always can. if ((vf_cool || (wtype.hasFlag(WeaponType.F_NO_FIRES) && !isInferno)) && (Targetable.TYPE_HEX_IGNITE == target.getTargetType())) { return Messages.getString("WeaponAttackAction.WeaponCantIgnite"); } - + // only woods and buildings can be set intentionally on fire if ((target.getTargetType() == Targetable.TYPE_HEX_IGNITE) && game.getOptions().booleanOption(OptionsConstants.ADVANCED_NO_IGNITE_CLEAR) @@ -2130,9 +2151,9 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta .containsTerrain(Terrains.BUILDING))) { return Messages.getString("WeaponAttackAction.CantIntentionallyBurn"); } - + // Conventional Infantry Attacks - + if (isAttackerInfantry && !(ae instanceof BattleArmor)) { // 0 MP infantry units: move or shoot, except for anti-mech attacks, // those are handled above @@ -2167,7 +2188,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // Extinguishing Fires // You can use certain types of flamer/sprayer ammo or infantry firefighting engineers @@ -2187,15 +2208,15 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.TargetNotBurning"); } } - + // Gauss weapons using the TacOps powered down rule can't fire - if ((wtype instanceof GaussWeapon) + if ((wtype instanceof GaussWeapon) && wtype.hasModes() && weapon.curMode().equals(Weapon.MODE_GAUSS_POWERED_DOWN)) { return Messages.getString("WeaponAttackAction.WeaponNotReady"); } - + // Ground-to-air attacks - + // air2air and air2ground cannot be combined by any aerospace units if (Compute.isAirToAir(ae, target) || Compute.isAirToGround(ae, target)) { for (Enumeration i = game.getActions(); i.hasMoreElements();) { @@ -2215,12 +2236,12 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // Can't make ground-to-air attacks against a target above altitude 8 if ((target.getAltitude() > 8) && Compute.isGroundToAir(ae, target)) { return Messages.getString("WeaponAttackAction.AeroTooHighForGta"); } - + // Infantry can't make ground-to-air attacks, unless using field guns, specialized AA infantry weapons, // or direct-fire artillery flak attacks boolean isWeaponFieldGuns = isAttackerInfantry && (weapon.getLocation() == Infantry.LOC_FIELD_GUNS); @@ -2229,7 +2250,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta && !isWeaponFieldGuns) { return Messages.getString("WeaponAttackAction.NoInfantryGta"); } - + // only one ground-to-air attack allowed per turn // grounded spheroid dropships dont have this limitation if (!ae.isAirborne() && !((ae instanceof Dropship) && ((Aero) ae).isSpheroid())) { @@ -2255,9 +2276,9 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } } } - + // Indirect Fire (LRMs) - + // Can't fire Indirect LRM with direct LOS if (isIndirect && game.getOptions().booleanOption(OptionsConstants.BASE_INDIRECT_FIRE) && !game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_INDIRECT_ALWAYS_POSSIBLE) @@ -2268,19 +2289,19 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta && !wtype.hasFlag(WeaponType.F_MORTARTYPE_INDIRECT)) { return Messages.getString("WeaponAttackAction.NoIndirectWithLOS"); } - + // Can't fire Indirect LRMs if the option is turned off if (isIndirect && !game.getOptions().booleanOption(OptionsConstants.BASE_INDIRECT_FIRE)) { return Messages.getString("WeaponAttackAction.IndirectFireOff"); } // Can't fire an MML indirectly when loaded with SRM munitions - if (isIndirect && usesAmmo + if (isIndirect && usesAmmo && atype != null && (atype.getAmmoType() == AmmoType.T_MML) && !atype.hasFlag(AmmoType.F_MML_LRM)) { return Messages.getString("WeaponAttackAction.NoIndirectSRM"); } - - // Can't fire anything but Mech Mortars and Artillery Cannons indirectly without a spotter + + // Can't fire anything but Mech Mortars and Artillery Cannons indirectly without a spotter // unless the attack has the Oblique Attacker SPA if (isIndirect) { if ((spotter == null) && !(wtype instanceof ArtilleryCannonWeapon) @@ -2289,7 +2310,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.NoSpotter"); } } - + // Infantry Leg attacks and Swarm attacks if (Infantry.LEG_ATTACK.equals(wtype.getInternalName()) && te != null) { toHit = Compute.getLegAttackBaseToHit(ae, te, game); @@ -2331,7 +2352,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if (Entity.NONE == ae.getSwarmTargetId()) { return Messages.getString("WeaponAttackAction.NotSwarming"); } - } + } // Swarming infantry always hit their target, but // they can only target the Mek they're swarming. if ((te != null) && (ae.getSwarmTargetId() == te.getId())) { @@ -2349,9 +2370,9 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } else if (Entity.NONE != ae.getSwarmTargetId()) { return Messages.getString("WeaponAttackAction.MustTargetSwarmed"); } - + // MG arrays - + // Can't fire one if none of the component MGs are functional if (wtype.hasFlag(WeaponType.F_MGA) && (weapon.getCurrentShots() == 0)) { return Messages.getString("WeaponAttackAction.NoWorkingMGs"); @@ -2365,13 +2386,13 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.MGPartOfArray"); } } - + // Protomech can fire MGA only into front arc, TW page 137 if (!Compute.isInArc(ae.getPosition(), ae.getFacing(), target, Compute.ARC_FORWARD) && wtype.hasFlag(WeaponType.F_MGA) && (ae instanceof Protomech)) { return Messages.getString("WeaponAttackAction.ProtoMGAOnlyFront"); } - + // NARC and iNARC if ((wtype.getAmmoType() == AmmoType.T_NARC) || (wtype.getAmmoType() == AmmoType.T_INARC)) { // Cannot be used against targets inside buildings @@ -2383,16 +2404,16 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta return Messages.getString("WeaponAttackAction.CantNarcInfantry"); } } - + // PPCs linked to capacitors can't fire while charging if (weapon.getType().hasFlag(WeaponType.F_PPC) && (weapon.getLinkedBy() != null) && weapon.getLinkedBy().getType().hasFlag(MiscType.F_PPC_CAPACITOR) && weapon.getLinkedBy().pendingMode().equals(Weapon.MODE_PPC_CHARGE)) { return Messages.getString("WeaponAttackAction.PPCCharging"); } - + // Some weapons can only be fired by themselves - + // Check to see if another solo weapon was fired boolean hasSoloAttack = false; String soloWeaponName = ""; @@ -2415,7 +2436,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if (hasSoloAttack) { return String.format(Messages.getString("WeaponAttackAction.CantFireWithOtherWeapons"), soloWeaponName); } - + // Handle solo attack weapons. if (wtype.hasFlag(WeaponType.F_SOLO_ATTACK)) { for (EntityAction ea : game.getActionsVector()) { @@ -2452,19 +2473,19 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta } // TAG - + // The TAG system cannot target Airborne Aeros. if (isTAG && (te != null) && (te.isAirborne() || te.isSpaceborne())) { return Messages.getString("WeaponAttackAction.CantTAGAero"); } - + // The TAG system cannot target infantry. if (isTAG && (te != null) && (te instanceof Infantry)) { return Messages.getString("WeaponAttackAction.CantTAGInf"); } - + // TSEMPs - + // Can't fire a one-shot TSEMP more than once if (wtype.hasFlag(WeaponType.F_TSEMP) && wtype.hasFlag(WeaponType.F_ONESHOT) && weapon.isFired()) { return Messages.getString("WeaponAttackAction.OneShotTSEMP"); @@ -2474,9 +2495,9 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if (wtype.hasFlag(WeaponType.F_TSEMP) && weapon.isFired()) { return Messages.getString("WeaponAttackAction.TSEMPRecharging"); } - + // Weapon Bays - + // Large Craft weapon bays cannot bracket small craft at short range if (wtype.hasModes() && (weapon.curMode().equals(Weapon.MODE_CAPITAL_BRACKET_80) || weapon.curMode().equals(Weapon.MODE_CAPITAL_BRACKET_60) @@ -2486,7 +2507,7 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta true, false) == RangeType.RANGE_SHORT)) { return Messages.getString("WeaponAttackAction.TooCloseForSCBracket"); } - + // you must have enough weapons in your bay to be able to use bracketing if (wtype.hasModes() && weapon.curMode().equals(Weapon.MODE_CAPITAL_BRACKET_80) && (weapon.getBayWeapons().size() < 2)) { return Messages.getString("WeaponAttackAction.BayTooSmallForBracket"); @@ -2497,14 +2518,14 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta if (wtype.hasModes() && weapon.curMode().equals(Weapon.MODE_CAPITAL_BRACKET_40) && (weapon.getBayWeapons().size() < 4)) { return Messages.getString("WeaponAttackAction.BayTooSmallForBracket"); } - + // If you're an aero, can't fire an AMS Bay at all or a Point Defense bay that's in PD Mode if (wtype.hasFlag(WeaponType.F_AMSBAY)) { return Messages.getString("WeaponAttackAction.AutoWeapon"); } else if (wtype.hasModes() && weapon.curMode().equals(Weapon.MODE_POINT_DEFENSE)) { return Messages.getString("WeaponAttackAction.PDWeapon"); } - + // Weapon in arc? if (!Compute.isInArc(game, attackerId, weaponId, target) && (!Compute.isAirToGround(ae, target) || isArtilleryIndirect) @@ -2512,9 +2533,11 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta && !ae.isOffBoard()) { return Messages.getString("WeaponAttackAction.OutOfArc"); } - + // Weapon operational? - if (!weapon.canFire(isStrafing)) { + // TODO move to top for early-out if possible, as this is the most common + // reason shot is impossible + if ((!evenIfAlreadyFired) && (!weapon.canFire(isStrafing, evenIfAlreadyFired))) { return Messages.getString("WeaponAttackAction.WeaponNotReady"); } } @@ -2522,30 +2545,30 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta // If we get here, the shot is possible return null; } - + /** * Method that tests each attack to see if it would automatically hit. * If so, a reason string will be returned. A null return means we can continue * processing the attack - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param target The Targetable object being attacked * @param ttype The targetable object type * @param los The calculated LOS between attacker and target - * + * * @param distance The distance in hexes from attacker to target - * + * * @param wtype The WeaponType of the weapon being used * @param weapon The Mounted weapon being used - * + * * @param isBearingsOnlyMissile flag that indicates whether this is a bearings-only capital missile attack */ private static String toHitIsAutomatic(Game game, Entity ae, Targetable target, int ttype, LosEffects los, int distance, WeaponType wtype, Mounted weapon, boolean isBearingsOnlyMissile) { - + // Buildings - + // Attacks against adjacent buildings automatically hit. if ((distance == 1) && ((ttype == Targetable.TYPE_BUILDING) || (ttype == Targetable.TYPE_BLDG_IGNITE) @@ -2563,28 +2586,28 @@ private static String toHitIsAutomatic(Game game, Entity ae, Targetable target, || (target instanceof GunEmplacement))) { return Messages.getString("WeaponAttackAction.InsideBuilding"); } - + // Special Weapon Rules - + // B-Pod firing at infantry in the same hex autohit if (wtype != null && wtype.hasFlag(WeaponType.F_B_POD) && (target instanceof Infantry) && target.getPosition().equals(ae.getPosition())) { return Messages.getString("WeaponAttackAction.BPodAtInf"); } - + // Capital Missiles in bearings-only mode target hexes and always hit them if (isBearingsOnlyMissile) { if (game.getPhase().isTargeting() && (distance >= RangeType.RANGE_BEARINGS_ONLY_MINIMUM)) { return Messages.getString("WeaponAttackAction.BoMissileHex"); } } - + // Screen launchers target hexes and hit automatically (if in range) if (wtype != null && ((wtype.getAmmoType() == AmmoType.T_SCREEN_LAUNCHER) || (wtype instanceof ScreenLauncherBayWeapon)) && distance <= wtype.getExtremeRange()) { return Messages.getString("WeaponAttackAction.ScreenAutoHit"); } - + // Vehicular grenade launchers if (weapon != null && weapon.getType().hasFlag(WeaponType.F_VGL)) { int facing = weapon.getFacing(); @@ -2596,7 +2619,7 @@ private static String toHitIsAutomatic(Game game, Entity ae, Targetable target, return Messages.getString("WeaponAttackAction.Vgl"); } } - + // If we get here, the shot isn't an auto-hit return null; } @@ -2704,7 +2727,7 @@ public int[] getBombPayload() { } /** - * + * * @param load This is the "bomb payload". It's an array indexed by the constants declared in BombType. * Each element indicates how many types of that bomb should be fired. */ @@ -2743,7 +2766,7 @@ public boolean isHomingShot() { public void setHomingShot(boolean isHomingShot) { this.isHomingShot = isHomingShot; } - + /** * Needed by teleoperated missiles * @param velocity - an integer representing initial velocity @@ -2751,11 +2774,11 @@ public void setHomingShot(boolean isHomingShot) { public void setLaunchVelocity(int velocity) { this.launchVelocity = velocity; } - + //This is a stub. ArtilleryAttackActions actually need to use it public void updateTurnsTilHit(Game game) { } - + /** * Convenience method that compiles the ToHit modifiers applicable to the weather or other special environmental * effects. These affect everyone on the board. @@ -2769,12 +2792,12 @@ public void updateTurnsTilHit(Game game) { */ private static ToHitData compileEnvironmentalToHitMods(Game game, Entity ae, Targetable target, WeaponType wtype, AmmoType atype, ToHitData toHit, boolean isArtilleryIndirect) { - + if (toHit == null) { // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + // Night combat modifiers if (!isArtilleryIndirect) { toHit.append(AbstractAttackAction.nightModifiers(game, target, atype, ae, true)); @@ -2841,7 +2864,7 @@ private static ToHitData compileEnvironmentalToHitMods(Game game, Entity ae, Tar // gravity mods (not in space) if (!game.getBoard().inSpace()) { int mod = (int) Math.floor(Math.abs((game.getPlanetaryConditions().getGravity() - 1.0f) / 0.2f)); - if ((mod != 0) && wtype != null && + if ((mod != 0) && wtype != null && ((wtype.hasFlag(WeaponType.F_BALLISTIC) && wtype.hasFlag(WeaponType.F_DIRECT_FIRE)) || wtype.hasFlag(WeaponType.F_MISSILE))) { toHit.addModifier(mod, Messages.getString("WeaponAttackAction.Gravity")); } @@ -2853,58 +2876,58 @@ private static ToHitData compileEnvironmentalToHitMods(Game game, Entity ae, Tar } return toHit; } - + /** * Convenience method that compiles the ToHit modifiers applicable to the weapon being fired * Got a heavy large laser that gets a +1 TH penalty? You'll find that here. * Bonuses related to the attacker's condition? Ammunition being used? Those are in other methods. - * + * * @param game The current {@link Game} * @param ae The attacking entity * @param spotter The spotting entity, if using indirect fire * @param target The Targetable object being attacked * @param ttype The Targetable object type * @param toHit The running total ToHitData for this WeaponAttackAction - * + * * @param wtype The WeaponType of the weapon being used * @param weapon The Mounted weapon being used for this attack * @param atype The AmmoType being used for this attack * @param munition Long indicating the munition type flag being used, if applicable - * + * * @param isFlakAttack flag that indicates whether the attacker is using Flak against an airborne target * @param isIndirect flag that indicates whether this is an indirect attack (LRM, mortar...) * @param narcSpotter flag that indicates whether this spotting entity is using NARC equipment */ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spotter, Targetable target, - int ttype, ToHitData toHit, WeaponType wtype, Mounted weapon, AmmoType atype, long munition, + int ttype, ToHitData toHit, WeaponType wtype, Mounted weapon, AmmoType atype, long munition, boolean isFlakAttack, boolean isIndirect, boolean narcSpotter) { if (ae == null || wtype == null || weapon == null) { // Can't calculate weapon mods without a valid weapon and an attacker to fire it return toHit; } - + if (toHit == null) { // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + Entity te = null; if (ttype == Targetable.TYPE_ENTITY) { //Some of these weapons only target valid entities te = (Entity) target; } - + // +4 for trying to fire ASEW or antiship missile at a target of < 500 tons if ((wtype.hasFlag(WeaponType.F_ANTI_SHIP) || wtype.getAmmoType() == AmmoType.T_ASEW_MISSILE) && (te != null) && (te.getWeight() < 500)) { toHit.addModifier(4, Messages.getString("WeaponAttackAction.TeTooSmallForASM")); } - + // AAA mode makes targeting large craft more difficult if (wtype.hasModes() && weapon.curMode().equals(Weapon.MODE_CAP_LASER_AAA) && te != null && te.isLargeCraft()) { toHit.addModifier(+1, Messages.getString("WeaponAttackAction.AAALaserAtShip")); } - + // Bombast Lasers if (wtype instanceof ISBombastLaser) { double damage = Compute.dialDownDamage(weapon, wtype); @@ -2925,17 +2948,17 @@ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spo if (wtype.hasModes() && weapon.curMode().equals(Weapon.MODE_CAPITAL_BRACKET_40)) { toHit.addModifier(-3, Messages.getString("WeaponAttackAction.Bracket40")); } - + // Capital ship mass driver penalty. YOU try hitting a maneuvering target with a spinal-mount weapon! if (wtype.hasFlag(WeaponType.F_MASS_DRIVER)) { toHit.addModifier(2, Messages.getString("WeaponAttackAction.MassDriver")); } - + // Capital missiles in waypoint launch mode if (weapon.isInWaypointLaunchMode()) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.WaypointLaunch")); } - + // Capital weapon (except missiles) penalties at small targets if (wtype.isCapital() && (wtype.getAtClass() != WeaponType.CLASS_CAPITAL_MISSILE) && (wtype.getAtClass() != WeaponType.CLASS_AR10) && te != null && !te.isLargeCraft()) { @@ -2950,7 +2973,7 @@ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spo toHit.addModifier(5 - aaaMod, Messages.getString("WeaponAttackAction.CapSmallTe")); } } - + // Check whether we're eligible for a flak bonus... if (isFlakAttack) { // ...and if so, which one (HAGs get an extra -1 as per TW p. 136 @@ -2961,8 +2984,8 @@ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spo toHit.addModifier(-2, Messages.getString("WeaponAttackAction.Flak")); } } - - // Flat to hit modifiers defined in WeaponType + + // Flat to hit modifiers defined in WeaponType if (wtype.getToHitModifier() != 0) { int modifier = wtype.getToHitModifier(); if (wtype instanceof VariableSpeedPulseLaserWeapon) { @@ -2988,14 +3011,14 @@ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spo toHit.addModifier(-1, Messages.getString("WeaponAttackAction.ObliqueAttacker")); } } - + // Indirect fire suffers a +1 penalty if the spotter is making attacks of its own if (isIndirect) { // semiguided ammo negates this modifier, if TAG succeeded if ((atype != null) && ((atype.getAmmoType() == AmmoType.T_LRM) || (atype.getAmmoType() == AmmoType.T_LRM_IMP) || (atype.getAmmoType() == AmmoType.T_MML) - || (atype.getAmmoType() == AmmoType.T_NLRM) + || (atype.getAmmoType() == AmmoType.T_NLRM) || (atype.getAmmoType() == AmmoType.T_MEK_MORTAR)) && (munition == AmmoType.M_SEMIGUIDED)) { @@ -3005,13 +3028,13 @@ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spo } else if (!narcSpotter && (spotter != null)) { // Unless the target has been tagged, or the spotter has an active command console toHit.append(Compute.getSpotterMovementModifier(game, spotter.getId())); - if (spotter.isAttackingThisTurn() && !spotter.getCrew().hasActiveCommandConsole() && + if (spotter.isAttackingThisTurn() && !spotter.getCrew().hasActiveCommandConsole() && !Compute.isTargetTagged(target, game)) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.SpotterAttacking")); } } } - + // And if this is a Mech Mortar if (wtype.hasFlag(WeaponType.F_MORTARTYPE_INDIRECT)) { if (isIndirect) { @@ -3024,14 +3047,14 @@ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spo toHit.addModifier(3, Messages.getString("WeaponAttackAction.DirectMortar")); } } - + // +1 to hit if the Kinder Rapid-Fire ACs optional rule is turned on, but only Jams on a 2. // See TacOps Autocannons for the rest of the rules - if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_KIND_RAPID_AC) + if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_KIND_RAPID_AC) && weapon.curMode().equals(Weapon.MODE_AC_RAPID)) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.AcRapid")); } - + // VSP Lasers // SPA Environmental Specialist @@ -3112,7 +3135,7 @@ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spo // quirks - + // Flat -1 for Accurate Weapon if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_POS_ACCURATE)) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.AccWeapon")); @@ -3125,7 +3148,7 @@ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spo if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_POS_STABLE_WEAPON) && (ae.moved == EntityMovementType.MOVE_RUN)) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.StableWeapon")); } - // +1 for a Misrepaired Weapon - See StratOps Partial Repairs + // +1 for a Misrepaired Weapon - See StratOps Partial Repairs if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_NEG_MISREPAIRED)) { toHit.addModifier(+1, Messages.getString("WeaponAttackAction.MisrepairedWeapon")); } @@ -3135,23 +3158,23 @@ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spo } return toHit; } - + /** * Convenience method that compiles the ToHit modifiers applicable to the ammunition being used * Using precision AC rounds that get a -1 TH bonus? You'll find that here. * Bonuses related to the attacker's condition? Using a weapon with a TH penalty? Those are in other methods. - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param target The Targetable object being attacked * @param ttype The targetable object type * @param toHit The running total ToHitData for this WeaponAttackAction - * + * * @param wtype The WeaponType of the weapon being used * @param weapon The Mounted weapon being used * @param atype The AmmoType being used for this attack * @param munition Long indicating the munition type flag being used, if applicable - * + * * @param bApollo flag that indicates whether the attacker is using an Apollo FCS for MRMs * @param bArtemisV flag that indicates whether the attacker is using an Artemis V FCS * @param bFTL flag that indicates whether the attacker is using FTL missiles @@ -3166,12 +3189,12 @@ private static ToHitData compileAmmoToHitMods(Game game, Entity ae, Targetable t // Can't calculate ammo mods without valid ammo and an attacker to fire it return toHit; } - + if (toHit == null) { // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + Entity te = null; if (ttype == Targetable.TYPE_ENTITY) { //Some ammo can only target valid entities @@ -3179,18 +3202,18 @@ private static ToHitData compileAmmoToHitMods(Game game, Entity ae, Targetable t } // Autocannon Munitions - + // Armor Piercing ammo is a flat +1 - if (((atype.getAmmoType() == AmmoType.T_AC) + if (((atype.getAmmoType() == AmmoType.T_AC) || (atype.getAmmoType() == AmmoType.T_LAC) || (atype.getAmmoType() == AmmoType.T_AC_IMP) || (atype.getAmmoType() == AmmoType.T_PAC)) && (munition == AmmoType.M_ARMOR_PIERCING)) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.ApAmmo")); } - + // Bombs - + // Air-to-air Arrow and Light Air-to-air missiles if (((atype.getAmmoType() == AmmoType.T_AAA_MISSILE) || (atype.getAmmoType() == AmmoType.T_LAA_MISSILE)) && Compute.isAirToGround(ae, target)) { @@ -3201,31 +3224,31 @@ private static ToHitData compileAmmoToHitMods(Game game, Entity ae, Targetable t toHit.addModifier(+3, Messages.getString("WeaponAttackAction.AaaLowAlt")); } } - + // Flat modifiers defined in AmmoType if (atype.getToHitModifier() != 0) { toHit.addModifier(atype.getToHitModifier(), atype.getSubMunitionName() + Messages.getString("WeaponAttackAction.AmmoMod")); } - + // Missile Munitions - + // Apollo FCS for MRMs if (bApollo) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.ApolloFcs")); } - + // add Artemis V bonus if (bArtemisV) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.ArtemisV")); } - + // Follow-the-leader LRMs if (bFTL) { toHit.addModifier(2, atype.getSubMunitionName() + Messages.getString("WeaponAttackAction.AmmoMod")); } - + // Heat Seeking Missiles if (bHeatSeeking) { Hex hexTarget = game.getBoard().getHex(target.getPosition()); @@ -3260,15 +3283,15 @@ private static ToHitData compileAmmoToHitMods(Game game, Entity ae, Targetable t toHit.addModifier(2, Messages.getString("WeaponAttackAction.HsmThruFire")); } } - + // Narc-capable missiles homing on an iNarc beacon if (isINarcGuided) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.iNarcHoming")); } - + // Listen-Kill ammo from War of 3039 sourcebook? if (!isECMAffected - && ((atype.getAmmoType() == AmmoType.T_LRM) + && ((atype.getAmmoType() == AmmoType.T_LRM) || (atype.getAmmoType() == AmmoType.T_LRM_IMP) || (atype.getAmmoType() == AmmoType.T_MML) || (atype.getAmmoType() == AmmoType.T_SRM) @@ -3276,31 +3299,31 @@ private static ToHitData compileAmmoToHitMods(Game game, Entity ae, Targetable t && (munition == AmmoType.M_LISTEN_KILL) && !((te != null) && te.isClan())) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.ListenKill")); } - + return toHit; } - + /** * Convenience method that compiles the ToHit modifiers applicable to the attacker's condition * Attacker has damaged sensors? You'll find that here. * Defender's a superheavy mech? Using a weapon with a TH penalty? Those are in other methods. - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param target The Targetable object being attacked * @param los The calculated LOS between attacker and target * @param toHit The running total ToHitData for this WeaponAttackAction * @param toSubtract An int value representing a running total of mods to disregard - used for some special attacks - * + * * @param aimingAt An int value representing the location being aimed at * @param aimingMode An int value that determines the reason aiming is allowed - * + * * @param wtype The WeaponType of the weapon being used * @param weapon The Mounted weapon being used * @param weaponId The id number of the weapon being used - used by some external calculations * @param atype The AmmoType being used for this attack * @param munition Long indicating the munition type flag being used, if applicable - * + * * @param isFlakAttack flag that indicates whether the attacker is using Flak against an airborne target * @param isHaywireINarced flag that indicates whether the attacker is affected by an iNarc Haywire pod * @param isNemesisConfused flag that indicates whether the attack is affected by an iNarc Nemesis pod @@ -3320,23 +3343,23 @@ private static ToHitData compileAttackerToHitMods(Game game, Entity ae, Targetab // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - - // if we don't have a weapon, that we are attacking with, then the rest of this is + + // if we don't have a weapon, that we are attacking with, then the rest of this is // either meaningless or likely to fail if (weaponId == WeaponType.WEAPON_NA) { return toHit; } - + // Modifiers related to an action the attacker is taking - + // attacker movement toHit.append(Compute.getAttackerMovementModifier(game, ae.getId())); - + // attacker prone if (weaponId > WeaponType.WEAPON_NA) { toHit.append(Compute.getProneMods(game, ae, weaponId)); } - + // add penalty for called shots and change hit table, if necessary if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_CALLED_SHOTS) && weapon != null) { @@ -3374,88 +3397,88 @@ private static ToHitData compileAttackerToHitMods(Game game, Entity ae, Targetab toSubtract += 3; } } - - // Dropping units get hit with a +2 dropping penalty AND the +3 Jumping penalty (SO p22) + + // Dropping units get hit with a +2 dropping penalty AND the +3 Jumping penalty (SO p22) if (ae.isAirborne() && !ae.isAero()) { toHit.addModifier(+2, Messages.getString("WeaponAttackAction.Dropping")); toHit.addModifier(+3, Messages.getString("WeaponAttackAction.Jumping")); } - + // Infantry taking cover suffer a +1 penalty if ((ae instanceof Infantry) && ((Infantry) ae).isTakingCover()) { if (ae.getPosition().direction(target.getPosition()) == ae.getFacing()) { toHit.addModifier(+1, Messages.getString("WeaponAttackAction.FireThruCover")); } } - + // Quadvee converting to a new mode if (ae instanceof QuadVee && ae.isConvertingNow()) { toHit.addModifier(+3, Messages.getString("WeaponAttackAction.QuadVeeConverting")); } - + // we are bracing if (ae.isBracing() && (ae.braceLocation() == weapon.getLocation())) { toHit.addModifier(-2, Messages.getString("WeaponAttackAction.Bracing")); } - + // Secondary targets modifier, // if this is not a iNarc Nemesis confused attack // Inf field guns don't get secondary target mods, TO pg 311 if (!isNemesisConfused && !isWeaponFieldGuns) { toHit.append(Compute.getSecondaryTargetMod(game, ae, target)); } - + // if we're spotting for indirect fire, add +1 if (ae.isSpotting() && !ae.getCrew().hasActiveCommandConsole() && game.getTagInfo().stream().noneMatch(inf -> inf.attackerId == ae.getId())) { toHit.addModifier(+1, Messages.getString("WeaponAttackAction.AeSpotting")); } - + // Special effects (like tasers) affecting the attacker - + // Attacker is battle armor and affected by BA taser feedback if (ae.getTaserFeedBackRounds() > 0) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.AeTaserFeedback")); } - + // If a unit is suffering from electromagnetic interference, they get a // blanket +2. Sucks to be them. if (ae.isSufferingEMI()) { toHit.addModifier(+2, Messages.getString("WeaponAttackAction.EMI")); } - + // heat if (ae.getHeatFiringModifier() != 0) { toHit.addModifier(ae.getHeatFiringModifier(), Messages.getString("WeaponAttackAction.Heat")); } - + // Attacker hit with an iNarc Haywire pod if (isHaywireINarced) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.iNarcHaywire")); } - + // Attacker affected by Taser interference if (ae.getTaserInterferenceRounds() > 0) { toHit.addModifier(ae.getTaserInterference(), Messages.getString("WeaponAttackAction.AeHitByTaser")); } - + // Attacker affected by TSEMP interference if (ae.getTsempEffect() == MMConstants.TSEMP_EFFECT_INTERFERENCE) { toHit.addModifier(+2, Messages.getString("WeaponAttackAction.AeTsemped")); } - + // Special Equipment that that attacker possesses - + // Attacker has an AES system if (weapon != null && ae.hasFunctionalArmAES(weapon.getLocation()) && !weapon.isSplit()) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.AES")); } - + // Heavy infantry have +1 penalty if ((ae instanceof Infantry) && ae.hasWorkingMisc(MiscType.F_TOOLS, MiscType.S_HEAVY_ARMOR)) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.HeavyArmor")); } - + // industrial cockpit: +1 to hit if ((ae instanceof Mech) && (((Mech) ae).getCockpitType() == Mech.COCKPIT_INDUSTRIAL)) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.IndustrialNoAfc")); @@ -3470,7 +3493,7 @@ private static ToHitData compileAttackerToHitMods(Game game, Entity ae, Targetab && ((Mech) ae).isIndustrial()) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.PrimIndustrialAfc")); } - + // Support vehicle basic/advanced fire control systems if ((ae instanceof SupportTank) || (ae instanceof SupportVTOL)) { if (!ae.hasWorkingMisc(MiscType.F_BASIC_FIRECONTROL) @@ -3481,7 +3504,7 @@ private static ToHitData compileAttackerToHitMods(Game game, Entity ae, Targetab toHit.addModifier(1, Messages.getString("WeaponAttackAction.SupVeeBfc")); } } - + // Is the attacker hindered by a shield? if (ae.hasShield() && weapon != null) { // active shield has already been checked as it makes shots @@ -3494,7 +3517,7 @@ private static ToHitData compileAttackerToHitMods(Game game, Entity ae, Targetab toHit.addModifier(+1, Messages.getString("WeaponAttackAction.Shield")); } } - + // add targeting computer (except with LBX cluster ammo) if (aimingMode.isTargetingComputer() && (aimingAt != Entity.LOC_NONE)) { if (ae.hasActiveEiCockpit()) { @@ -3520,19 +3543,19 @@ private static ToHitData compileAttackerToHitMods(Game game, Entity ae, Targetab toHit.addModifier(-1, Messages.getString("WeaponAttackAction.TComp")); } } - + // penalty for an active void signature system if (ae.isVoidSigActive()) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.AeVoidSig")); } - + // Critical damage effects - + // actuator & sensor damage to attacker (includes partial repairs) if (weapon != null) { toHit.append(Compute.getDamageWeaponMods(ae, weapon)); } - + // Vehicle criticals if (ae instanceof Tank) { Tank tank = (Tank) ae; @@ -3545,16 +3568,16 @@ private static ToHitData compileAttackerToHitMods(Game game, Entity ae, Targetab "stabiliser damage"); } } - + // Quirks - + // Anti-air targeting quirk vs airborne unit if (ae.hasQuirk(OptionsConstants.QUIRK_POS_ANTI_AIR) && (target instanceof Entity)) { if (target.isAirborneVTOLorWIGE() || target.isAirborne()) { toHit.addModifier(-2, Messages.getString("WeaponAttackAction.AaVsAir")); } } - + // Sensor ghosts quirk if (ae.hasQuirk(OptionsConstants.QUIRK_NEG_SENSOR_GHOSTS)) { toHit.addModifier(+1, Messages.getString("WeaponAttackAction.SensorGhosts")); @@ -3562,28 +3585,28 @@ private static ToHitData compileAttackerToHitMods(Game game, Entity ae, Targetab return toHit; } - + /** - * Convenience method that compiles the ToHit modifiers applicable to the attacker's condition, + * Convenience method that compiles the ToHit modifiers applicable to the attacker's condition, * if the attacker is an aero * Attacker has damaged sensors? You'll find that here. * Defender's a superheavy mech? Using a weapon with a TH penalty? Those are in other methods. - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param target The Targetable object being attacked * @param ttype The targetable object type * @param toHit The running total ToHitData for this WeaponAttackAction - * + * * @param aimingAt An int value representing the location being aimed at * @param aimingMode An int value that determines the reason aiming is allowed * @param eistatus An int value representing the ei cockpit/pilot upgrade status - * + * * @param wtype The WeaponType of the weapon being used * @param weapon The Mounted weapon being used * @param atype The AmmoType being used for this attack * @param munition Long indicating the munition type flag being used, if applicable - * + * * @param isArtilleryIndirect flag that indicates whether this is an indirect-fire artillery attack * @param isFlakAttack flag that indicates whether the attacker is using Flak against an airborne target * @param isNemesisConfused flag that indicates whether the attack is affected by an iNarc Nemesis pod @@ -3603,31 +3626,31 @@ private static ToHitData compileAeroAttackerToHitMods(Game game, Entity ae, Targ // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + Entity te = null; if (ttype == Targetable.TYPE_ENTITY) { //Some of these weapons only target valid entities te = (Entity) target; } - + // Generic modifiers that apply to airborne and ground attackers - + // actuator & sensor damage to attacker (includes partial repairs) if (weapon != null) { toHit.append(Compute.getDamageWeaponMods(ae, weapon)); } - + // heat if (ae.getHeatFiringModifier() != 0) { toHit.addModifier(ae.getHeatFiringModifier(), Messages.getString("WeaponAttackAction.Heat")); } - + // Secondary targets modifier, if this is not a iNarc Nemesis confused attack // Also does not apply for altitude bombing or strafing if (!isNemesisConfused && wtype != null && !wtype.hasFlag(WeaponType.F_ALT_BOMB) && !isStrafing) { toHit.append(Compute.getSecondaryTargetMod(game, ae, target)); } - + // add targeting computer (except with LBX cluster ammo) if (aimingMode.isTargetingComputer() && (aimingAt != Entity.LOC_NONE)) { if (ae.hasActiveEiCockpit()) { @@ -3653,7 +3676,7 @@ private static ToHitData compileAeroAttackerToHitMods(Game game, Entity ae, Targ toHit.addModifier(-1, Messages.getString("WeaponAttackAction.TComp")); } } - + // Modifiers for aero units, including fighter LAMs if (ae.isAero()) { IAero aero = (IAero) ae; @@ -3665,7 +3688,7 @@ private static ToHitData compileAeroAttackerToHitMods(Game game, Entity ae, Targ && !(ae instanceof Jumpship)) { toHit.addModifier(+1, Messages.getString("WeaponAttackAction.FighterHeavyGauss")); } - + // Space ECM if (game.getBoard().inSpace() && game.getOptions().booleanOption(OptionsConstants.ADVAERORULES_STRATOPS_ECM)) { int ecm = ComputeECM.getLargeCraftECM(ae, ae.getPosition(), target.getPosition()); @@ -3720,7 +3743,7 @@ private static ToHitData compileAeroAttackerToHitMods(Game game, Entity ae, Targ } } } - + // air-to-ground strikes if (Compute.isAirToGround(ae, target) || (ae.isMakingVTOLGroundAttack())) { @@ -3808,7 +3831,7 @@ private static ToHitData compileAeroAttackerToHitMods(Game game, Entity ae, Targ if ((fcs > 0) && !aero.isCapitalFighter()) { toHit.addModifier(fcs * 2, Messages.getString("WeaponAttackAction.FcsDamage")); } - + // CIC hits if (aero instanceof Jumpship) { Jumpship js = (Jumpship) aero; @@ -3820,17 +3843,17 @@ private static ToHitData compileAeroAttackerToHitMods(Game game, Entity ae, Targ // targeting mods for evasive action by large craft // Per TW, this does not apply when firing Capital Missiles - if (aero.isEvading() && wtype != null && + if (aero.isEvading() && wtype != null && (!(wtype.getAtClass() == WeaponType.CLASS_CAPITAL_MISSILE || wtype.getAtClass() == WeaponType.CLASS_AR10 || wtype.getAtClass() == WeaponType.CLASS_TELE_MISSILE))) { toHit.addModifier(+2, Messages.getString("WeaponAttackAction.AeEvading")); } - + // stratops page 113: ECHO maneuvers for large craft if (((aero instanceof Warship) || (aero instanceof Dropship)) && (aero.getFacing() != aero.getSecondaryFacing())) { - // if we're computing this for an "attack preview", then we add 2 MP to + // if we're computing this for an "attack preview", then we add 2 MP to // the mp used, as we haven't used the MP yet. If we're actually processing // the attack, then the entity will be marked as 'done' and we have already added // the 2 MP, so we don't need to double-count it @@ -3912,39 +3935,39 @@ else if (wtype.getAtClass() == WeaponType.CLASS_LBX_AC) { } return toHit; } - + /** * Convenience method that compiles the ToHit modifiers applicable to the attacker's crew/pilot * Pilot wounded? Has an SPA? You'll find that here. * Defender's a superheavy mech? Using a weapon with a TH penalty? Those are in other methods. - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param te The target Entity * @param toHit The running total ToHitData for this WeaponAttackAction - * + * * @param wtype The WeaponType of the weapon being used - * + * */ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, ToHitData toHit, WeaponType wtype) { - + if (ae == null) { // These checks won't work without a valid attacker return toHit; } - + if (toHit == null) { // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + //Now for modifiers affecting the attacker's crew - + // Bonuses for dual cockpits, etc // Bonus to gunnery if both crew members are active; a pilot who takes the gunner's role get +1. if (ae instanceof Mech && ((Mech) ae).getCockpitType() == Mech.COCKPIT_DUAL) { if (!ae.getCrew().isActive(ae.getCrew().getCrewType().getGunnerPos())) { - toHit.addModifier(1, Messages.getString("WeaponAttackAction.GunnerHit")); + toHit.addModifier(1, Messages.getString("WeaponAttackAction.GunnerHit")); } else if (ae.getCrew().hasDedicatedGunner()) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.DualCockpit")); } @@ -3954,15 +3977,15 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T if ((ae instanceof TripodMech || ae instanceof QuadVee) && !ae.getCrew().hasDedicatedGunner()) { toHit.addModifier(+2, Messages.getString("WeaponAttackAction.GunnerHit")); } - + // Fatigue if (game.getOptions().booleanOption(OptionsConstants.ADVANCED_TACOPS_FATIGUE) && ae.getCrew().isGunneryFatigued()) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.Fatigue")); } - + // Injuries - + // Aero unit pilot/crew hits if (ae instanceof Aero) { int pilothits = ae.getCrew().getHits(); @@ -3970,7 +3993,7 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T toHit.addModifier(pilothits, Messages.getString("WeaponAttackAction.PilotHits")); } } - + // Vehicle crew hits if (ae instanceof Tank) { Tank tank = (Tank) ae; @@ -3982,9 +4005,9 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T } } } - + // Manei Domini Upgrades - + // VDNI if (ae.hasAbility(OptionsConstants.MD_VDNI) || ae.hasAbility(OptionsConstants.MD_BVDNI)) { @@ -3998,10 +4021,10 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T toHit.addModifier(-1, Messages.getString("WeaponAttackAction.MdEye")); } } - + // SPAs - - // Unofficial weapon class specialist - Does not have an unspecialized penalty + + // Unofficial weapon class specialist - Does not have an unspecialized penalty if (ae.hasAbility(OptionsConstants.UNOFF_GUNNERY_LASER) && wtype != null && wtype.hasFlag(WeaponType.F_ENERGY)) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.GunLSkill")); @@ -4047,12 +4070,12 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T } } } - + // Target SPAs if (te != null) { // Shaky Stick - Target gets a +1 bonus against Ground-to-Air attacks - if (te.hasAbility(OptionsConstants.PILOT_SHAKY_STICK) - && (te.isAirborne() || te.isAirborneVTOLorWIGE()) + if (te.hasAbility(OptionsConstants.PILOT_SHAKY_STICK) + && (te.isAirborne() || te.isAirborneVTOLorWIGE()) && !ae.isAirborne() && !ae.isAirborneVTOLorWIGE()) { toHit.addModifier(+1, Messages.getString("WeaponAttackAction.ShakyStick")); } @@ -4083,12 +4106,12 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T } return toHit; } - + /** * Convenience method that compiles the ToHit modifiers applicable to the defender's condition and actions * -4 for shooting at an immobile target? You'll find that here. * Attacker strafing? Using a weapon with a TH penalty? Those are in other methods. - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param target The Targetable object being attacked @@ -4096,16 +4119,16 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T * @param los The calculated LOS between attacker and target * @param toHit The running total ToHitData for this WeaponAttackAction * @param toSubtract An int value representing a running total of mods to disregard - used for some special attacks - * + * * @param aimingAt An int value representing the location being aimed at - used by immobile target calculations * @param aimingMode An int value that determines the reason aiming is allowed - used by immobile target calculations * @param distance The distance in hexes from attacker to target - * + * * @param wtype The WeaponType of the weapon being used * @param weapon The Mounted weapon being used * @param atype The AmmoType being used for this attack * @param munition Long indicating the munition type flag being used, if applicable - * + * * @param isArtilleryDirect flag that indicates whether this is a direct-fire artillery attack * @param isArtilleryIndirect flag that indicates whether this is an indirect-fire artillery attack * @param isAttackerInfantry flag that indicates whether the attacker is an infantry/BA unit @@ -4128,28 +4151,28 @@ private static ToHitData compileTargetToHitMods(Game game, Entity ae, Targetable // Can't handle these attacks without a valid attacker and target return toHit; } - + if (toHit == null) { // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + //Target's hex Hex targHex = game.getBoard().getHex(target.getPosition()); - + Entity te = null; if (ttype == Targetable.TYPE_ENTITY) { //Some weapons only target valid entities te = (Entity) target; } - + // Modifiers related to a special action the target is taking - + // evading bonuses if ((te != null) && te.isEvading()) { toHit.addModifier(te.getEvasionBonus(), Messages.getString("WeaponAttackAction.TeEvading")); } - + // Hull Down if ((te != null) && te.isHullDown()) { if ((te instanceof Mech) && !(te instanceof QuadVee && te.getConversionMode() == QuadVee.CONV_MODE_VEHICLE) @@ -4177,14 +4200,14 @@ else if ((te instanceof Tank || (te instanceof QuadVee && te.getConversionMode() } } } - + // Infantry taking cover per TacOps special rules if ((te instanceof Infantry) && ((Infantry) te).isTakingCover()) { if (te.getPosition().direction(ae.getPosition()) == te.getFacing()) { toHit.addModifier(+3, Messages.getString("WeaponAttackAction.FireThruCover")); } } - + // target prone ToHitData proneMod = null; if ((te != null) && te.isProne()) { @@ -4201,9 +4224,9 @@ else if ((te instanceof Tank || (te instanceof QuadVee && te.getConversionMode() toHit.append(proneMod); toSubtract += proneMod.getValue(); } - + // Special effects affecting the target - + // Target grappled? if (te != null) { int grapple = te.getGrappled(); @@ -4223,9 +4246,9 @@ else if ((te instanceof Tank || (te instanceof QuadVee && te.getConversionMode() } } } - + // Special Equipment and Quirks that the target possesses - + // ECM suite generating Ghost Targets if (game.getOptions().booleanOption(OptionsConstants.ADVANCED_TACOPS_GHOST_TARGET) && !isIndirect && !isArtilleryIndirect && !isArtilleryDirect) { @@ -4236,7 +4259,7 @@ else if ((te instanceof Tank || (te instanceof QuadVee && te.getConversionMode() bapMod = 1; } int tcMod = 0; - if (ae.hasTargComp() && wtype != null + if (ae.hasTargComp() && wtype != null && wtype.hasFlag(WeaponType.F_DIRECT_FIRE) && !wtype.hasFlag(WeaponType.F_CWS) && !wtype.hasFlag(WeaponType.F_TASER) && (atype != null) && (!usesAmmo || !(((atype.getAmmoType() == AmmoType.T_AC_LBX) @@ -4261,7 +4284,7 @@ else if ((te instanceof Tank || (te instanceof QuadVee && te.getConversionMode() } // Movement and Position modifiers - + // target movement - ignore for pointblank shots from hidden units if ((te != null) && !isPointBlankShot) { ToHitData thTemp = Compute.getTargetMovementModifier(game, target.getId()); @@ -4269,10 +4292,10 @@ else if ((te instanceof Tank || (te instanceof QuadVee && te.getConversionMode() toSubtract += thTemp.getValue(); // semiguided ammo negates this modifier, if TAG succeeded - if ((atype != null) && ((atype.getAmmoType() == AmmoType.T_LRM) + if ((atype != null) && ((atype.getAmmoType() == AmmoType.T_LRM) || (atype.getAmmoType() == AmmoType.T_LRM_IMP) || (atype.getAmmoType() == AmmoType.T_MML) - || (atype.getAmmoType() == AmmoType.T_NLRM) + || (atype.getAmmoType() == AmmoType.T_NLRM) || (atype.getAmmoType() == AmmoType.T_MEK_MORTAR)) && (munition == AmmoType.M_SEMIGUIDED) && (te.getTaggedBy() != -1)) { int nAdjust = thTemp.getValue(); @@ -4282,7 +4305,7 @@ else if ((te instanceof Tank || (te instanceof QuadVee && te.getConversionMode() } // precision ammo reduces this modifier else if ((atype != null) - && ((atype.getAmmoType() == AmmoType.T_AC) + && ((atype.getAmmoType() == AmmoType.T_AC) || (atype.getAmmoType() == AmmoType.T_LAC) || (atype.getAmmoType() == AmmoType.T_AC_IMP) || (atype.getAmmoType() == AmmoType.T_PAC)) @@ -4293,7 +4316,7 @@ else if ((atype != null) } } } - + // Ground-to-air attacks against a target flying at NOE if (Compute.isGroundToAir(ae, target) && (null != te) && te.isNOE()) { if (te.passedWithin(ae.getPosition(), 1)) { @@ -4313,9 +4336,9 @@ else if ((atype != null) } toHit.addModifier(vMod, Messages.getString("WeaponAttackAction.TeVelocity")); } - + // target immobile - boolean mekMortarMunitionsIgnoreImmobile = wtype != null && wtype.hasFlag(WeaponType.F_MEK_MORTAR) + boolean mekMortarMunitionsIgnoreImmobile = wtype != null && wtype.hasFlag(WeaponType.F_MEK_MORTAR) && (atype != null) && (munition == AmmoType.M_AIRBURST); if (wtype != null && !(wtype instanceof ArtilleryCannonWeapon) && !mekMortarMunitionsIgnoreImmobile) { ToHitData immobileMod; @@ -4326,15 +4349,15 @@ else if ((atype != null) } else { immobileMod = Compute.getImmobileMod(target, aimingAt, aimingMode); } - + if (immobileMod != null) { toHit.append(immobileMod); toSubtract += immobileMod.getValue(); } } - + // Unit-specific modifiers - + // -1 to hit a SuperHeavy mech if ((te instanceof Mech) && ((Mech) te).isSuperHeavy()) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.TeSuperheavyMech")); @@ -4349,7 +4372,7 @@ else if ((atype != null) if ((te instanceof SmallCraft) && (te.getUnitType() == UnitType.SMALL_CRAFT) && !te.isAirborne() && !te.isSpaceborne()) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.TeGroundedSmallCraft")); } - + // Battle Armor targets are hard for Meks and Tanks to hit. if (!isAttackerInfantry && (te instanceof BattleArmor)) { toHit.addModifier(1, Messages.getString("WeaponAttackAction.BaTarget")); @@ -4399,7 +4422,7 @@ else if ((atype != null) if (game.getOptions().booleanOption(OptionsConstants.ADVAERORULES_STRATOPS_SENSOR_SHADOW) && game.getBoard().inSpace()) { for (Entity en : Compute.getAdjacentEntitiesAlongAttack(ae.getPosition(), target.getPosition(), game)) { - if (!en.isEnemyOf(te) && en.isLargeCraft() + if (!en.isEnemyOf(te) && en.isLargeCraft() && ((en.getWeight() - te.getWeight()) >= -STRATOPS_SENSOR_SHADOW_WEIGHT_DIFF)) { toHit.addModifier(+1, Messages.getString("WeaponAttackAction.SensorShadow")); break; @@ -4432,7 +4455,7 @@ else if ((atype != null) * Woods along the LOS? Target Underwater? Partial cover? You'll find that here. * Also, if the to-hit table is changed due to cover/angle/elevation, look here. * -4 for shooting at an immobile target? Using a weapon with a TH penalty? Those are in other methods. - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param target The Targetable object being attacked @@ -4445,15 +4468,15 @@ else if ((atype != null) * @param toHit The running total ToHitData for this WeaponAttackAction * @param losMods A cached set of LOS-related modifiers * @param toSubtract An int value representing a running total of mods to disregard - used for some special attacks - * + * * @param eistatus An int value representing the ei cockpit/pilot upgrade status - * + * * @param wtype The WeaponType of the weapon being used * @param weapon The Mounted weapon being used - * @param weaponId The id number of the weapon being used - used by some external calculations + * @param weaponId The id number of the weapon being used - used by some external calculations * @param atype The AmmoType being used for this attack * @param munition Long indicating the munition type flag being used, if applicable - * + * * @param inSameBuilding flag that indicates whether this attack originates from within the same building * @param isIndirect flag that indicates whether this is an indirect attack (LRM, mortar...) * @param isPointBlankShot flag that indicates whether or not this is a PBS by a hidden unit @@ -4467,47 +4490,47 @@ private static ToHitData compileTerrainAndLosToHitMods(Game game, Entity ae, Tar // Can't handle these attacks without a valid attacker and target return toHit; } - + if (toHit == null) { // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + //Target's hex Hex targHex = game.getBoard().getHex(target.getPosition()); - + boolean targetHexContainsWater = targHex != null && targHex.containsTerrain(Terrains.WATER); boolean targetHexContainsFortified = targHex != null && targHex.containsTerrain(Terrains.FORTIFIED); - + Entity te = null; if (ttype == Targetable.TYPE_ENTITY) { //Some of these weapons only target valid entities te = (Entity) target; } - + // Add range mods - If the attacker and target are in the same building // & hex, range mods don't apply (and will cause the shot to fail) // Don't apply this to bomb attacks either, which are going to be at 0 range of necessity if (((los.getThruBldg() == null) || !los.getTargetPosition().equals(ae.getPosition())) - && (wtype != null + && (wtype != null && (!(wtype.hasFlag(WeaponType.F_ALT_BOMB) || wtype.hasFlag(WeaponType.F_DIVE_BOMB)))) && weaponId > WeaponType.WEAPON_NA) { toHit.append(Compute.getRangeMods(game, ae, weaponId, target)); } - + // add in LOS mods that we've been keeping toHit.append(losMods); - + // Attacker Terrain toHit.append(Compute.getAttackerTerrainModifier(game, ae.getId())); - + // Target Terrain - + // BMM p. 31, semi-guided indirect missile attacks vs tagged targets ignore terrain modifiers - boolean semiGuidedIndirectVsTaggedTarget = isIndirect && - (atype != null) && atype.getMunitionType() == AmmoType.M_SEMIGUIDED && + boolean semiGuidedIndirectVsTaggedTarget = isIndirect && + (atype != null) && atype.getMunitionType() == AmmoType.M_SEMIGUIDED && Compute.isTargetTagged(target, game); - + // Base terrain calculations, not applicable when delivering minefields or bombs // also not applicable in pointblank shots from hidden units if ((ttype != Targetable.TYPE_MINEFIELD_DELIVER) && !isPointBlankShot && !semiGuidedIndirectVsTaggedTarget) { @@ -4515,7 +4538,7 @@ private static ToHitData compileTerrainAndLosToHitMods(Game game, Entity ae, Tar toSubtract += Compute.getTargetTerrainModifier(game, target, eistatus, inSameBuilding, underWater) .getValue(); } - + // Fortified/Dug-In Infantry if ((target instanceof Infantry) && wtype != null && !wtype.hasFlag(WeaponType.F_FLAMER)) { if (targetHexContainsFortified @@ -4531,14 +4554,14 @@ private static ToHitData compileTerrainAndLosToHitMods(Game game, Entity ae, Tar } if ((te != null) && targetHexContainsWater // target in partial water - && (targHex.terrainLevel(Terrains.WATER) == partialWaterLevel) && (targEl == 0) && (te.height() > 0)) { + && (targHex.terrainLevel(Terrains.WATER) == partialWaterLevel) && (targEl == 0) && (te.height() > 0)) { los.setTargetCover(los.getTargetCover() | LosEffects.COVER_HORIZONTAL); losMods = los.losModifiers(game, eistatus, underWater); } - + // Change hit table for partial cover, accommodate for partial underwater (legs) if (los.getTargetCover() != LosEffects.COVER_NONE) { - if (underWater && (targetHexContainsWater && (targEl == 0) + if (underWater && (targetHexContainsWater && (targEl == 0) && (te != null && te.height() > 0))) { // weapon underwater, target in partial water toHit.setHitTable(ToHitData.HIT_PARTIAL_COVER); @@ -4562,9 +4585,9 @@ private static ToHitData compileTerrainAndLosToHitMods(Game game, Entity ae, Tar toHit.setCoverBuildingSecondary(los.getCoverBuildingSecondary()); } } - + // Special Equipment - + // if we have BAP and there are woods in the // way, and we are within BAP range, we reduce the BTH by 1 // Per TacOps errata, this bonus also applies to all units on the same C3 network @@ -4576,7 +4599,7 @@ private static ToHitData compileTerrainAndLosToHitMods(Game game, Entity ae, Tar || (los.getLightWoods() > 0) || (los.getHeavyWoods() > 0) || (los.getUltraWoods() > 0)) || ae.hasNetworkBAP()) { if (ae.hasBAP()) { - // If you want the bonus, the entity with the BAP should fire first. + // If you want the bonus, the entity with the BAP should fire first. // Not sure how to get around this for (Entity en : game.getC3NetworkMembers(ae)) { en.setNetworkBAP(true); @@ -4584,24 +4607,24 @@ private static ToHitData compileTerrainAndLosToHitMods(Game game, Entity ae, Tar } toHit.addModifier(-1, Messages.getString("WeaponAttackAction.BAPInWoods")); } - + // To-hit table changes with no to-hit modifiers - + // Aeros in air-to-air combat can hit above and below if (Compute.isAirToAir(ae, target)) { if ((ae.getAltitude() - target.getAltitude()) > 2) { toHit.setHitTable(ToHitData.HIT_ABOVE); } else if ((target.getAltitude() - ae.getAltitude()) > 2) { toHit.setHitTable(ToHitData.HIT_BELOW); - } else if (((ae.getAltitude() - target.getAltitude()) > 0) + } else if (((ae.getAltitude() - target.getAltitude()) > 0) && te != null && (te.isAero() && ((IAero) te).isSpheroid())) { toHit.setHitTable(ToHitData.HIT_ABOVE); - } else if (((ae.getAltitude() - target.getAltitude()) < 0) + } else if (((ae.getAltitude() - target.getAltitude()) < 0) && te != null && (te.isAero() && ((IAero) te).isSpheroid())) { toHit.setHitTable(ToHitData.HIT_BELOW); } } - + // Change hit table for elevation differences inside building. if ((null != los.getThruBldg()) && (aElev != tElev)) { @@ -4617,12 +4640,12 @@ private static ToHitData compileTerrainAndLosToHitMods(Game game, Entity ae, Tar } } } - + // Ground-to-air attacks always hit from below if (Compute.isGroundToAir(ae, target) && ((ae.getAltitude() - target.getAltitude()) > 2)) { toHit.setHitTable(ToHitData.HIT_BELOW); } - + // factor in target side if (isAttackerInfantry && (0 == distance)) { // Infantry attacks from the same hex are resolved against the @@ -4633,15 +4656,15 @@ private static ToHitData compileTerrainAndLosToHitMods(Game game, Entity ae, Tar toHit.setSideTable(Compute.targetSideTable(ae, target, weapon.getCalledShot().getCall())); } } - + // Change hit table for surface naval vessels hit by underwater attacks if (underWater && targetHexContainsWater && (null != te) && te.isSurfaceNaval()) { toHit.setHitTable(ToHitData.HIT_UNDERWATER); } - + return toHit; } - + /** * Quick routine to determine if the target should be treated as being in a short building. */ @@ -4649,14 +4672,14 @@ public static boolean targetInShortCoverBuilding(Targetable target) { if (target.getTargetType() != Targetable.TYPE_ENTITY) { return false; } - + Entity targetEntity = (Entity) target; - + Hex targetHex = targetEntity.getGame().getBoard().getHex(target.getPosition()); if (targetHex == null) { return false; } - + // the idea here is that we're in a building that provides partial cover // if the unit involved is tall (at least 2 levels, e.g. mech or superheavy vehicle) // and its height above the hex ceiling (i.e building roof) is 1 @@ -4665,17 +4688,17 @@ public static boolean targetInShortCoverBuilding(Targetable target) { (targetEntity.getHeight() > 0) && (targetEntity.relHeight() == 1); } - + /** * If you're using a weapon that does something totally special and doesn't apply mods like everything else, look here - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param target The Targetable object being attacked * @param ttype The targetable object type * @param los The calculated LOS between attacker and target * @param toHit The running total ToHitData for this WeaponAttackAction - * + * * @param wtype The WeaponType of the weapon being used * @param atype The AmmoType being used for this attack * @param srt Class that stores whether or not this WAA should return a special resolution @@ -4686,29 +4709,29 @@ private static ToHitData handleSpecialWeaponAttacks(Game game, Entity ae, Target //*Should* be impossible at this point in the process return toHit; } - + Entity te = null; if (ttype == Targetable.TYPE_ENTITY) { //Some of these weapons only target valid entities te = (Entity) target; } - + if (toHit == null) { // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + // Battle Armor bomb racks (Micro bombs) use gunnery skill and no other mods per TWp228 2018 errata if ((atype != null) && (atype.getAmmoType() == AmmoType.T_BA_MICRO_BOMB)) { if (ae.getPosition().equals(target.getPosition())) { toHit = new ToHitData(ae.getCrew().getGunnery(), Messages.getString("WeaponAttackAction.GunSkill")); - } else { + } else { toHit = new ToHitData(TargetRoll.IMPOSSIBLE, Messages.getString("WeaponAttackAction.OutOfRange")); } srt.setSpecialResolution(true); return toHit; } - + // Engineer's fire extinguisher has fixed to hit number, // Note that coolant trucks make a regular attack. if (wtype.hasFlag(WeaponType.F_EXTINGUISHER)) { @@ -4724,7 +4747,7 @@ private static ToHitData handleSpecialWeaponAttacks(Game game, Entity ae, Target srt.setSpecialResolution(true); return toHit; } - + // if this is a space bombing attack then get the to hit and return if (wtype.hasFlag(WeaponType.F_SPACE_BOMB)) { if (te != null) { @@ -4736,16 +4759,16 @@ private static ToHitData handleSpecialWeaponAttacks(Game game, Entity ae, Target //If we get here, no special weapons apply. Return the input data and continue on return toHit; } - + /** * Convenience method that compiles the ToHit modifiers applicable to infantry/BA swarm attacks - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param target The Targetable object being attacked * @param ttype The targetable object type * @param toHit The running total ToHitData for this WeaponAttackAction - * + * * @param wtype The WeaponType of the weapon being used * @param srt Class that stores whether or not this WAA should return a special resolution */ @@ -4759,12 +4782,12 @@ private static ToHitData handleInfantrySwarmAttacks(Game game, Entity ae, Target //Can only swarm a valid entity target return toHit; } - + if (toHit == null) { // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + Entity te = (Entity) target; // Leg attacks and Swarm attacks have their own base toHit values if (Infantry.LEG_ATTACK.equals(wtype.getInternalName())) { @@ -4847,10 +4870,10 @@ else if ((ae.getSwarmTargetId() == te.getId())) { //If we get here, no swarm attack applies return toHit; } - + /** * Method to handle modifiers for swarm missile secondary targets - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param target The Targetable object being attacked @@ -4858,15 +4881,15 @@ else if ((ae.getSwarmTargetId() == te.getId())) { * @param swarmSecondaryTarget The current Targetable object being attacked * @param toHit The running total ToHitData for this WeaponAttackAction * @param toSubtract An int value representing a running total of mods to disregard - * + * * @param eistatus An int value representing the ei cockpit/pilot upgrade status - used for terrain calculation * @param aimingAt An int value representing the location being aimed at - used for immobile target * @param aimingMode An int value that determines the reason aiming is allowed - used for immobile target - * + * * @param weapon The Mounted weapon being used * @param atype The AmmoType being used for this attack * @param munition Long indicating the munition type flag being used, if applicable - * + * * @param isECMAffected flag that indicates whether the target is inside an ECM bubble * @param inSameBuilding flag that indicates whether this attack originates from within the same building * @param underWater flag that indicates whether the weapon being used is underwater @@ -4884,23 +4907,23 @@ private static ToHitData handleSwarmSecondaryAttacks(Game game, Entity ae, Targe // This method won't work without these 3 things return toHit; } - + if (toHit == null) { // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + toHit.addModifier(-toSubtract, Messages.getString("WeaponAttackAction.OriginalTargetMods")); toHit.append(Compute.getImmobileMod(swarmSecondaryTarget, aimingAt, aimingMode)); toHit.append(Compute.getTargetTerrainModifier(game, game.getTarget(swarmSecondaryTarget.getTargetType(), swarmSecondaryTarget.getId()), eistatus, inSameBuilding, underWater)); toHit.setCover(LosEffects.COVER_NONE); - + Hex targHex = game.getBoard().getHex(swarmSecondaryTarget.getPosition()); int targEl = swarmSecondaryTarget.relHeight(); int distance = Compute.effectiveDistance(game, ae, swarmSecondaryTarget); - + // We might not attack the new target from the same side as the // old, so recalculate; the attack *direction* is still traced from // the original source. @@ -4969,20 +4992,20 @@ private static ToHitData handleSwarmSecondaryAttacks(Game game, Entity ae, Targe } return toHit; } - + /** * Convenience method that compiles the ToHit modifiers applicable to artillery attacks - * + * * @param game The current {@link Game} * @param ae The Entity making this attack * @param target The Targetable object being attacked * @param ttype The targetable object type * @param toHit The running total ToHitData for this WeaponAttackAction - * + * * @param wtype The WeaponType of the weapon being used * @param weapon The Mounted weapon being used * @param atype The AmmoType being used for this attack - * + * * @param isArtilleryDirect flag that indicates whether this is a direct-fire artillery attack * @param isArtilleryFLAK flag that indicates whether this is a flak artillery attack * @param isArtilleryIndirect flag that indicates whether this is an indirect-fire artillery attack @@ -4991,32 +5014,32 @@ private static ToHitData handleSwarmSecondaryAttacks(Game game, Entity ae, Targe * @param srt Class that stores whether or not this WAA should return a special resolution */ private static ToHitData handleArtilleryAttacks(Game game, Entity ae, Targetable target, int ttype, - ToHitData losMods, ToHitData toHit, WeaponType wtype, Mounted weapon, AmmoType atype, + ToHitData losMods, ToHitData toHit, WeaponType wtype, Mounted weapon, AmmoType atype, boolean isArtilleryDirect, boolean isArtilleryFLAK, boolean isArtilleryIndirect, boolean isHoming, boolean usesAmmo, SpecialResolutionTracker srt) { Entity te = null; if (ttype == Targetable.TYPE_ENTITY) { te = (Entity) target; } - + if (toHit == null) { // Without valid toHit data, the rest of this will fail toHit = new ToHitData(); } - + //Homing warheads just need a flat 4 to seek out a successful TAG - if (isHoming) { + if (isHoming) { srt.setSpecialResolution(true); return new ToHitData(4, Messages.getString("WeaponAttackAction.HomingArty")); } - + //Don't bother adding up modifiers if the target hex has been hit before if (game.getEntity(ae.getId()).getOwner().getArtyAutoHitHexes().contains(target.getPosition()) && !isArtilleryFLAK) { srt.setSpecialResolution(true); return new ToHitData(TargetRoll.AUTOMATIC_SUCCESS, Messages.getString("WeaponAttackAction.ArtyDesTarget")); } - + // Handle direct artillery attacks. if (isArtilleryDirect) { //If an airborne unit occupies the target hex, standard artillery ammo makes a flak attack against it @@ -5072,7 +5095,7 @@ private static ToHitData handleArtilleryAttacks(Game game, Entity ae, Targetable } toHit.addModifier(mod, Messages.getString("WeaponAttackAction.IndirectArty")); int adjust = 0; - if (weapon != null) { + if (weapon != null) { adjust = ae.aTracker.getModifier(weapon, target.getPosition()); } boolean spotterIsForwardObserver = ae.aTracker.getSpotterHasForwardObs(); From 9e881cdf38bf0f45945917023a001532c390b975 Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Wed, 14 Jun 2023 20:01:49 -0700 Subject: [PATCH 10/13] avoid instanceof by moving summary method to interface --- .../client/ui/swing/MovementDisplay.java | 50 ++++- .../boardview/AbstractBoardViewOverlay.java | 18 +- .../boardview/PlanetaryConditionsOverlay.java | 25 ++- .../swing/boardview/TurnDetailsOverlay.java | 4 +- .../ui/swing/tooltip/EntityActionLog.java | 192 +----------------- .../common/actions/AbstractAttackAction.java | 9 +- .../common/actions/AbstractEntityAction.java | 25 ++- .../common/actions/ChargeAttackAction.java | 6 + .../common/actions/ClubAttackAction.java | 20 +- .../common/actions/DfaAttackAction.java | 7 + .../megamek/common/actions/EntityAction.java | 3 + .../common/actions/KickAttackAction.java | 55 +++-- .../ProtomechPhysicalAttackAction.java | 13 +- .../common/actions/PunchAttackAction.java | 37 +++- .../common/actions/PushAttackAction.java | 9 +- .../actions/SearchlightAttackAction.java | 49 ++--- .../megamek/common/actions/SpotAction.java | 28 ++- .../actions/TeleMissileAttackAction.java | 55 ++--- .../common/actions/WeaponAttackAction.java | 10 + 19 files changed, 296 insertions(+), 319 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index dfe43bfd7a..514ab8566e 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -22,6 +22,8 @@ import megamek.client.event.BoardViewEvent; import megamek.client.ui.Messages; import megamek.client.ui.SharedUtility; +import megamek.client.ui.swing.boardview.AbstractBoardViewOverlay; +import megamek.client.ui.swing.boardview.TurnDetailsOverlay; import megamek.client.ui.swing.util.CommandAction; import megamek.client.ui.swing.util.KeyCommandBind; import megamek.client.ui.swing.util.MegaMekController; @@ -55,6 +57,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static megamek.client.ui.swing.util.UIUtil.colorString; import static megamek.common.MiscType.F_CHAFF_POD; import static megamek.common.options.OptionsConstants.ADVGRNDMOV_TACOPS_ZIPLINES; @@ -339,6 +342,7 @@ public static MoveCommand[] values(int f, GameOptions opts, boolean forwardIni) public static final int GEAR_LONGEST_RUN = 10; public static final int GEAR_LONGEST_WALK = 11; public static final int GEAR_STRAFE = 12; + public static final String turnDetailsFormat = "%s%-3s %-12s %1s %2dMP%s"; /** * Creates and lays out a new movement phase display for the specified @@ -1058,45 +1062,73 @@ protected void updateDonePanel() { } else if (!possible.isMoveLegal()) { updateDonePanelButtons(Messages.getString("MovementDisplay.IllegalMove"), Messages.getString("MovementDisplay.Skip"), false, null); } else { + String validTextColor = AbstractBoardViewOverlay.colorToHex( AbstractBoardViewOverlay.getTextColor()); + String invalidTextColor = AbstractBoardViewOverlay.colorToHex( AbstractBoardViewOverlay.getTextColor(), 0.7f); + int mp = possible.countMp(possible.isJumping()); boolean psrCheck = (!SharedUtility.doPSRCheck(cmd.clone()).isBlank()) || (!SharedUtility.doThrustCheck(cmd.clone(), clientgui.getClient()).isBlank()); boolean damageCheck = cmd.shouldMechanicalJumpCauseFallDamage() || cmd.hasActiveMASC() || (!(ce() instanceof VTOL) && cmd.hasActiveSupercharger()) || cmd.willCrushBuildings(); -// final String format = "%-12s x%2d (%2dMP)%s"; - final String format = "%-3s %-12s %2dMP%s"; - MoveStepType accumType = null; int accumTypeCount = 0; int accumMP = 0; int accumDanger = 0; - ArrayList turnDetails = new ArrayList(); + boolean accumLegal = true; + String unicodeIcon = ""; + ArrayList turnDetails = new ArrayList<>(); for (final Enumeration step = cmd.getSteps(); step.hasMoreElements(); ) { MoveStep currentStep = step.nextElement(); MoveStepType currentType = currentStep.getType(); int currentDanger = currentStep.isDanger() ? 1 : 0; + boolean currentLegal = currentStep.isLegal(cmd); - if (accumTypeCount != 0 && accumType == currentType) { + if (accumTypeCount != 0 && accumType == currentType && accumLegal == currentLegal) { accumTypeCount++; accumMP += currentStep.getMp(); accumDanger += currentDanger; continue; } + // switching to a new move type, so write a line if (accumTypeCount != 0) { - turnDetails.add(String.format(format, + turnDetails.add(String.format(turnDetailsFormat, + accumLegal ? validTextColor : invalidTextColor, accumTypeCount == 1 ? "" : "x"+accumTypeCount, - accumType, accumMP, "*".repeat(accumDanger))); + accumType, unicodeIcon, accumMP, "*".repeat(accumDanger))); } + // switching to a new move type, reset accumType = currentType; accumTypeCount = 1; accumMP = currentStep.getMp(); accumDanger = currentDanger; + accumLegal = currentLegal; + switch (accumType) { + case TURN_LEFT: + unicodeIcon = "\u21B0"; + break; + case TURN_RIGHT: + unicodeIcon = "\u21B1"; + break; + case FORWARDS: + unicodeIcon = "\u2191"; + break; + case BACKWARDS: + unicodeIcon = "\u2193"; + break; + case START_JUMP: + unicodeIcon = "\u21EF"; + break; + default: + unicodeIcon = ""; + break; + } } - turnDetails.add(String.format(format, + turnDetails.add(String.format(turnDetailsFormat, + accumLegal ? validTextColor : invalidTextColor, accumTypeCount == 1 ? "" : "x"+accumTypeCount, - accumType, accumMP, "*".repeat(accumDanger))); + accumType, unicodeIcon, accumMP, "*".repeat(accumDanger))); String moveMsg = Messages.getString("MovementDisplay.Move") + " (" + mp + "MP)" + (psrCheck ? "*" : "") + (psrCheck ? "*" : "") + (damageCheck ? "!" : ""); diff --git a/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java index 3dc3617c3e..7436561047 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java @@ -53,7 +53,7 @@ public abstract class AbstractBoardViewOverlay implements IDisplayable, IPreferenceChangeListener { private static final int PADDING_X = 10; private static final int PADDING_Y = 5; - private static final Color SHADOW_COLOR = Color.DARK_GRAY; + private static final Color SHADOW_COLOR = java.awt.Color.DARK_GRAY; private static final float FADE_SPEED = 0.2f; ClientGUI clientGui; protected static final GUIPreferences GUIP = GUIPreferences.getInstance(); @@ -200,7 +200,7 @@ private Rectangle getSize(Graphics graph, List lines, FontMetrics fm) { * otherwise TEXT_COLOR is used. */ private void drawShadowedString(Graphics graph, String s, int x, int y) { - Color textColor = getTextColorGUIPreference(); + Color textColor = getTextColor(); // Extract a color code from the start of the string // used to display headlines if it's there if (s.startsWith("#") && s.length() > 7) { @@ -307,8 +307,7 @@ protected void gameTurnOrPhaseChange() { setDirty(); } - protected Color getTextColorGUIPreference() { - // FIXME there is currently no custom option for key bindings + public static Color getTextColor() { return GUIP.getPlanetaryConditionsColorText(); } @@ -317,4 +316,15 @@ protected Color getTextColorGUIPreference() { protected abstract int getDistTop(Rectangle clipBounds, int overlayHeight); protected abstract int getDistSide(Rectangle clipBounds, int overlayWidth); + public static String colorToHex(Color color) { + return String.format("#%02X%02X%02X", + color.getRed(), color.getGreen(),color.getBlue(), color.getAlpha()); + } + + public static String colorToHex(Color color, float brightnessMultiplier) { + return String.format("#%02X%02X%02X", + (int)(color.getRed() * brightnessMultiplier), (int)(color.getGreen() * brightnessMultiplier), + (int)(color.getBlue() * brightnessMultiplier), (int)(color.getAlpha())); + } + } diff --git a/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java index d3f97eba42..8b870711b9 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/PlanetaryConditionsOverlay.java @@ -32,7 +32,6 @@ * */ public class PlanetaryConditionsOverlay extends AbstractBoardViewOverlay { - private static final String MSG_HEADING = Messages.getString("PlanetaryConditionsOverlay.heading"); private static final String MSG_TEMPERATURE = Messages.getString("PlanetaryConditionsOverlay.Temperature"); private static final String MSG_GRAVITY = Messages.getString("PlanetaryConditionsOverlay.Gravity"); private static final String MSG_LIGHT = Messages.getString("PlanetaryConditionsOverlay.Light"); @@ -50,7 +49,7 @@ public class PlanetaryConditionsOverlay extends AbstractBoardViewOverlay { */ public PlanetaryConditionsOverlay(Game game, ClientGUI cg) { super(game, cg, new Font("SansSerif", Font.PLAIN, 13), - Messages.getString(MSG_HEADING, KeyCommandBind.getDesc(KeyCommandBind.PLANETARY_CONDITIONS)) ); + Messages.getString("PlanetaryConditionsOverlay.heading", KeyCommandBind.getDesc(KeyCommandBind.PLANETARY_CONDITIONS))); } /** @return an ArrayList of all text lines to be shown. */ @@ -68,9 +67,9 @@ protected List assembleTextLines() { int temp = currentGame.getPlanetaryConditions().getTemperature(); if (currentGame.getPlanetaryConditions().isExtremeTemperatureHeat()) { - tempColor = String.format("#%02X%02X%02X", colorHot.getRed(), colorHot.getGreen(), colorHot.getBlue()); + tempColor = colorToHex(colorHot); } else if (currentGame.getPlanetaryConditions().isExtremeTemperatureCold()) { - tempColor = String.format("#%02X%02X%02X", colorCold.getRed(), colorCold.getGreen(), colorCold.getBlue()); + tempColor = colorToHex(colorHot); } boolean showDefaultConditions = GUIP.getPlanetaryConditionsShowDefaults(); @@ -81,14 +80,14 @@ protected List assembleTextLines() { String tmpStr; - if (((showDefaultConditions) || ((!showDefaultConditions) && (currentGame.getPlanetaryConditions().isExtremeTemperature())))) { + if (showDefaultConditions || (currentGame.getPlanetaryConditions().isExtremeTemperature())) { tmpStr = (showLabel ? MSG_TEMPERATURE + " " : ""); tmpStr = tmpStr + (showValue ? temp + "\u00B0C " : ""); tmpStr = tmpStr + (showIndicator ? (!showValue ? temp + "\u00B0C " : "" ) + currentGame.getPlanetaryConditions().getTemperatureIndicator() : ""); result.add(tempColor + tmpStr); } - if (((showDefaultConditions) || ((!showDefaultConditions) && (currentGame.getPlanetaryConditions().getGravity() != 1.0)))) { + if (showDefaultConditions || (currentGame.getPlanetaryConditions().getGravity() != 1.0)) { float grav = currentGame.getPlanetaryConditions().getGravity(); tmpStr = (showLabel ? MSG_GRAVITY + " " : ""); tmpStr = tmpStr + (showValue ? grav + "g " : ""); @@ -96,35 +95,35 @@ protected List assembleTextLines() { result.add(tmpStr); } - if (((showDefaultConditions) || ((!showDefaultConditions) && (currentGame.getPlanetaryConditions().getLight() != PlanetaryConditions.L_DAY)))) { + if (showDefaultConditions || (currentGame.getPlanetaryConditions().getLight() != PlanetaryConditions.L_DAY)) { tmpStr = (showLabel ? MSG_LIGHT + " " : ""); tmpStr = tmpStr + (showValue ? currentGame.getPlanetaryConditions().getLightDisplayableName() + " " : ""); tmpStr = tmpStr + (showIndicator ? currentGame.getPlanetaryConditions().getLightIndicator() : ""); result.add(tmpStr); } - if (((showDefaultConditions) || ((!showDefaultConditions) && (currentGame.getPlanetaryConditions().getAtmosphere() != PlanetaryConditions.ATMO_STANDARD)))) { + if (showDefaultConditions || (currentGame.getPlanetaryConditions().getAtmosphere() != PlanetaryConditions.ATMO_STANDARD)) { tmpStr = (showLabel ? MSG_ATMOSPHERICPREASSURE + " " : ""); tmpStr = tmpStr + (showValue ? currentGame.getPlanetaryConditions().getAtmosphereDisplayableName() + " " : ""); tmpStr = tmpStr + (showIndicator ? currentGame.getPlanetaryConditions().getAtmosphereIndicator() : ""); result.add(tmpStr); } - if (((showDefaultConditions) || ((!showDefaultConditions) && (currentGame.getPlanetaryConditions().hasEMI())))) { + if (showDefaultConditions || (currentGame.getPlanetaryConditions().hasEMI())) { tmpStr = (showLabel ? MSG_EMI + " " : ""); tmpStr = tmpStr + (showValue ? currentGame.getPlanetaryConditions().getEMIDisplayableValue() + " " : ""); tmpStr = tmpStr + (showIndicator ? currentGame.getPlanetaryConditions().getEMIIndicator() : ""); result.add(tmpStr); } - if (((showDefaultConditions) || ((!showDefaultConditions) && (currentGame.getPlanetaryConditions().getWeather() != PlanetaryConditions.WE_NONE)))) { + if (showDefaultConditions || (currentGame.getPlanetaryConditions().getWeather() != PlanetaryConditions.WE_NONE)) { tmpStr = (showLabel ? MSG_WEATHER + " " : ""); tmpStr = tmpStr + (showValue ? currentGame.getPlanetaryConditions().getWeatherDisplayableName() + " " : ""); tmpStr = tmpStr + (showIndicator ? currentGame.getPlanetaryConditions().getWeatherIndicator() : ""); result.add(tmpStr); } - if (((showDefaultConditions) || ((!showDefaultConditions) && (currentGame.getPlanetaryConditions().getWindStrength() != PlanetaryConditions.WI_NONE)))) { + if (showDefaultConditions || (currentGame.getPlanetaryConditions().getWindStrength() != PlanetaryConditions.WI_NONE)) { tmpStr = (showLabel ? MSG_WIND + " " : ""); tmpStr = tmpStr + (showValue ? currentGame.getPlanetaryConditions().getWindDisplayableName() + " " : ""); tmpStr = tmpStr + (showIndicator ? currentGame.getPlanetaryConditions().getWindStrengthIndicator() : ""); @@ -135,14 +134,14 @@ protected List assembleTextLines() { result.add(tmpStr); } - if (((showDefaultConditions) || ((!showDefaultConditions) && (currentGame.getPlanetaryConditions().getFog() != PlanetaryConditions.FOG_NONE)))) { + if (showDefaultConditions || (currentGame.getPlanetaryConditions().getFog() != PlanetaryConditions.FOG_NONE)) { tmpStr = (showLabel ? MSG_FOG + " " : ""); tmpStr = tmpStr + (showValue ? currentGame.getPlanetaryConditions().getFogDisplayableName() + " " : ""); tmpStr = tmpStr + (showIndicator ? currentGame.getPlanetaryConditions().getFogIndicator() : ""); result.add(tmpStr); } - if (((showDefaultConditions) || ((!showDefaultConditions) && (currentGame.getPlanetaryConditions().isSandBlowing())))) { + if (showDefaultConditions || (currentGame.getPlanetaryConditions().isSandBlowing())) { tmpStr = (showLabel ? MSG_BLOWINGSAND + " " : ""); tmpStr = tmpStr + (showValue ? currentGame.getPlanetaryConditions().getSandBlowingDisplayableValue() + " " : ""); tmpStr = tmpStr + (showIndicator ? currentGame.getPlanetaryConditions().getSandBlowingIndicator() : ""); diff --git a/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java index 8a384daede..aded002904 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/TurnDetailsOverlay.java @@ -36,8 +36,10 @@ public class TurnDetailsOverlay extends AbstractBoardViewOverlay { List lines = new ArrayList<>(); + static String validTextColor, invalidTextColor; + public TurnDetailsOverlay(Game game, ClientGUI cg) { - super(game, cg, new Font(Font.MONOSPACED, Font.PLAIN, 12), + super(game, cg, new Font(Font.MONOSPACED, Font.BOLD, 12), Messages.getString("TurnDetailsOverlay.heading", KeyCommandBind.getDesc(KeyCommandBind.TURN_DETAILS)) ); } diff --git a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java index b14511a92c..ad10c45d58 100644 --- a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java +++ b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java @@ -72,8 +72,7 @@ public void add(int index, EntityAction entityAction) { } /** - * Remove an item and its description cache - * @param entityAction + * Remove an item and its caches */ @Override public boolean remove(Object o) { @@ -138,37 +137,21 @@ void addDescription(EntityAction entityAction) { if (entityAction instanceof WeaponAttackAction) { // ToHit may change as other actions are added, // so always re-evaluate all WeaponAttack Actions - addEntityAction((WeaponAttackAction) entityAction); + addWeaponAttackAction((WeaponAttackAction) entityAction); } else if (infoCache.containsKey(entityAction)) { // reuse previous description descriptions.add(infoCache.get(entityAction)); - } else if (entityAction instanceof KickAttackAction) { - addEntityAction((KickAttackAction) entityAction); - } else if (entityAction instanceof PunchAttackAction) { - addEntityAction((PunchAttackAction) entityAction); - } else if (entityAction instanceof PushAttackAction) { - addEntityAction((PushAttackAction) entityAction); - } else if (entityAction instanceof ClubAttackAction) { - addEntityAction((ClubAttackAction) entityAction); - } else if (entityAction instanceof ChargeAttackAction) { - addEntityAction((ChargeAttackAction) entityAction); - } else if (entityAction instanceof DfaAttackAction) { - addEntityAction((DfaAttackAction) entityAction); - } else if (entityAction instanceof ProtomechPhysicalAttackAction) { - addEntityAction((ProtomechPhysicalAttackAction) entityAction); - } else if (entityAction instanceof SearchlightAttackAction) { - addEntityAction((SearchlightAttackAction) entityAction); - } else if (entityAction instanceof SpotAction) { - addEntityAction((SpotAction) entityAction); } else { - addEntityAction(entityAction); + final String buffer = entityAction.toSummaryString(game); + descriptions.add(buffer); + infoCache.put(entityAction, buffer); } } /** - * Adds a weapon to this attack + * Adds description, using an existing line if the same weapon type was already listed */ - void addEntityAction(WeaponAttackAction attack) { + void addWeaponAttackAction(WeaponAttackAction attack) { ToHitData toHit = attack.toHit(game, true); String table = toHit.getTableDesc(); final String buffer = toHit.getValueAsString() + ((!table.isEmpty()) ? ' '+table : ""); @@ -188,167 +171,8 @@ void addEntityAction(WeaponAttackAction attack) { } if (!found) { - descriptions.add(weaponName + Messages.getString("BoardView1.needs") + buffer); - } - } - - void addEntityAction(KickAttackAction attack) { - String buffer; - if (infoCache.containsKey(attack)) { - buffer = infoCache.get(attack); - } else { - String rollLeft; - String rollRight; - final int leg = attack.getLeg(); - switch (leg) { - case KickAttackAction.BOTH: - rollLeft = KickAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), KickAttackAction.LEFT).getValueAsString(); - rollRight = KickAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), KickAttackAction.RIGHT).getValueAsString(); - buffer = Messages.getString("BoardView1.kickBoth", rollLeft, rollRight); - break; - case KickAttackAction.LEFT: - rollLeft = KickAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), KickAttackAction.LEFT).getValueAsString(); - buffer = Messages.getString("BoardView1.kickLeft", rollLeft); - break; - case KickAttackAction.RIGHT: - rollRight = KickAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), KickAttackAction.RIGHT).getValueAsString(); - buffer = Messages.getString("BoardView1.kickRight", rollRight); - break; - default: - buffer = "Error on kick action"; - } - infoCache.put(attack, buffer); - } - descriptions.add(buffer); - } - - void addEntityAction(PunchAttackAction attack) { - String buffer; - if (infoCache.containsKey(attack)) { - buffer = infoCache.get(attack); - } else { - String rollLeft; - String rollRight; - final int arm = attack.getArm(); - switch (arm) { - case PunchAttackAction.BOTH: - rollLeft = PunchAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), PunchAttackAction.LEFT, false).getValueAsString(); - rollRight = PunchAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), PunchAttackAction.RIGHT, false).getValueAsString(); - buffer = Messages.getString("BoardView1.punchBoth", rollLeft, rollRight); - break; - case PunchAttackAction.LEFT: - rollLeft = PunchAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), PunchAttackAction.LEFT, false).getValueAsString(); - buffer = Messages.getString("BoardView1.punchLeft", rollLeft); - break; - case PunchAttackAction.RIGHT: - rollRight = PunchAttackAction.toHit(game, attack.getEntityId(), game.getTarget(attack.getTargetType(), attack.getTargetId()), PunchAttackAction.RIGHT, false).getValueAsString(); - buffer = Messages.getString("BoardView1.punchRight", rollRight); - break; - default: - buffer = "Error on punch action"; - } - infoCache.put(attack, buffer); - } - descriptions.add(buffer); - } - - void addEntityAction(PushAttackAction attack) { - String buffer; - if (infoCache.containsKey(attack)) { - buffer = infoCache.get(attack); - } else { - final String roll = attack.toHit(game).getValueAsString(); - buffer = Messages.getString("BoardView1.PushAttackAction", roll); - infoCache.put(attack, buffer); - } - descriptions.add(buffer); - } - - void addEntityAction(ClubAttackAction attack) { - String buffer; - if (infoCache.containsKey(attack)) { - buffer = infoCache.get(attack); - } else { - final String roll = attack.toHit(game).getValueAsString(); - final String club = attack.getClub().getName(); - buffer = Messages.getString("BoardView1.ClubAttackAction", club, roll); - infoCache.put(attack, buffer); - } - descriptions.add(buffer); - } - - void addEntityAction(ChargeAttackAction attack) { - String buffer; - if (infoCache.containsKey(attack)) { - buffer = infoCache.get(attack); - } else { - final String roll = attack.toHit(game).getValueAsString(); - buffer = Messages.getString("BoardView1.ChargeAttackAction", roll); - infoCache.put(attack, buffer); - } - descriptions.add(buffer); - } - - void addEntityAction(DfaAttackAction attack) { - String buffer; - if (infoCache.containsKey(attack)) { - buffer = infoCache.get(attack); - } else { - final String roll = attack.toHit(game).getValueAsString(); - buffer = Messages.getString("BoardView1.DfaAttackAction", roll); - infoCache.put(attack, buffer); - } - descriptions.add(buffer); - } - - void addEntityAction(ProtomechPhysicalAttackAction attack) { - String buffer; - if (infoCache.containsKey(attack)) { - buffer = infoCache.get(attack); - } else { - final String roll = attack.toHit(game).getValueAsString(); - buffer = Messages.getString("BoardView1.ProtomechPhysicalAttackAction", roll); - infoCache.put(attack, buffer); - } - descriptions.add(buffer); - } - - void addEntityAction(SearchlightAttackAction attack) { - String buffer; - if (infoCache.containsKey(attack)) { - buffer = infoCache.get(attack); - } else { - Entity target = game.getEntity(attack.getTargetId()); - buffer = Messages.getString("BoardView1.SearchlightAttackAction") + ((target != null) ? ' ' +target.getShortName() : ""); - infoCache.put(attack, buffer); - } - descriptions.add(buffer); - } - - void addEntityAction(SpotAction attack) { - String buffer; - - if (infoCache.containsKey(attack)) { - buffer = infoCache.get(attack); - } else { - Entity target = game.getEntity(attack.getTargetId()); - buffer = Messages.getString("BoardView1.SpotAction", (target != null) ? target.getShortName() : "" ); - infoCache.put(attack, buffer); - } - descriptions.add(buffer); - } - - void addEntityAction(EntityAction entityAction) { - String buffer; - - if (infoCache.containsKey(entityAction)) { - buffer = infoCache.get(entityAction); - } else { - String typeName = entityAction.getClass().getTypeName(); - buffer = typeName.substring(typeName.lastIndexOf('.') + 1); - infoCache.put(entityAction, buffer); + descriptions.add( weaponName + Messages.getString("BoardView1.needs") + buffer); } - descriptions.add(buffer); } @Override diff --git a/megamek/src/megamek/common/actions/AbstractAttackAction.java b/megamek/src/megamek/common/actions/AbstractAttackAction.java index dd71c634a0..4e008125bf 100644 --- a/megamek/src/megamek/common/actions/AbstractAttackAction.java +++ b/megamek/src/megamek/common/actions/AbstractAttackAction.java @@ -15,6 +15,7 @@ package megamek.common.actions; import megamek.client.Client; +import megamek.client.ui.Messages; import megamek.common.*; import megamek.common.annotations.Nullable; import megamek.common.enums.IlluminationLevel; @@ -74,7 +75,7 @@ public void setTargetId(int targetId) { public @Nullable Entity getEntity(Game g) { return getEntity(g, getEntityId()); } - + /** * Gets an entity with the given ID, using the passed-in game object. * @return the entity even if it was destroyed or fled. @@ -125,7 +126,7 @@ public static ToHitData nightModifiers(Game game, Targetable target, AmmoType at } } } - // Searchlights reduce the penalty to zero (or 1 for pitch-black) + // Searchlights reduce the penalty to zero (or 1 for pitch-black) // (except for dusk/dawn) int searchlightMod = Math.min(3, night_modifier); if ((te != null) && (lightCond > PlanetaryConditions.L_DUSK) @@ -150,11 +151,11 @@ public static ToHitData nightModifiers(Game game, Targetable target, AmmoType at night_modifier -= searchlightMod; } else if (atype != null) { // Certain ammunitions reduce the penalty - if (((atype.getAmmoType() == AmmoType.T_AC) + if (((atype.getAmmoType() == AmmoType.T_AC) || (atype.getAmmoType() == AmmoType.T_LAC) || (atype.getAmmoType() == AmmoType.T_AC_IMP) || (atype.getAmmoType() == AmmoType.T_PAC)) - && ((atype.getMunitionType() == AmmoType.M_INCENDIARY_AC) + && ((atype.getMunitionType() == AmmoType.M_INCENDIARY_AC) || (atype.getMunitionType() == AmmoType.M_TRACER))) { toHit.addModifier(-1, "incendiary/tracer ammo"); night_modifier--; diff --git a/megamek/src/megamek/common/actions/AbstractEntityAction.java b/megamek/src/megamek/common/actions/AbstractEntityAction.java index 8549c1f528..1eb844a134 100644 --- a/megamek/src/megamek/common/actions/AbstractEntityAction.java +++ b/megamek/src/megamek/common/actions/AbstractEntityAction.java @@ -1,20 +1,21 @@ /** * MegaMek - Copyright (C) 2000-2002 Ben Mazur (bmazur@sev.org) - * - * This program 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 2 of the License, or (at your option) + * + * This program 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 2 of the License, or (at your option) * any later version. - * - * This program 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 + * + * This program 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. */ package megamek.common.actions; import megamek.client.Client; +import megamek.common.Game; import java.io.Serializable; @@ -24,7 +25,7 @@ public abstract class AbstractEntityAction implements Serializable, EntityAction { /** - * + * */ private static final long serialVersionUID = -758003433608975464L; private int entityId; @@ -47,4 +48,10 @@ public void setEntityId(int entityId) { public String toDisplayableString(Client client) { return this.toString(); } + + @Override + public String toSummaryString(final Game game) { + String typeName = this.getClass().getTypeName(); + return typeName.substring(typeName.lastIndexOf('.') + 1); + } } diff --git a/megamek/src/megamek/common/actions/ChargeAttackAction.java b/megamek/src/megamek/common/actions/ChargeAttackAction.java index 3969dfb01c..cad05da15d 100644 --- a/megamek/src/megamek/common/actions/ChargeAttackAction.java +++ b/megamek/src/megamek/common/actions/ChargeAttackAction.java @@ -15,6 +15,7 @@ import java.util.Enumeration; +import megamek.client.ui.Messages; import megamek.common.*; import megamek.common.MovePath.MoveStepType; import megamek.common.options.OptionsConstants; @@ -495,4 +496,9 @@ public static int getDamageTakenBy(Entity entity, Entity target, .getWeight() + entity.getWeight())) / 10); } + @Override + public String toSummaryString(final Game game) { + final String roll = this.toHit(game).getValueAsString(); + return Messages.getString("BoardView1.ChargeAttackAction", roll); + } } diff --git a/megamek/src/megamek/common/actions/ClubAttackAction.java b/megamek/src/megamek/common/actions/ClubAttackAction.java index 887f68cc9e..b076eabdb5 100644 --- a/megamek/src/megamek/common/actions/ClubAttackAction.java +++ b/megamek/src/megamek/common/actions/ClubAttackAction.java @@ -13,6 +13,7 @@ */ package megamek.common.actions; +import megamek.client.ui.Messages; import megamek.common.Compute; import megamek.common.CriticalSlot; import megamek.common.Entity; @@ -50,7 +51,7 @@ public ClubAttackAction(int entityId, int targetId, Mounted club, this.club = club; aiming = aimTable; } - + /** * Creates a new club attack * @param entityId - id of entity performing the attack @@ -66,7 +67,7 @@ public ClubAttackAction(int entityId, int targetType, int targetId, this.club = club; aiming = aimTable; this.zweihandering = zweihandering; - + } /** @@ -147,7 +148,7 @@ public static int getDamageFor(Entity entity, Mounted club, } else if (mType.hasSubType(MiscType.S_SPOT_WELDER)) { nDamage = 5; } - + //SMASH! CamOps, pg. 82 if (zweihandering) { nDamage += (int) Math.floor(entity.getWeight() / 10.0); @@ -227,13 +228,13 @@ public static int getHitModFor(MiscType clubType) { } /** - * + * * @return true if the entity is zweihandering (attacking with both hands) */ public boolean isZweihandering() { return zweihandering; } - + public ToHitData toHit(Game game) { return ClubAttackAction.toHit(game, getEntityId(), game.getTarget(getTargetType(), getTargetId()), getClub(), @@ -318,7 +319,7 @@ public static ToHitData toHit(Game game, int attackerId, + targHex.getLevel(); final int targetHeight = targetElevation + target.getHeight(); final boolean bothArms = (club.getType().hasFlag(MiscType.F_CLUB) - && ((MiscType) club.getType()).hasSubType(MiscType.S_CLUB)) + && ((MiscType) club.getType()).hasSubType(MiscType.S_CLUB)) || zweihandering; // Cast is safe because non-'Mechs never even get here. final boolean hasClaws = ((Mech) ae).hasClaw(Mech.LOC_RARM) @@ -562,4 +563,11 @@ public Mounted getClub() { public void setClub(Mounted club) { this.club = club; } + + @Override + public String toSummaryString(final Game game) { + final String roll = this.toHit(game).getValueAsString(); + final String club = this.getClub().getName(); + return Messages.getString("BoardView1.ClubAttackAction", club, roll); + } } diff --git a/megamek/src/megamek/common/actions/DfaAttackAction.java b/megamek/src/megamek/common/actions/DfaAttackAction.java index ada0626467..739d93d345 100644 --- a/megamek/src/megamek/common/actions/DfaAttackAction.java +++ b/megamek/src/megamek/common/actions/DfaAttackAction.java @@ -15,6 +15,7 @@ import java.util.Enumeration; +import megamek.client.ui.Messages; import megamek.common.*; import megamek.common.MovePath.MoveStepType; import megamek.common.options.OptionsConstants; @@ -420,4 +421,10 @@ public static boolean hasTalons(Entity entity) { return false; } + + @Override + public String toSummaryString(final Game game) { + final String roll = this.toHit(game).getValueAsString(); + return Messages.getString("BoardView1.DfaAttackAction", roll); + } } diff --git a/megamek/src/megamek/common/actions/EntityAction.java b/megamek/src/megamek/common/actions/EntityAction.java index d88c313309..fa4d3120d7 100644 --- a/megamek/src/megamek/common/actions/EntityAction.java +++ b/megamek/src/megamek/common/actions/EntityAction.java @@ -14,6 +14,7 @@ package megamek.common.actions; import megamek.client.Client; +import megamek.common.Game; public interface EntityAction { int getEntityId(); @@ -21,4 +22,6 @@ public interface EntityAction { void setEntityId(int entityId); String toDisplayableString(Client client); + + String toSummaryString(Game game); } diff --git a/megamek/src/megamek/common/actions/KickAttackAction.java b/megamek/src/megamek/common/actions/KickAttackAction.java index 98ad1f2a2d..84d5460dbb 100644 --- a/megamek/src/megamek/common/actions/KickAttackAction.java +++ b/megamek/src/megamek/common/actions/KickAttackAction.java @@ -13,21 +13,8 @@ */ package megamek.common.actions; -import megamek.common.Compute; -import megamek.common.Dropship; -import megamek.common.Entity; -import megamek.common.GunEmplacement; -import megamek.common.Game; -import megamek.common.Hex; -import megamek.common.ILocationExposureStatus; -import megamek.common.Infantry; -import megamek.common.Mech; -import megamek.common.MiscType; -import megamek.common.Mounted; -import megamek.common.Tank; -import megamek.common.TargetRoll; -import megamek.common.Targetable; -import megamek.common.ToHitData; +import megamek.client.ui.Messages; +import megamek.common.*; /** * The attacker kicks the target. @@ -62,7 +49,7 @@ public void setLeg(int leg) { /** * Damage that the specified mech does with a kick - * + * * @return The kick damage for the 'Mech, or 0 for non-'Mech entities. */ public static int getDamageFor(Entity entity, int leg, @@ -134,11 +121,11 @@ public static ToHitData toHit(Game game, int attackerId, if (!(ae instanceof Mech)) { return new ToHitData(TargetRoll.IMPOSSIBLE, "Non-'Mechs can't kick."); } - + if (ae.isStuck()) { return new ToHitData(TargetRoll.IMPOSSIBLE, "Bogged-down units can't kick."); } - + String impossible = PhysicalAttackAction.toHitIsImpossible(game, ae, target); if (impossible != null) { return new ToHitData(TargetRoll.IMPOSSIBLE, "impossible"); @@ -223,7 +210,7 @@ public static ToHitData toHit(Game game, int attackerId, return new ToHitData(TargetRoll.IMPOSSIBLE, "Target elevation not in range"); } - + // check facing // Don't check arc for stomping infantry or tanks. if ((0 != range) @@ -299,8 +286,8 @@ public static ToHitData toHit(Game game, int attackerId, } else { toHit.setHitTable(ToHitData.HIT_NORMAL); } - - //What to do with grounded dropships? Awaiting rules clarification, but + + //What to do with grounded dropships? Awaiting rules clarification, but //until then, we will assume that if the attacker height is less than half //the target elevation, then use HIT_KICK, otherwise HIT_NORMAL //See Dropship.rollHitLocation to see how HIT_KICK is handled @@ -324,4 +311,30 @@ public static ToHitData toHit(Game game, int attackerId, // done! return toHit; } + + @Override + public String toSummaryString(final Game game) { + String rollLeft; + String rollRight; + String buffer; + final int leg = this.getLeg(); + switch (leg) { + case KickAttackAction.BOTH: + rollLeft = KickAttackAction.toHit(game, this.getEntityId(), game.getTarget(this.getTargetType(), this.getTargetId()), KickAttackAction.LEFT).getValueAsString(); + rollRight = KickAttackAction.toHit(game, this.getEntityId(), game.getTarget(this.getTargetType(), this.getTargetId()), KickAttackAction.RIGHT).getValueAsString(); + buffer = Messages.getString("BoardView1.kickBoth", rollLeft, rollRight); + break; + case KickAttackAction.LEFT: + rollLeft = KickAttackAction.toHit(game, this.getEntityId(), game.getTarget(this.getTargetType(), this.getTargetId()), KickAttackAction.LEFT).getValueAsString(); + buffer = Messages.getString("BoardView1.kickLeft", rollLeft); + break; + case KickAttackAction.RIGHT: + rollRight = KickAttackAction.toHit(game, this.getEntityId(), game.getTarget(this.getTargetType(), this.getTargetId()), KickAttackAction.RIGHT).getValueAsString(); + buffer = Messages.getString("BoardView1.kickRight", rollRight); + break; + default: + buffer = "Error on kick action"; + } + return buffer; + } } diff --git a/megamek/src/megamek/common/actions/ProtomechPhysicalAttackAction.java b/megamek/src/megamek/common/actions/ProtomechPhysicalAttackAction.java index 1cf3fcb7b6..6cef9cd6b5 100644 --- a/megamek/src/megamek/common/actions/ProtomechPhysicalAttackAction.java +++ b/megamek/src/megamek/common/actions/ProtomechPhysicalAttackAction.java @@ -14,6 +14,7 @@ package megamek.common.actions; +import megamek.client.ui.Messages; import megamek.common.*; import megamek.common.options.OptionsConstants; @@ -108,9 +109,9 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target) { final int attackerElevation = ae.getElevation() + attHex.getLevel(); final int targetHeight = target.relHeight() + targHex.getLevel(); final int targetElevation = target.getElevation() + targHex.getLevel(); - + boolean inSameBuilding = Compute.isInSameBuilding(game, ae, te); - + ToHitData toHit; // can't target yourself @@ -177,7 +178,7 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target) { // target terrain if (te != null) { toHit.append(Compute.getTargetTerrainModifier(game, te, 0, inSameBuilding)); - } + } // target prone if ((te != null) && te.isProne()) { @@ -202,4 +203,10 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target) { // done! return toHit; } + + @Override + public String toSummaryString(final Game game) { + final String roll = this.toHit(game).getValueAsString(); + return Messages.getString("BoardView1.ProtomechPhysicalAttackAction", roll); + } } diff --git a/megamek/src/megamek/common/actions/PunchAttackAction.java b/megamek/src/megamek/common/actions/PunchAttackAction.java index 5b9930923b..2afe09af3a 100644 --- a/megamek/src/megamek/common/actions/PunchAttackAction.java +++ b/megamek/src/megamek/common/actions/PunchAttackAction.java @@ -13,6 +13,7 @@ */ package megamek.common.actions; +import megamek.client.ui.Messages; import megamek.common.Compute; import megamek.common.Dropship; import megamek.common.Entity; @@ -46,7 +47,7 @@ public PunchAttackAction(int entityId, int targetId, int arm) { super(entityId, targetId); this.arm = arm; } - + /** * Punch attack vs an entity or other type of target (e.g. building) */ @@ -71,9 +72,9 @@ public int getArm() { public void setArm(int arm) { this.arm = arm; } - + /** - * + * * @return true if the entity is zweihandering (attacking with both hands) */ public boolean isZweihandering() { @@ -254,7 +255,7 @@ else if (!Compute.isInArc(ae.getPosition(), ae.getSecondaryFacing(), if (!ae.hasWorkingSystem(Mech.ACTUATOR_LOWER_ARM, armLoc)) { toHit.addModifier(2, "Lower arm actuator missing or destroyed"); } - + if (zweihandering) { if (!ae.hasWorkingSystem(Mech.ACTUATOR_UPPER_ARM, otherArm)) { toHit.addModifier(2, "Upper arm actuator destroyed"); @@ -350,7 +351,7 @@ public static int getDamageFor(Entity entity, int arm, if (((Mech) entity).hasClaw(armLoc)) { damage = (int) Math.ceil(entity.getWeight() / 7.0); } - + //CamOps, pg. 82 if (zweihandering) { damage += (int) Math.floor(entity.getWeight() / 10.0); @@ -381,4 +382,30 @@ public static int getDamageFor(Entity entity, int arm, } return toReturn; } + + @Override + public String toSummaryString(final Game game) { + String buffer; + String rollLeft; + String rollRight; + final int arm = this.getArm(); + switch (arm) { + case PunchAttackAction.BOTH: + rollLeft = PunchAttackAction.toHit(game, this.getEntityId(), game.getTarget(this.getTargetType(), this.getTargetId()), PunchAttackAction.LEFT, false).getValueAsString(); + rollRight = PunchAttackAction.toHit(game, this.getEntityId(), game.getTarget(this.getTargetType(), this.getTargetId()), PunchAttackAction.RIGHT, false).getValueAsString(); + buffer = Messages.getString("BoardView1.punchBoth", rollLeft, rollRight); + break; + case PunchAttackAction.LEFT: + rollLeft = PunchAttackAction.toHit(game, this.getEntityId(), game.getTarget(this.getTargetType(), this.getTargetId()), PunchAttackAction.LEFT, false).getValueAsString(); + buffer = Messages.getString("BoardView1.punchLeft", rollLeft); + break; + case PunchAttackAction.RIGHT: + rollRight = PunchAttackAction.toHit(game, this.getEntityId(), game.getTarget(this.getTargetType(), this.getTargetId()), PunchAttackAction.RIGHT, false).getValueAsString(); + buffer = Messages.getString("BoardView1.punchRight", rollRight); + break; + default: + buffer = "Error on punch action"; + } + return buffer; + } } diff --git a/megamek/src/megamek/common/actions/PushAttackAction.java b/megamek/src/megamek/common/actions/PushAttackAction.java index 46ea98d579..4aa85d7ff7 100644 --- a/megamek/src/megamek/common/actions/PushAttackAction.java +++ b/megamek/src/megamek/common/actions/PushAttackAction.java @@ -13,6 +13,7 @@ */ package megamek.common.actions; +import megamek.client.ui.Messages; import megamek.common.*; import megamek.common.options.OptionsConstants; @@ -131,7 +132,7 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target) { if (ae.entityIsQuad()) { return new ToHitData(TargetRoll.IMPOSSIBLE, "Attacker is a quad"); } - + // LAM AirMechs can only push when grounded. if (ae.isAirborneVTOLorWIGE()) { return new ToHitData(TargetRoll.IMPOSSIBLE, "Cannot push while airborne"); @@ -325,4 +326,10 @@ public static ToHitData toHit(Game game, int attackerId, Targetable target) { // done! return toHit; } + + @Override + public String toSummaryString(final Game game) { + final String roll = this.toHit(game).getValueAsString(); + return Messages.getString("BoardView1.PushAttackAction", roll); + } } diff --git a/megamek/src/megamek/common/actions/SearchlightAttackAction.java b/megamek/src/megamek/common/actions/SearchlightAttackAction.java index 8cc7ddef3a..967f4a4578 100644 --- a/megamek/src/megamek/common/actions/SearchlightAttackAction.java +++ b/megamek/src/megamek/common/actions/SearchlightAttackAction.java @@ -18,6 +18,7 @@ import java.util.Enumeration; import java.util.Vector; +import megamek.client.ui.Messages; import megamek.common.Compute; import megamek.common.Coords; import megamek.common.Entity; @@ -47,33 +48,29 @@ public SearchlightAttackAction(int entityId, int targetType, int targetId) { } public boolean isPossible(Game game) { - return SearchlightAttackAction.isPossible(game, getEntityId(), game - .getTarget(getTargetType(), getTargetId()), this); + return SearchlightAttackAction.isPossible(game, getEntityId(), game.getTarget(getTargetType(), getTargetId()), this); } - public static boolean isPossible(Game game, int attackerId, - Targetable target, SearchlightAttackAction exempt) { + public static boolean isPossible(Game game, int attackerId, Targetable target, SearchlightAttackAction exempt) { final Entity attacker = game.getEntity(attackerId); - + // can't light up if either you or the target don't exist, or you don't have your light on if ((attacker == null) || !attacker.isUsingSearchlight() || (target == null)) { return false; } - + // can't light up if you're stunned if ((attacker instanceof Tank) && (((Tank) attacker).getStunnedTurns() > 0)) { return false; } - + // can't searchlight if target is outside of the front firing arc - if (!Compute.isInArc(attacker.getPosition(), attacker - .getSecondaryFacing(), target, - attacker.getForwardArc())) { + if (!Compute.isInArc(attacker.getPosition(), attacker.getSecondaryFacing(), target, attacker.getForwardArc())) { return false; } - + // can't light up more than once per round - for (Enumeration actions = game.getActions(); actions.hasMoreElements();) { + for (Enumeration actions = game.getActions(); actions.hasMoreElements(); ) { EntityAction action = actions.nextElement(); if (action instanceof SearchlightAttackAction) { SearchlightAttackAction act = (SearchlightAttackAction) action; @@ -85,14 +82,14 @@ public static boolean isPossible(Game game, int attackerId, } } } - + // per TacOps, integrated searchlights have max range of 30 hexes // hand-held ones have a max range of 10 hexes, but are not implemented if (attacker.getPosition().distance(target.getPosition()) > 30) { return false; } - - // can't light up if out of LOS. Most expensive calculation, so keep it last + + // can't light up if out of LOS. Most expensive calculation, so keep it last return LosEffects.calculateLOS(game, attacker, target).canSee(); } @@ -125,8 +122,8 @@ public Vector resolveAction(Game game) { attacker.setUsedSearchlight(true); ArrayList in = Coords.intervening(apos, tpos); // nb includes - // attacker & - // target + // attacker & + // target for (Coords c : in) { for (Entity en : game.getEntitiesVector(c)) { LosEffects los = LosEffects.calculateLOS(game, attacker, en); @@ -143,16 +140,16 @@ public Vector resolveAction(Game game) { } return reports; } - + /** * Updates the supplied Game's list of hexes illuminated. - * + * * @param game The {@link Game} to update - * @return True if new hexes were added, else false. + * @return True if new hexes were added, else false. */ public boolean setHexesIlluminated(Game game) { boolean hexesAdded = false; - + final Entity attacker = getEntity(game); final Coords apos = attacker.getPosition(); final Targetable target = getTarget(game); @@ -177,8 +174,8 @@ public boolean willIlluminate(Game game, Entity who) { final Coords tpos = target.getPosition(); ArrayList in = Coords.intervening(apos, tpos); // nb includes - // attacker & - // target + // attacker & + // target for (Coords c : in) { for (Entity en : game.getEntitiesVector(c)) { LosEffects los = LosEffects.calculateLOS(game, attacker, en); @@ -189,4 +186,10 @@ public boolean willIlluminate(Game game, Entity who) { } return false; } + + @Override + public String toSummaryString(final Game game) { + Entity target = game.getEntity(this.getTargetId()); + return Messages.getString("BoardView1.SearchlightAttackAction") + ((target != null) ? ' ' + target.getShortName() : ""); + } } diff --git a/megamek/src/megamek/common/actions/SpotAction.java b/megamek/src/megamek/common/actions/SpotAction.java index 13b8624527..cb052fe459 100644 --- a/megamek/src/megamek/common/actions/SpotAction.java +++ b/megamek/src/megamek/common/actions/SpotAction.java @@ -1,23 +1,27 @@ /* * MegaMek - Copyright (C) 2000-2002 Ben Mazur (bmazur@sev.org) - * - * This program 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 2 of the License, or (at your option) + * + * This program 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 2 of the License, or (at your option) * any later version. - * - * This program 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 + * + * This program 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. */ package megamek.common.actions; +import megamek.client.ui.Messages; +import megamek.common.Entity; +import megamek.common.Game; + public class SpotAction extends AbstractEntityAction { /** - * + * */ private static final long serialVersionUID = 3629300334304478911L; private int targetId; @@ -30,4 +34,10 @@ public SpotAction(int entityId, int targetId) { public int getTargetId() { return targetId; } + + @Override + public String toSummaryString(final Game game) { + Entity target = game.getEntity(this.getTargetId()); + return Messages.getString("BoardView1.SpotAction", (target != null) ? target.getShortName() : "" ); + } } diff --git a/megamek/src/megamek/common/actions/TeleMissileAttackAction.java b/megamek/src/megamek/common/actions/TeleMissileAttackAction.java index 741c696d2d..b55497ddb3 100644 --- a/megamek/src/megamek/common/actions/TeleMissileAttackAction.java +++ b/megamek/src/megamek/common/actions/TeleMissileAttackAction.java @@ -22,20 +22,21 @@ import java.util.ArrayList; import java.util.Enumeration; +import megamek.client.ui.Messages; import megamek.common.*; import megamek.common.options.OptionsConstants; import megamek.common.weapons.AttackHandler; /** * Represents one tele-controlled missile attack - * + * * @author Ben Mazur */ public class TeleMissileAttackAction extends AbstractAttackAction { private static final long serialVersionUID = -1054613811287285482L; // only used server-side for manually guided Telemissile attacks private transient ArrayList vCounterEquipment; - + // Large Craft Point Defense/AMS Bay Stuff public int CounterAVInt = 0; private boolean pdOverheated = false; // true if counterfire + offensive weapon attacks made this round cause the defending unit to overheat. Used for reporting. @@ -45,17 +46,17 @@ public TeleMissileAttackAction(Entity attacker, Targetable target) { super(attacker.getId(), target.getTargetType(), target.getId()); } - public static int getDamageFor(Entity entity) { + public static int getDamageFor(Entity entity) { return (entity instanceof TeleMissile) ? ((TeleMissile) entity).getDamageValue() : 0; } - + /** * Returns the value of the pdOverheated flag */ public boolean getPDOverheated() { return pdOverheated; } - + /** * Returns the list of Counter Equipment used against this physical attack * This is for AMS assignment to manual tele-operated missiles @@ -63,7 +64,7 @@ public boolean getPDOverheated() { public ArrayList getCounterEquipment() { return vCounterEquipment; } - + /** * Adds 'm' to the list of Counter Equipment used against this physical attack * This is for AMS assignment to manual tele-operated missiles @@ -74,7 +75,7 @@ public void addCounterEquipment(Mounted m) { } vCounterEquipment.add(m); } - + /** * Checks to see if the basic conditions needed for point defenses to work are in place * Since this normally only applies to weaponhandlers, we must recreate it to deal with telemissile @@ -89,7 +90,7 @@ private boolean checkPDConditions(Game game, Targetable target) { } return true; } - + /** * Returns the heat generated by a large craft's weapons fire declarations during the round * Used to determine whether point defenses can engage. @@ -125,7 +126,7 @@ protected int getLargeCraftHeat(Entity e) { } return totalheat; } - + /** * Checks to see if this point defense/AMS bay can engage a capital missile * This should return true. Only when handling capital missile attacks can this be false. @@ -133,11 +134,11 @@ protected int getLargeCraftHeat(Entity e) { protected boolean canEngageCapitalMissile(Mounted counter) { return counter.getBayWeapons().size() >= 2; } - + /** * Calculates the attack value of point defense weapons used against a missile bay attack * This is the main large craft point defense method - */ + */ public int calcCounterAV(Game game, Targetable target) { if (!checkPDConditions(game, target)) { return 0; @@ -154,7 +155,7 @@ public int calcCounterAV(Game game, Targetable target) { for (Mounted counter : lCounters) { // Point defenses only fire vs attacks against the arc they protect Entity pdEnt = counter.getEntity(); - + // We already checked arc when AMS was assigned. No need to worry about fleet missile defense here: // Telemissiles are entities. Other craft can just shoot at them. @@ -170,23 +171,23 @@ public int calcCounterAV(Game game, Targetable target) { if (!canEngageCapitalMissile(counter)) { continue; } - + // Set up differences between point defense and AMS bays boolean isAMSBay = counter.getType().hasFlag(WeaponType.F_AMSBAY); boolean isPDBay = counter.getType().hasFlag(WeaponType.F_PDBAY); - + // Point defense bays can only fire at one attack per round if (isPDBay) { if (counter.isUsedThisRound()) { continue; } } - + // If the target is overheating don't process any more point defense attacks if (pdOverheated) { break; } - + // Now for heat, damage and ammo we need the individual weapons in the bay // First, reset the temporary damage counters amsAV = 0; @@ -195,7 +196,7 @@ public int calcCounterAV(Game game, Targetable target) { Mounted bayW = pdEnt.getEquipment(wId); Mounted bayWAmmo = bayW.getLinked(); WeaponType bayWType = ((WeaponType) bayW.getType()); - + // build up some heat // First Check to see if we have enough heat capacity to fire if ((weaponHeat + bayW.getCurrentHeat()) > pdEnt.getHeatCapacity()) { @@ -211,7 +212,7 @@ public int calcCounterAV(Game game, Targetable target) { pdEnt.heatBuildup += bayW.getCurrentHeat(); weaponHeat += bayW.getCurrentHeat(); } - + // Bays use lots of ammo. Check to make sure we haven't run out if (bayWAmmo != null) { if (bayWAmmo.getBaseShotsLeft() == 0) { @@ -230,11 +231,11 @@ public int calcCounterAV(Game game, Targetable target) { // get the attack value pdAV += bayWType.getShortAV(); // set the pdbay as having fired, if it was able to - counter.setUsedThisRound(true); + counter.setUsedThisRound(true); } } // non-AMS only add half their damage, rounded up - counterAV += (int) Math.ceil(pdAV / 2.0); + counterAV += (int) Math.ceil(pdAV / 2.0); // AMS add their full damage counterAV += amsAV; } @@ -243,14 +244,14 @@ public int calcCounterAV(Game game, Targetable target) { CounterAVInt = counterAV; return counterAV; } - + /** * To-hit number for a charge, assuming that movement has been handled */ public ToHitData toHit(Game game) { return toHit(game, game.getTarget(getTargetType(), getTargetId())); } - + public ToHitData toHit(Game game, Targetable target) { final Entity ae = getEntity(game); @@ -263,7 +264,7 @@ public ToHitData toHit(Game game, Targetable target) { if (target == null) { return new ToHitData(TargetRoll.IMPOSSIBLE, "Target is null"); } - + if (!game.getOptions().booleanOption(OptionsConstants.BASE_FRIENDLY_FIRE)) { // a friendly unit can never be the target of a direct attack. if ((target.getTargetType() == Targetable.TYPE_ENTITY) @@ -279,20 +280,20 @@ public ToHitData toHit(Game game, Targetable target) { ToHitData toHit = new ToHitData(2, "base"); TeleMissile tm = (TeleMissile) ae; - + // thrust used if (ae.mpUsed > 0) { toHit.addModifier(ae.mpUsed, "thrust used"); } - + // out of fuel if (tm.getCurrentFuel() <= 0) { toHit.addModifier(+6, "out of fuel"); } - + // modifiers for the originating unit need to be added later, because // they may change as a result of damage - + // done! return toHit; } diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 6d7453ab31..c72c4c4242 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -5143,4 +5143,14 @@ private static ToHitData handleArtilleryAttacks(Game game, Entity ae, Targetable public String toDisplayableString(Client client) { return "attacking " + getTarget(client.getGame()).getDisplayName() + " with " + getEntity(client.getGame()).getEquipment(weaponId).getName(); } + + @Override + public String toSummaryString(final Game game) { + ToHitData toHit = this.toHit(game, true); + String table = toHit.getTableDesc(); + final String buffer = toHit.getValueAsString() + ((!table.isEmpty()) ? ' '+table : ""); + final Entity entity = game.getEntity(this.getEntityId()); + final String weaponName = ((WeaponType) entity.getEquipment(this.getWeaponId()).getType()).getName(); + return weaponName + Messages.getString("BoardView1.needs") + buffer; + } } From c8d4375039f6b268971e27f2bc491d406fc5ba1b Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Thu, 15 Jun 2023 20:59:20 -0700 Subject: [PATCH 11/13] code cleanup --- .../client/ui/swing/MovementDisplay.java | 132 +++++++++--------- .../boardview/AbstractBoardViewOverlay.java | 11 +- 2 files changed, 71 insertions(+), 72 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index 514ab8566e..fb176e1f55 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -1048,11 +1048,13 @@ private void updateAeroButtons() { } } - /** toggles the status of the Done and No Nag buttons based on if the current move order is valid */ + /** + * toggles the status of the Done and No Nag buttons based on if the current move order is valid + */ @Override protected void updateDonePanel() { if (cmd == null || cmd.length() == 0) { - updateDonePanelButtons( Messages.getString("MovementDisplay.Move"), Messages.getString("MovementDisplay.Skip"), false, null); + updateDonePanelButtons(Messages.getString("MovementDisplay.Move"), Messages.getString("MovementDisplay.Skip"), false, null); return; } else { MovePath possible = cmd.clone(); @@ -1062,80 +1064,78 @@ protected void updateDonePanel() { } else if (!possible.isMoveLegal()) { updateDonePanelButtons(Messages.getString("MovementDisplay.IllegalMove"), Messages.getString("MovementDisplay.Skip"), false, null); } else { - String validTextColor = AbstractBoardViewOverlay.colorToHex( AbstractBoardViewOverlay.getTextColor()); - String invalidTextColor = AbstractBoardViewOverlay.colorToHex( AbstractBoardViewOverlay.getTextColor(), 0.7f); - int mp = possible.countMp(possible.isJumping()); boolean psrCheck = (!SharedUtility.doPSRCheck(cmd.clone()).isBlank()) || (!SharedUtility.doThrustCheck(cmd.clone(), clientgui.getClient()).isBlank()); boolean damageCheck = cmd.shouldMechanicalJumpCauseFallDamage() || cmd.hasActiveMASC() || (!(ce() instanceof VTOL) && cmd.hasActiveSupercharger()) || cmd.willCrushBuildings(); + String moveMsg = Messages.getString("MovementDisplay.Move") + " (" + mp + "MP)" + (psrCheck ? "*" : "") + (damageCheck ? "!" : ""); + updateDonePanelButtons(moveMsg, Messages.getString("MovementDisplay.Skip"), true, computeTurnDetails()); + } + } + } - MoveStepType accumType = null; - int accumTypeCount = 0; - int accumMP = 0; - int accumDanger = 0; - boolean accumLegal = true; - String unicodeIcon = ""; - ArrayList turnDetails = new ArrayList<>(); - for (final Enumeration step = cmd.getSteps(); step.hasMoreElements(); ) { - MoveStep currentStep = step.nextElement(); - MoveStepType currentType = currentStep.getType(); - int currentDanger = currentStep.isDanger() ? 1 : 0; - boolean currentLegal = currentStep.isLegal(cmd); - - if (accumTypeCount != 0 && accumType == currentType && accumLegal == currentLegal) { - accumTypeCount++; - accumMP += currentStep.getMp(); - accumDanger += currentDanger; - continue; - } - - // switching to a new move type, so write a line - if (accumTypeCount != 0) { - turnDetails.add(String.format(turnDetailsFormat, - accumLegal ? validTextColor : invalidTextColor, - accumTypeCount == 1 ? "" : "x"+accumTypeCount, - accumType, unicodeIcon, accumMP, "*".repeat(accumDanger))); - } + ArrayList computeTurnDetails(){ + String validTextColor = AbstractBoardViewOverlay.colorToHex(AbstractBoardViewOverlay.getTextColor()); + String invalidTextColor = AbstractBoardViewOverlay.colorToHex(AbstractBoardViewOverlay.getTextColor(), 0.7f); - // switching to a new move type, reset - accumType = currentType; - accumTypeCount = 1; - accumMP = currentStep.getMp(); - accumDanger = currentDanger; - accumLegal = currentLegal; - switch (accumType) { - case TURN_LEFT: - unicodeIcon = "\u21B0"; - break; - case TURN_RIGHT: - unicodeIcon = "\u21B1"; - break; - case FORWARDS: - unicodeIcon = "\u2191"; - break; - case BACKWARDS: - unicodeIcon = "\u2193"; - break; - case START_JUMP: - unicodeIcon = "\u21EF"; - break; - default: - unicodeIcon = ""; - break; - } - } + MoveStepType accumType = null; + int accumTypeCount = 0; + int accumMP = 0; + int accumDanger = 0; + boolean accumLegal = true; + String unicodeIcon = ""; + ArrayList turnDetails = new ArrayList<>(); + for( final Enumeration step = cmd.getSteps(); step.hasMoreElements();) { + MoveStep currentStep = step.nextElement(); + MoveStepType currentType = currentStep.getType(); + int currentDanger = currentStep.isDanger() ? 1 : 0; + boolean currentLegal = currentStep.isLegal(cmd); - turnDetails.add(String.format(turnDetailsFormat, - accumLegal ? validTextColor : invalidTextColor, - accumTypeCount == 1 ? "" : "x"+accumTypeCount, - accumType, unicodeIcon, accumMP, "*".repeat(accumDanger))); + if (accumTypeCount != 0 && accumType == currentType && accumLegal == currentLegal) { + accumTypeCount++; + accumMP += currentStep.getMp(); + accumDanger += currentDanger; + continue; + } - String moveMsg = Messages.getString("MovementDisplay.Move") - + " (" + mp + "MP)" + (psrCheck ? "*" : "") + (psrCheck ? "*" : "") + (damageCheck ? "!" : ""); + // switching to a new move type, so write a line + if (accumTypeCount != 0) { + turnDetails.add(String.format(turnDetailsFormat, accumLegal ? validTextColor : invalidTextColor, accumTypeCount == 1 ? "" : "x" + accumTypeCount, accumType, unicodeIcon, accumMP, "*".repeat(accumDanger))); + } - updateDonePanelButtons(moveMsg, Messages.getString("MovementDisplay.Skip"), true, turnDetails); + // switching to a new move type, reset + accumType = currentType; + accumTypeCount = 1; + accumMP = currentStep.getMp(); + accumDanger = currentDanger; + accumLegal = currentLegal; + switch (accumType) { + case TURN_LEFT: + unicodeIcon = "\u21B0"; + break; + case TURN_RIGHT: + unicodeIcon = "\u21B1"; + break; + case FORWARDS: + unicodeIcon = "\u2191"; + break; + case BACKWARDS: + unicodeIcon = "\u2193"; + break; + case START_JUMP: + unicodeIcon = "\u21EF"; + break; + default: + unicodeIcon = ""; + break; } } + + // add line for last moves + turnDetails.add(String.format(turnDetailsFormat, + accumLegal ?validTextColor :invalidTextColor, + accumTypeCount ==1?"":"x"+accumTypeCount, + accumType,unicodeIcon,accumMP,"*".repeat(accumDanger))); + return turnDetails; } /** diff --git a/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java b/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java index 7436561047..c30635fbc6 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java +++ b/megamek/src/megamek/client/ui/swing/boardview/AbstractBoardViewOverlay.java @@ -129,7 +129,7 @@ public void draw(Graphics graph, Rectangle clipBounds) { hasContents = false; } else { hasContents = true; - Rectangle r = getSize(graph, allLines, fm); + Rectangle r = getSize(allLines, fm); r = new Rectangle(r.width + 2 * PADDING_X, r.height + 2 * PADDING_Y); overlayWidth = r.width; overlayHeight = r.height; @@ -175,7 +175,7 @@ public void draw(Graphics graph, Rectangle clipBounds) { } /** Calculates the pixel size of the display from the necessary text lines. */ - private Rectangle getSize(Graphics graph, List lines, FontMetrics fm) { + private Rectangle getSize(List lines, FontMetrics fm) { int width = 0; for (String line: lines) { if (fm.stringWidth(line) > width) { @@ -290,7 +290,6 @@ public void gameTurnChange(GameTurnChangeEvent e) { @Override public void preferenceChange(PreferenceChangeEvent e) { - String name = e.getName(); if (getVisibilityGUIPreference() != visible) { visible = getVisibilityGUIPreference(); setDirty(); @@ -318,13 +317,13 @@ public static Color getTextColor() { public static String colorToHex(Color color) { return String.format("#%02X%02X%02X", - color.getRed(), color.getGreen(),color.getBlue(), color.getAlpha()); + color.getRed(), color.getGreen(),color.getBlue()); } public static String colorToHex(Color color, float brightnessMultiplier) { return String.format("#%02X%02X%02X", - (int)(color.getRed() * brightnessMultiplier), (int)(color.getGreen() * brightnessMultiplier), - (int)(color.getBlue() * brightnessMultiplier), (int)(color.getAlpha())); + (int)(color.getRed() * brightnessMultiplier), (int)(color.getGreen() * brightnessMultiplier), + (int)(color.getBlue() * brightnessMultiplier)); } } From 3b6b6a02a495724bf6e20ad70ad355cf5c7a5126 Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Thu, 15 Jun 2023 21:41:10 -0700 Subject: [PATCH 12/13] remove commented code --- megamek/src/megamek/common/Mounted.java | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/megamek/src/megamek/common/Mounted.java b/megamek/src/megamek/common/Mounted.java index fb3d16aa7e..50bcf9fa3f 100644 --- a/megamek/src/megamek/common/Mounted.java +++ b/megamek/src/megamek/common/Mounted.java @@ -1347,24 +1347,7 @@ public boolean canFire(boolean isStrafing, boolean evenIfAlreadyFired) { // Otherwise, the equipment can be fired. return true; } - -// public boolean canFire(boolean isStrafing) { -// -// // Equipment operational? -// if (!isReady(isStrafing) || isBreached() || isMissing() || isFired()) { -// return false; -// } -// -// // Is the entity even active? -// if (entity.isShutDown() -// || ((null != entity.getCrew()) && !entity.getCrew().isActive())) { -// return false; -// } -// -// // Otherwise, the equipment can be fired. -// return true; -// } - + /** * Determines whether this weapon should be considered crippled for damage * level purposes. From 348e979e59d84ed6780b3114a6f70c25a9858511 Mon Sep 17 00:00:00 2001 From: "DESKTOP-C2SG422\\Tim" Date: Fri, 16 Jun 2023 18:57:33 -0700 Subject: [PATCH 13/13] code review cleanup access levels and Copyrightwq --- .../client/ui/swing/AttackPhaseDisplay.java | 2 +- .../ui/swing/boardview/AttackSprite.java | 2 +- .../ui/swing/tooltip/EntityActionLog.java | 94 +++++++++++-------- megamek/src/megamek/common/Mounted.java | 2 +- .../common/actions/WeaponAttackAction.java | 4 +- 5 files changed, 60 insertions(+), 44 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java index 87b7794d9b..fff3a6194a 100644 --- a/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/AttackPhaseDisplay.java @@ -31,7 +31,7 @@ public abstract class AttackPhaseDisplay extends ActionPhaseDisplay { protected AttackPhaseDisplay(ClientGUI cg) { super(cg); - attacks = new EntityActionLog(clientgui.getClient()); + attacks = new EntityActionLog(clientgui.getClient().getGame()); } /** diff --git a/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java b/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java index bce56322b5..c9be8a0b79 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java +++ b/megamek/src/megamek/client/ui/swing/boardview/AttackSprite.java @@ -59,7 +59,7 @@ class AttackSprite extends Sprite { public AttackSprite(BoardView boardView1, final AttackAction attack) { super(boardView1); - attacks = new EntityActionLog(boardView1.clientgui.getClient()); + attacks = new EntityActionLog(boardView1.clientgui.getClient().getGame()); this.boardView1 = boardView1; entityId = attack.getEntityId(); targetType = attack.getTargetType(); diff --git a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java index ad10c45d58..d0e45255d8 100644 --- a/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java +++ b/megamek/src/megamek/client/ui/swing/tooltip/EntityActionLog.java @@ -1,6 +1,24 @@ +/* + * Copyright (c) 2023 - 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.tooltip; -import megamek.client.Client; import megamek.client.ui.Messages; import megamek.common.*; import megamek.common.actions.*; @@ -13,17 +31,14 @@ * created as the action is added. */ public class EntityActionLog implements Collection { - final Game game; - final Client client; - final protected ArrayList actions = new ArrayList(); -// final protected ArrayList weaponAttackActions = new ArrayList(); + private final Game game; + private final ArrayList actions = new ArrayList(); // cache to prevent regeneration of info if action already processed - final HashMap infoCache = new HashMap(); - final ArrayList descriptions = new ArrayList<>(); + private final HashMap infoCache = new HashMap(); + private final ArrayList descriptions = new ArrayList<>(); - public EntityActionLog(Client client) { - this.client = client; - this.game = client.getGame(); + public EntityActionLog(Game game) { + this.game = game; } /** @return a list of description strings. Note that there may be fewer than the number of actions @@ -33,6 +48,7 @@ public List getDescriptions() { return descriptions; } + /** repopulate the descriptions for each action, using the cache, or recomputing as appropriate */ public void rebuildDescriptions() { descriptions.clear(); for (EntityAction entityAction : actions) { @@ -133,7 +149,32 @@ public boolean contains(Object o) { return actions.contains(o); } - void addDescription(EntityAction entityAction) { + @Override + public int size() { + return actions.size(); + } + + @Override + public Iterator iterator() { + return actions.iterator(); + } + + @Override + public Object[] toArray() { + return new Object[0]; + } + + @Override + public T[] toArray(T[] a) { + return actions.toArray(a); + } + + @Override + public Stream stream() { + return actions.stream(); + } + + private void addDescription(EntityAction entityAction) { if (entityAction instanceof WeaponAttackAction) { // ToHit may change as other actions are added, // so always re-evaluate all WeaponAttack Actions @@ -151,12 +192,12 @@ void addDescription(EntityAction entityAction) { /** * Adds description, using an existing line if the same weapon type was already listed */ - void addWeaponAttackAction(WeaponAttackAction attack) { + private void addWeaponAttackAction(WeaponAttackAction attack) { ToHitData toHit = attack.toHit(game, true); String table = toHit.getTableDesc(); - final String buffer = toHit.getValueAsString() + ((!table.isEmpty()) ? ' '+table : ""); + final String buffer = toHit.getValueAsString() + ((!table.isEmpty()) ? ' ' + table : ""); final Entity entity = game.getEntity(attack.getEntityId()); - final String weaponName = ((WeaponType) entity.getEquipment(attack.getWeaponId()).getType()).getName(); + final String weaponName = (entity.getEquipment(attack.getWeaponId()).getType()).getName(); //add to an existing entry if possible boolean found = false; @@ -174,29 +215,4 @@ void addWeaponAttackAction(WeaponAttackAction attack) { descriptions.add( weaponName + Messages.getString("BoardView1.needs") + buffer); } } - - @Override - public int size() { - return actions.size(); - } - - @Override - public Iterator iterator() { - return actions.iterator(); - } - - @Override - public Object[] toArray() { - return new Object[0]; - } - - @Override - public T[] toArray(T[] a) { - return actions.toArray(a); - } - - @Override - public Stream stream() { - return actions.stream(); - } } diff --git a/megamek/src/megamek/common/Mounted.java b/megamek/src/megamek/common/Mounted.java index 50bcf9fa3f..7f33512913 100644 --- a/megamek/src/megamek/common/Mounted.java +++ b/megamek/src/megamek/common/Mounted.java @@ -1347,7 +1347,7 @@ public boolean canFire(boolean isStrafing, boolean evenIfAlreadyFired) { // Otherwise, the equipment can be fired. return true; } - + /** * Determines whether this weapon should be considered crippled for damage * level purposes. diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index c72c4c4242..505f0259ad 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -248,8 +248,8 @@ public ToHitData toHit(Game game) { /** * * @param game - * @param evenIfAlreadyFired true if evaluating hit change even if the weapon has already fired or is otherwise unable to fire - * @return + * @param evenIfAlreadyFired false: an already fired weapon will return a ToHitData with value IMPOSSIBLE + * true: an already fired weapon will return a ToHitData with the value of its chance to hit */ public ToHitData toHit(Game game, boolean evenIfAlreadyFired) { return toHit(game, getEntityId(), game.getTarget(getTargetType(), getTargetId()),