diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 4c96ba383f0..9cc5a513173 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -2693,7 +2693,6 @@ MovementDisplay.moveLowerElevation=Go Down MovementDisplay.BackWardsElevationChange=Moving Backwards over an elevation change.\n MovementDisplay.CarefulStand.title=Careful Stand? MovementDisplay.CarefulStand.message=Do you wish to use careful stand?\n -MovementDisplay.Traitor=Traitor Unit MovementDisplay.NotUpToSpeed=Selected unit cannot run/flank because it has poor performance and is not up to speed MovementDisplay.UpToSpeed=Selected unit is up to speed so it can run/flank MovementDisplay.UnjamRAC.title=Unjam? @@ -4630,6 +4629,9 @@ CMVPanel.font=Font: Gamemaster.Gamemaster=Gamemaster Gamemaster.EditDamage=Edit Damage Gamemaster.Configure=Configure +Gamemaster.Traitor=Traitor Unit +Gamemaster.SpecialCommands=Special Commands +Gamemaster.KillUnit=Kill Unit # Scenario Chooser ScenarioChooser.title=Choose Scenario diff --git a/megamek/i18n/megamek/client/messages_es.properties b/megamek/i18n/megamek/client/messages_es.properties index 086d314aee1..98e1e7aeb78 100644 --- a/megamek/i18n/megamek/client/messages_es.properties +++ b/megamek/i18n/megamek/client/messages_es.properties @@ -2438,7 +2438,6 @@ MovementDisplay.moveLowerElevation=Bajar MovementDisplay.BackWardsElevationChange=Moverse hacia atrás sobre un cambio de elevación.\n MovementDisplay.CarefulStand.title=¿Mantenerse con Precaución? MovementDisplay.CarefulStand.message=¿Desea Mantenerse con Precaución?\n -MovementDisplay.Traitor=Unidad Traidora MovementDisplay.NotUpToSpeed=La unidad seleccionada no puede correr/flanquear porque tiene un rendimiento deficiente y no está al día MovementDisplay.UpToSpeed=La unidad seleccionada está al día para que pueda correr/flanquear MovementDisplay.UnjamRAC.title=¿Destrabar? @@ -4267,3 +4266,5 @@ ForceGeneratorDialog.reinforced=Reforzado\ ForceGeneratorDialog.understrength=Más Débiles\ ForceGeneratorDialog.default=Predeterminado ForceGeneratorDialog.noCrew=Sin tripulación + +Gamemaster.Traitor=Unidad Traidora \ No newline at end of file diff --git a/megamek/i18n/megamek/client/messages_ru.properties b/megamek/i18n/megamek/client/messages_ru.properties index 06812521aca..69f44bd0bed 100644 --- a/megamek/i18n/megamek/client/messages_ru.properties +++ b/megamek/i18n/megamek/client/messages_ru.properties @@ -1373,7 +1373,6 @@ MovementDisplay.moveLowerElevation=Спуститься MovementDisplay.BackWardsElevationChange=Преодоление смены уровня высоты реверсом хода.\n MovementDisplay.CarefulStand.title=Использовать осторожную стойку? MovementDisplay.CarefulStand.message=Хотите ли вы использовать осторожную стойку?\n -MovementDisplay.Traitor=Юнит предатель MovementDisplay.NotUpToSpeed=Выбранный юнит не может бежать/ехать быстро потому что у него плохая форма и он не достигнет скорости MovementDisplay.UpToSpeed=Выбранному юниту хватает скорости и он может бежать/ехать быстро MovementDisplay.UnjamRAC.title=Расклинить? @@ -2258,3 +2257,5 @@ BotConfigDialog.princessHelp.title=Справка по Принцессе SavePrincessDialog.question=Сохранить изменения в эту конфигурацию? SavePrincessDialog.saveTargets=Сохранить список стратегичеких целей? + +Gamemaster.Traitor=Юнит предатель \ No newline at end of file diff --git a/megamek/i18n/megamek/common/report-messages.properties b/megamek/i18n/megamek/common/report-messages.properties index 20be24c6ee5..5bb6ad59327 100755 --- a/megamek/i18n/megamek/common/report-messages.properties +++ b/megamek/i18n/megamek/common/report-messages.properties @@ -56,8 +56,9 @@ 1242=no effect #1300s - Orbital Bombardment related -1300=An orbital bombardment hit hex !!! +1300=Orbital bombardment hit hex !!! 1301=End of orbital bombardment resolution +1302= has commenced an orbital bombardment, it will land at the end of the next weapons phase at hex . # 1500s - ammo handling related 1500= () selected diff --git a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java index a79418fee26..b992fc2d72b 100644 --- a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java +++ b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java @@ -356,11 +356,6 @@ public CommonMenuBar() { toggleCFWarning.setToolTipText(Messages.getString("CommonMenuBar.viewToggleCFWarningToolTip")); toggleCFWarning.setSelected(GUIP.getShowCFWarnings()); - /* - * TODO: moveTraitor = createMenuItem(menu, - * getString("CommonMenuBar.moveTraitor"), MovementDisplay.MOVE_TRAITOR); - */ - // Create the Help menu menu = new JMenu(Messages.getString("CommonMenuBar.HelpMenu")); menu.setMnemonic(VK_H); diff --git a/megamek/src/megamek/client/ui/swing/MapMenu.java b/megamek/src/megamek/client/ui/swing/MapMenu.java index 3fad530711a..743528ea0d2 100644 --- a/megamek/src/megamek/client/ui/swing/MapMenu.java +++ b/megamek/src/megamek/client/ui/swing/MapMenu.java @@ -24,22 +24,14 @@ import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.math.BigInteger; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Objects; -import java.util.StringTokenizer; -import java.util.Vector; - -import javax.swing.JMenu; -import javax.swing.JMenuItem; -import javax.swing.JPopupMenu; -import javax.swing.UIManager; +import java.util.*; + +import javax.swing.*; import megamek.client.Client; import megamek.client.event.BoardViewEvent; import megamek.client.ui.Messages; +import megamek.client.ui.swing.gmCommands.GamemasterCommandPanel; import megamek.client.ui.swing.lobby.LobbyUtility; import megamek.common.*; import megamek.common.Building.DemolitionCharge; @@ -54,6 +46,8 @@ import megamek.common.weapons.other.CLFireExtinguisher; import megamek.common.weapons.other.ISFireExtinguisher; import megamek.logging.MMLogger; +import megamek.server.Server; +import megamek.server.commands.*; /** * Context menu for the board. @@ -209,23 +203,6 @@ private boolean createMenu() { } } - - // Traitor Command - JMenuItem item = new JMenuItem(Messages.getString("MovementDisplay.Traitor")); - item.setActionCommand(MovementDisplay.MoveCommand.MOVE_TRAITOR.getCmd()); - item.addActionListener(evt -> { - try { - if (currentPanel instanceof MovementDisplay) { - ((MovementDisplay) currentPanel).actionPerformed(evt); - } - } catch (Exception ex) { - logger.error(ex, ""); - } - }); - - if (game.getPhase().isMovement()) { - add(item); - } } menu = touchOffExplosivesMenu(); @@ -433,10 +410,15 @@ private JMenu createGamemasterMenu() { JMenu dmgMenu = new JMenu(Messages.getString("Gamemaster.EditDamage")); JMenu cfgMenu = new JMenu(Messages.getString("Gamemaster.Configure")); + JMenu traitorMenu = new JMenu(Messages.getString("Gamemaster.Traitor")); + JMenu killMenu = new JMenu(Messages.getString("Gamemaster.KillUnit")); + JMenu specialCommandsMenu = createGMSpecialCommandsMenu(); var entities = client.getGame().getEntitiesVector(coords); for (Entity entity : entities) { dmgMenu.add(createUnitEditorMenuItem(entity)); cfgMenu.add(createCustomMekMenuItem(entity)); + traitorMenu.add(createTraitorMenuItem(entity)); + killMenu.add(createKillMenuItem(entity)); } if (dmgMenu.getItemCount() != 0) { menu.add(dmgMenu); @@ -444,10 +426,41 @@ private JMenu createGamemasterMenu() { if (cfgMenu.getItemCount() != 0) { menu.add(cfgMenu); } + if (traitorMenu.getItemCount() != 0) { + menu.add(traitorMenu); + } + if (killMenu.getItemCount() != 0) { + menu.add(killMenu); + } + menu.add(specialCommandsMenu); return menu; } } + private record Tuple(String name, GamemasterServerCommand command) {} + + private JMenu createGMSpecialCommandsMenu() { + JMenu menu = new JMenu(Messages.getString("Gamemaster.SpecialCommands")); + var commands = List.of( + new Tuple("Kill Unit", new KillCommand(null, null)), + new Tuple("Change Ownership", new ChangeOwnershipCommand(null, null)), + new Tuple("Change Weather", new ChangeWeatherCommand(null, null)), + new Tuple("Disasters", new DisasterCommand(null, null)), + new Tuple("Orbital Bombardment", new OrbitalBombardmentCommand(null, null)), + new Tuple("Remove Smoke", new RemoveSmokeCommand(null, null)), + new Tuple("Firestarter", new FirestarterCommand(null, null)), + new Tuple("Firestorm", new FirestormCommand(null, null)) + ); + + for (var cmd : commands) { + JMenuItem item = new JMenuItem(cmd.name()); + item.addActionListener(evt -> new GamemasterCommandPanel(gui.getFrame(), gui, cmd.command()).setVisible(true)); + menu.add(item); + } + + return menu; + } + JMenuItem createCustomMekMenuItem(Entity entity) { JMenuItem item = new JMenuItem(entity.getDisplayName()); item.addActionListener(evt -> { @@ -473,6 +486,79 @@ JMenuItem createUnitEditorMenuItem(Entity entity) { return item; } + private JMenuItem createTraitorMenuItem(Entity en) { + // Traitor Command + JMenuItem item = new JMenuItem(Messages.getString("Gamemaster.Traitor") + " " + en.getDisplayName()); + item.addActionListener(evt -> { + gui.getBoardView().setShouldIgnoreKeys(false); + var players = client.getGame().getPlayersList(); + Integer[] playerIds = new Integer[players.size() - 1]; + String[] playerNames = new String[players.size() - 1]; + String[] options = new String[players.size() - 1]; + + Player currentOwner = en.getOwner(); + // Loop through the players vector and fill in the arrays + int idx = 0; + for (var player : players) { + if (player.getName().equals(currentOwner.getName()) + || (player.getTeam() == Player.TEAM_UNASSIGNED)) { + continue; + } + playerIds[idx] = player.getId(); + playerNames[idx] = player.getName(); + options[idx] = player.getName() + " (ID: " + player.getId() + ")"; + idx++; + } + + // No players available? + if (idx == 0) { + JOptionPane.showMessageDialog(gui.getFrame(), + "No players available. Units cannot be traitored to players " + + "that aren't assigned to a team."); + return; + } + + // Dialog for choosing which player to transfer to + String option = (String) JOptionPane.showInputDialog(gui.getFrame(), + "Choose the player to gain ownership of this unit (" + en.getDisplayName() + ") when it turns traitor", + "Traitor", JOptionPane.QUESTION_MESSAGE, null, + options, options[0]); + + // Verify that we have a valid option... + if (option != null) { + // Now that we've selected a player, correctly associate the ID and name + int id = playerIds[Arrays.asList(options).indexOf(option)]; + String name = playerNames[Arrays.asList(options).indexOf(option)]; + + // And now we perform the actual transfer + int confirm = JOptionPane.showConfirmDialog( + gui.getFrame(), + en.getDisplayName() + " will switch to " + name + + "'s side at the end of this turn. Are you sure?", + "Confirm", + JOptionPane.YES_NO_OPTION); + if (confirm == JOptionPane.YES_OPTION) { + client.sendChat(String.format("/changeOwner %d %d", en.getId(), id)); + } + } + }); + + return item; + } + + private JMenuItem createKillMenuItem(Entity en) { + JMenuItem item = new JMenuItem(Messages.getString("Gamemaster.KillUnit") + " " + en.getDisplayName()); + item.addActionListener(evt -> { + int confirm = JOptionPane.showConfirmDialog( + gui.getFrame(), + "Are you sure you want to kill " + en.getDisplayName() + "?", "Confirm", JOptionPane.YES_NO_OPTION); + if (confirm == JOptionPane.YES_OPTION) { + client.sendChat(String.format("/kill %d", en.getId())); + } + }); + return item; + } + private JMenu createSelectMenu() { JMenu menu = new JMenu("Select"); // add select options diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index 57b1492f687..47ef702b5f8 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -19,20 +19,6 @@ */ package megamek.client.ui.swing; -import static megamek.common.MiscType.F_CHAFF_POD; -import static megamek.common.options.OptionsConstants.ADVGRNDMOV_TACOPS_ZIPLINES; - -import java.awt.Color; -import java.awt.event.ActionEvent; -import java.awt.event.InputEvent; -import java.awt.event.MouseEvent; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; - import megamek.client.event.BoardViewEvent; import megamek.client.ui.Messages; import megamek.client.ui.SharedUtility; @@ -62,6 +48,19 @@ import megamek.common.preference.PreferenceManager; import megamek.logging.MMLogger; +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.MouseEvent; +import java.util.List; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static megamek.common.MiscType.F_CHAFF_POD; +import static megamek.common.options.OptionsConstants.ADVGRNDMOV_TACOPS_ZIPLINES; + public class MovementDisplay extends ActionPhaseDisplay { private static final MMLogger logger = MMLogger.create(MovementDisplay.class); @@ -230,7 +229,7 @@ public String toString() { } public String getHotKeyDesc() { - String result = ""; + String result; String msgNext = Messages.getString("Next"); String msgPrevious = Messages.getString("Previous"); @@ -5297,11 +5296,15 @@ public synchronized void actionPerformed(ActionEvent ev) { String[] playerNames = new String[players.size() - 1]; String[] options = new String[players.size() - 1]; Entity e = ce(); + if (e == null) { + return; + } + Player currentOwner = e.getOwner(); // Loop through the players vector and fill in the arrays int idx = 0; for (var player : players) { - if (player.getName().equals(clientgui.getClient().getLocalPlayer().getName()) + if (player.getName().equals(currentOwner.getName()) || (player.getTeam() == Player.TEAM_UNASSIGNED)) { continue; } @@ -5321,7 +5324,7 @@ public synchronized void actionPerformed(ActionEvent ev) { // Dialog for choosing which player to transfer to String option = (String) JOptionPane.showInputDialog(clientgui.getFrame(), - "Choose the player to gain ownership of this unit when it turns traitor", + "Choose the player to gain ownership of this unit (" + e.getDisplayName() + ") when it turns traitor", "Traitor", JOptionPane.QUESTION_MESSAGE, null, options, options[0]); diff --git a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java index 1a5d79f083a..50c9e923c09 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java +++ b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java @@ -87,6 +87,7 @@ import megamek.common.util.ImageUtil; import megamek.common.util.fileUtils.MegaMekFile; import megamek.logging.MMLogger; +import megamek.server.props.OrbitalBombardment; /** * Displays the board; lets the user scroll around and select points on it. @@ -637,6 +638,7 @@ public void mouseDragged(MouseEvent e) { SpecialHexDisplay.Type.BOMB_HIT.init(); SpecialHexDisplay.Type.BOMB_DRIFT.init(); SpecialHexDisplay.Type.PLAYER_NOTE.init(); + SpecialHexDisplay.Type.ORBITAL_BOMBARDMENT.init(); fovHighlightingAndDarkening = new FovHighlightingAndDarkening(this); @@ -1454,6 +1456,38 @@ private Mounted selectedWeapon() { return (clientgui != null) ? clientgui.getDisplayedWeapon().orElse(null) : null; } + private void drawOrbitalBombardmentHexes(Graphics boardGraphics) { + Image orbitalBombardmentImage = tileManager.getOrbitalBombardmentImage(); + Rectangle view = boardGraphics.getClipBounds(); + boolean justDraw = false; + // Compute the origin of the viewing area + int drawX = (view.x / (int) (HEX_WC * scale)) - 1; + int drawY = (view.y / (int) (HEX_H * scale)) - 1; + + // Compute size of viewing area + int drawWidth = (view.width / (int) (HEX_WC * scale)) + 3; + int drawHeight = (view.height / (int) (HEX_H * scale)) + 3; + + // Draw incoming artillery sprites - requires server to update client's + // view of game + for (Enumeration attacks = game.getOrbitalBombardmentAttacks(); attacks.hasMoreElements();) { + final OrbitalBombardment orbitalBombardment = attacks.nextElement(); + final Coords c = new Coords(orbitalBombardment.getX(), orbitalBombardment.getY()); + // Is the Coord within the viewing area? + boolean insideViewArea = ((c.getX() >= drawX) && (c.getX() <= (drawX + drawWidth)) + && (c.getY() >= drawY) && (c.getY() <= (drawY + drawHeight))); + if (insideViewArea) { + Point p = getHexLocation(c); + boardGraphics.drawImage(getScaledImage(orbitalBombardmentImage, true), p.x, p.y, boardPanel); + for (Coords c2 : c.allAtDistanceOrLess(orbitalBombardment.getRadius())) { + Point p2 = getHexLocation(c2); + boardGraphics.drawImage(getScaledImage(orbitalBombardmentImage, true), p2.x, p2.y, boardPanel); + } + } + + } + } + /** * Display artillery modifier in pretargeted hexes */ @@ -1639,6 +1673,9 @@ public BufferedImage getEntireBoardImage(boolean ignoreUnits, boolean useBaseZoo // Artillery targets drawArtilleryHexes(boardGraph); + // draw Orbital Bombardment targets; + drawOrbitalBombardmentHexes(boardGraph); + // draw highlight border drawSprite(boardGraph, highlightSprite); diff --git a/megamek/src/megamek/client/ui/swing/boardview/TerrainShadowHelper.java b/megamek/src/megamek/client/ui/swing/boardview/TerrainShadowHelper.java index 9b04715eb81..2de4ebb2d26 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/TerrainShadowHelper.java +++ b/megamek/src/megamek/client/ui/swing/boardview/TerrainShadowHelper.java @@ -185,9 +185,11 @@ BufferedImage updateShadowMap() { surrounded = false; } else { Hex nhex = board.getHex(c.translated(dir)); - int lv = nhex.getLevel(); - if (lv < level) { - surrounded = false; + if (nhex != null) { + int lv = nhex.getLevel(); + if (lv < level) { + surrounded = false; + } } } } diff --git a/megamek/src/megamek/client/ui/swing/gmCommands/GamemasterCommandPanel.java b/megamek/src/megamek/client/ui/swing/gmCommands/GamemasterCommandPanel.java new file mode 100644 index 00000000000..f85f1b7b55d --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/gmCommands/GamemasterCommandPanel.java @@ -0,0 +1,82 @@ +package megamek.client.ui.swing.gmCommands; + +import megamek.client.IClient; +import megamek.client.ui.swing.ClientGUI; +import megamek.server.commands.GamemasterServerCommand; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.EnumArgument; +import megamek.server.commands.arguments.IntegerArgument; + +import javax.swing.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +// JPanel wrapper for game master commands +public class GamemasterCommandPanel extends JDialog { + private final GamemasterServerCommand command; + private final ClientGUI client; + + public GamemasterCommandPanel(JFrame parent, ClientGUI client, GamemasterServerCommand command) { + super(parent, command.getName(), true); + this.command = command; + this.client = client; + setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); + + JLabel helpLabel = new JLabel(command.getHelp()); + add(helpLabel); + + List> arguments = command.defineArguments(); + Map argumentComponents = new HashMap<>(); + + for (Argument argument : arguments) { + JLabel label = new JLabel(argument.getName() + ":"); + add(label); + + if (argument instanceof IntegerArgument intArg) { + JSpinner spinner = new JSpinner(new SpinnerNumberModel( + intArg.hasDefaultValue() ? intArg.getValue() : 0, + intArg.getMinValue(), + intArg.getMaxValue(), + 1)); + add(spinner); + argumentComponents.put(argument.getName(), spinner); + } else if (argument instanceof EnumArgument enumArg) { + JComboBox comboBox = new JComboBox<>(); + for (Enum constant : enumArg.getEnumType().getEnumConstants()) { + comboBox.addItem(constant.name()); + } + if (enumArg.getValue() != null) { + comboBox.setSelectedItem(enumArg.getValue().name()); + } + add(comboBox); + argumentComponents.put(argument.getName(), comboBox); + } + } + + JButton executeButton = new JButton("Execute Command"); + executeButton.addActionListener(e -> executeCommand(argumentComponents)); + add(executeButton); + + pack(); + setLocationRelativeTo(parent); + } + + private void executeCommand(Map argumentComponents) { + List> arguments = command.defineArguments(); + String[] args = new String[arguments.size()]; + + for (int i = 0; i < arguments.size(); i++) { + Argument argument = arguments.get(i); + JComponent component = argumentComponents.get(argument.getName()); + + if (component instanceof JSpinner) { + args[i] = argument.getName() + "=" + ((JSpinner) component).getValue().toString(); + } else if (component instanceof JComboBox) { + args[i] = argument.getName() + "=" + Objects.requireNonNull(((JComboBox) component).getSelectedItem()); + } + } + client.getClient().sendChat("/" + command.getName() + " " + String.join(" ", args)); + } +} diff --git a/megamek/src/megamek/client/ui/swing/minimap/Minimap.java b/megamek/src/megamek/client/ui/swing/minimap/Minimap.java index 7aa0f11da92..10df3f5a6ba 100644 --- a/megamek/src/megamek/client/ui/swing/minimap/Minimap.java +++ b/megamek/src/megamek/client/ui/swing/minimap/Minimap.java @@ -536,6 +536,9 @@ private void drawMap(boolean forceDraw) { for (int j = 0; j < board.getWidth(); j++) { for (int k = 0; k < board.getHeight(); k++) { Hex h = board.getHex(j, k); + if (h == null) { + continue; + } if (dirtyMap || dirty[j / 10][k / 10]) { gg.setColor(terrainColor(h)); if (h.containsTerrain(SPACE)) { diff --git a/megamek/src/megamek/client/ui/swing/tileset/TilesetManager.java b/megamek/src/megamek/client/ui/swing/tileset/TilesetManager.java index b051ee2b2e9..1a3ec4ff91e 100644 --- a/megamek/src/megamek/client/ui/swing/tileset/TilesetManager.java +++ b/megamek/src/megamek/client/ui/swing/tileset/TilesetManager.java @@ -77,6 +77,8 @@ public class TilesetManager implements IPreferenceChangeListener { private static final String FILENAME_ARTILLERY_ADJUSTED_IMAGE = "artyadj.gif"; private static final String FILENAME_ARTILLERY_INCOMING_IMAGE = "artyinc.gif"; + private static final String FILENAME_ORBITAL_BOMBARDMENT_INCOMING_IMAGE = "obinc.gif"; + public static final int ARTILLERY_AUTOHIT = 0; public static final int ARTILLERY_ADJUSTED = 1; public static final int ARTILLERY_INCOMING = 2; @@ -106,6 +108,7 @@ public class TilesetManager implements IPreferenceChangeListener { private Image artilleryAutohit; private Image artilleryAdjusted; private Image artilleryIncoming; + private Image orbitalBombardmentIncoming; /** * Hexes under the effects of ECM have a shaded "static" image displayed, @@ -414,6 +417,10 @@ public Image getEcmStaticImage(Color tint) { return image; } + public Image getOrbitalBombardmentImage() { + return orbitalBombardmentIncoming; + } + public Image getArtilleryTarget(int which) { switch (which) { case ARTILLERY_AUTOHIT: @@ -486,6 +493,7 @@ public void loadNeededImages(Game game) { artilleryAutohit = LoadSpecificImage(Configuration.hexesDir(), FILENAME_ARTILLERY_AUTOHIT_IMAGE); artilleryAdjusted = LoadSpecificImage(Configuration.hexesDir(), FILENAME_ARTILLERY_ADJUSTED_IMAGE); artilleryIncoming = LoadSpecificImage(Configuration.hexesDir(), FILENAME_ARTILLERY_INCOMING_IMAGE); + orbitalBombardmentIncoming = LoadSpecificImage(Configuration.hexesDir(), FILENAME_ORBITAL_BOMBARDMENT_INCOMING_IMAGE); started = true; } diff --git a/megamek/src/megamek/common/Board.java b/megamek/src/megamek/common/Board.java index 66bd8facb55..f3bdc36db1f 100644 --- a/megamek/src/megamek/common/Board.java +++ b/megamek/src/megamek/common/Board.java @@ -21,11 +21,7 @@ package megamek.common; import static java.util.stream.Collectors.toList; -import static megamek.common.SpecialHexDisplay.Type.ARTILLERY_DRIFT; -import static megamek.common.SpecialHexDisplay.Type.ARTILLERY_MISS; -import static megamek.common.SpecialHexDisplay.Type.BOMB_DRIFT; -import static megamek.common.SpecialHexDisplay.Type.BOMB_HIT; -import static megamek.common.SpecialHexDisplay.Type.BOMB_MISS; +import static megamek.common.SpecialHexDisplay.Type.*; import java.io.*; import java.util.*; @@ -1752,7 +1748,7 @@ public Hashtable> getSpecialHexDisplayTabl public void setSpecialHexDisplayTable(Hashtable> shd) { Hashtable> temp = new Hashtable<>(); - // Grab all current ARTILLERY_MISS instances + // Grab all current ARTILLERY_MISS and ARTILLERY_DRIFT instances for (Map.Entry> e : specialHexes.entrySet()) { for (SpecialHexDisplay special : e.getValue()) { if (Set.of(ARTILLERY_MISS, ARTILLERY_DRIFT).contains(special.getType())) { @@ -2087,4 +2083,25 @@ public static int decodeCustomDeploymentZoneID(int zoneID) { public static int encodeCustomDeploymentZoneID(int zoneID) { return zoneID + NUM_ZONES_X2; } + + public void clearOrbitalBombardmentIcons() { + for (Coords coords : specialHexes.keySet()) { + removeOrbitalBombardmentIcons(coords); + } + } + + public void removeOrbitalBombardmentIcons(Coords coords) { + // Do nothing if the coords aren't on this board. + if (!this.contains(coords) || null == specialHexes.get(coords)) { + return; + } + + // Use iterator so we can remove while traversing + for (Iterator iterator = specialHexes.get(coords).iterator(); iterator.hasNext();) { + SpecialHexDisplay shd = iterator.next(); + if (ORBITAL_BOMBARDMENT.equals(shd.getType())) { + iterator.remove(); + } + } + } } diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index 06d10d45800..d54020dc530 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -4790,6 +4790,16 @@ public List getCriticalSlots(int location) { return result; } + public boolean hasUndamagedCriticalSlots() { + return IntStream.range(0, locations()) + .mapToLong(i -> getCriticalSlots(i) + .stream() + .filter(Objects::nonNull) + .filter(CriticalSlot::isHittable) + .count() + ).sum() > 0; + } + /** * @return True when this unit has a RISC Super-Cooled Myomer System (even if * the SCM is destroyed). diff --git a/megamek/src/megamek/common/Game.java b/megamek/src/megamek/common/Game.java index cd106d57c9d..55f5ea13373 100644 --- a/megamek/src/megamek/common/Game.java +++ b/megamek/src/megamek/common/Game.java @@ -15,12 +15,6 @@ */ package megamek.common; -import static java.util.stream.Collectors.toList; - -import java.io.Serializable; -import java.util.*; -import java.util.concurrent.CopyOnWriteArrayList; - import megamek.MMConstants; import megamek.Version; import megamek.client.bot.princess.BehaviorSettings; @@ -39,9 +33,16 @@ import megamek.common.weapons.AttackHandler; import megamek.logging.MMLogger; import megamek.server.SmokeCloud; +import megamek.server.props.OrbitalBombardment; import megamek.server.victory.VictoryHelper; import megamek.server.victory.VictoryResult; +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; + +import static java.util.stream.Collectors.toList; + /** * The game class is the root of all data about the game in progress. Both the * Client and the Server should have one of these objects, and it is their job @@ -113,7 +114,7 @@ public final class Game extends AbstractGame implements Serializable, PlanetaryC private Vector vibrabombs = new Vector<>(); private Vector attacks = new Vector<>(); private Vector offboardArtilleryAttacks = new Vector<>(); - + private Vector orbitalBombardmentAttacks = new Vector(); private int lastEntityId; private Vector tagInfoForTurn = new Vector<>(); @@ -2202,6 +2203,19 @@ public int removeSpecificEntityTurnsFor(Entity entity) { return turnsToRemove.size(); } + public void setOrbitalBombardmentVector(Vector v) { + orbitalBombardmentAttacks = v; + processGameEvent(new GameBoardChangeEvent(this)); + } + + public void resetOrbitalBombardmentAttacks() { + orbitalBombardmentAttacks.removeAllElements(); + } + + public Enumeration getOrbitalBombardmentAttacks() { + return orbitalBombardmentAttacks.elements(); + } + public void setArtilleryVector(Vector v) { offboardArtilleryAttacks = v; processGameEvent(new GameBoardChangeEvent(this)); diff --git a/megamek/src/megamek/common/SpecialHexDisplay.java b/megamek/src/megamek/common/SpecialHexDisplay.java index 170966e71dd..2fb11e1cac3 100644 --- a/megamek/src/megamek/common/SpecialHexDisplay.java +++ b/megamek/src/megamek/common/SpecialHexDisplay.java @@ -96,16 +96,12 @@ public boolean drawBefore() { } }, PLAYER_NOTE(new MegaMekFile(Configuration.hexesDir(), "note.png").toString()) { - @Override - public boolean drawBefore() { - return true; - } - @Override public boolean drawAfter() { return true; } - }; + }, + ORBITAL_BOMBARDMENT(new MegaMekFile(Configuration.hexesDir(), "obinc.gif").toString()); private transient Image defaultImage; private final String defaultImagePath; @@ -250,16 +246,17 @@ public int getObscuredLevel() { * @return */ public boolean isObscured(Player other) { + if (owner == null) { + return false; + } if ((obscured == SHD_OBSCURED_OWNER) && owner.equals(other)) { return false; } else if ((obscured == SHD_OBSCURED_TEAM) && (other != null) && (owner.getTeam() == other.getTeam())) { return false; - } else if (obscured == SHD_OBSCURED_ALL) { - return false; - } else { - return true; } + + return obscured != SHD_OBSCURED_ALL; } public void setObscured(int obscured) { @@ -272,7 +269,7 @@ public void setObscured(int obscured) { * display * in the appropriate phase. Other bomb- or artillery-related graphics are * optional. - * + * * @param phase * @param curRound * @param playerChecking diff --git a/megamek/src/megamek/common/weapons/AreaEffectHelper.java b/megamek/src/megamek/common/weapons/AreaEffectHelper.java index b9a4e04ca5d..32ea8900b05 100644 --- a/megamek/src/megamek/common/weapons/AreaEffectHelper.java +++ b/megamek/src/megamek/common/weapons/AreaEffectHelper.java @@ -13,19 +13,16 @@ */ package megamek.common.weapons; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Vector; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; import megamek.common.*; import megamek.common.planetaryconditions.Atmosphere; import megamek.common.planetaryconditions.PlanetaryConditions; import megamek.logging.MMLogger; import megamek.server.totalwarfare.TWGameManager; +import org.apache.commons.lang3.IntegerRange; /** * Class containing functionality that helps out with area effect weapons. @@ -805,6 +802,14 @@ public static void applyExplosionClusterDamageToEntity(Entity entity, int damage HitData hit = entity.rollHitLocation(table, Compute.targetSideTable(position, entity)); vDesc.addAll(gameManager.damageEntity(entity, hit, cluster, false, DamageType.IGNORE_PASSENGER, false, true)); + + // If there is nothing left to destroy in the unit + if ( + entity.isDoomed() + && (!entity.hasUndamagedCriticalSlots() || entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_DEVASTATED) + ) { + break; + } damage -= cluster; } } diff --git a/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java b/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java index d50836c22e2..d9ca02e4e9f 100644 --- a/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java +++ b/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java @@ -21,59 +21,54 @@ import megamek.common.Entity; import megamek.common.Player; import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.IntegerArgument; import megamek.server.totalwarfare.TWGameManager; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + /** * The Server Command "/changeOwner" that will switch an entity's owner to another player. * * @author Luana Scoppio */ -public class ChangeOwnershipCommand extends ServerCommand implements IsGM { - - private final TWGameManager gameManager; +public class ChangeOwnershipCommand extends GamemasterServerCommand { public ChangeOwnershipCommand(Server server, TWGameManager gameManager) { super(server, + gameManager, "changeOwner", "Switches ownership of a player's entity to another player during the end phase. " - + "Usage: /changeOwner " - + "The following is an example of changing unit ID 7 to player ID 2: /changeOwner 7 2 "); - this.gameManager = gameManager; + + "Usage: /changeOwner " + + "The following is an example of changing unit ID 7 to player ID 2: /changeOwner 7 2 "); } - /** - * Run this command with the arguments supplied - * - * @see ServerCommand#run(int, String[]) - */ @Override - public void run(int connId, String[] args) { - try { - if (!isGM(connId)) { - server.sendServerChat(connId, "You are not a Game Master."); - return; - } - - int eid = Integer.parseInt(args[1]); - Entity ent = gameManager.getGame().getEntity(eid); - int pid = Integer.parseInt(args[2]); - Player player = server.getGame().getPlayer(pid); - if (null == ent) { - server.sendServerChat(connId, "No such entity."); - } else if (null == player) { - server.sendServerChat(connId, "No such player."); - } else if (player.getTeam() == Player.TEAM_UNASSIGNED) { - server.sendServerChat(connId, "Player must be assigned a team."); - } else { - server.sendServerChat(connId, ent.getDisplayName() + " will switch to " + player.getName() + "'s side at the end of this turn."); - ent.setTraitorId(pid); - } - } catch (NumberFormatException ignored) { - } + public List> defineArguments() { + List> arguments = new ArrayList<>(); + arguments.add(new IntegerArgument("unitID", 0, Integer.MAX_VALUE)); + arguments.add(new IntegerArgument("playerID", 0, Integer.MAX_VALUE)); + return arguments; } @Override - public TWGameManager getGameManager() { - return gameManager; + protected void runAsGM(int connId, Map> args) { + IntegerArgument unitID = (IntegerArgument) args.get("unitID"); + IntegerArgument playerID = (IntegerArgument) args.get("playerID"); + + Entity ent = gameManager.getGame().getEntity(unitID.getValue()); + Player player = server.getGame().getPlayer(playerID.getValue()); + if (null == ent) { + server.sendServerChat(connId, "No such entity."); + } else if (null == player) { + server.sendServerChat(connId, "No such player."); + } else if (player.getTeam() == Player.TEAM_UNASSIGNED) { + server.sendServerChat(connId, "Player must be assigned a team."); + } else { + server.sendServerChat(connId, ent.getDisplayName() + " will switch to " + player.getName() + "'s side at the end of this turn."); + ent.setTraitorId(player.getId()); + } } } diff --git a/megamek/src/megamek/server/commands/ChangeWeatherCommand.java b/megamek/src/megamek/server/commands/ChangeWeatherCommand.java index 1e75fd97e2d..3ff818d1c1d 100644 --- a/megamek/src/megamek/server/commands/ChangeWeatherCommand.java +++ b/megamek/src/megamek/server/commands/ChangeWeatherCommand.java @@ -20,9 +20,13 @@ import megamek.common.planetaryconditions.*; import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.IntegerArgument; import megamek.server.totalwarfare.TWGameManager; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -31,9 +35,7 @@ /** * @author Luana Scoppio */ -public class ChangeWeatherCommand extends ServerCommand { - - private final TWGameManager gameManager; +public class ChangeWeatherCommand extends GamemasterServerCommand { private static final String HELP_TEXT = "GM changes (weather) planetary conditions. The parameters are optional and unordered " + "and the effects are applied at the beginning of the next turn. The square brackets means that argument is optional. " + @@ -44,19 +46,30 @@ public class ChangeWeatherCommand extends ServerCommand { "winddir= 0: south, 1: southwest, 2: northwest, 3: north, 4: northeast, 5: southeast, 6: random " + "atmo= 0: vacuum, 1: trace, 2: thin, 3: standard, 4: high, 5: very high " + "blowsand= 0: no, 1: yes " + + "emi= 0: no, 1: yes " + "weather= 0: clear, 1: light rain, 2: moderate rain, 3: heavy rain, 4: gusting rain, 5: downpour, 6: light snow " + "7: moderate snow, 8: snow flurries, 9: heavy snow, 10: sleet, 11: ice storm, 12: light hail, 13: heavy hail " + "14: lightning storm"; /** Creates new ChangeWeatherCommand */ public ChangeWeatherCommand(Server server, TWGameManager gameManager) { - super(server, "weather", HELP_TEXT); - this.gameManager = gameManager; + super(server, gameManager, "weather", HELP_TEXT); + } + + public List> defineArguments() { + List> arguments = new ArrayList<>(); + arguments.add(new IntegerArgument("fog", 0, 2)); + arguments.add(new IntegerArgument("light", 0, 6)); + arguments.add(new IntegerArgument("wind", 0, 6)); + arguments.add(new IntegerArgument("atmo", 0, 5)); + arguments.add(new IntegerArgument("blowsand", 0, 1)); + arguments.add(new IntegerArgument("emi", 0, 1)); + arguments.add(new IntegerArgument("weather", 0, 14)); + return arguments; } - private void updatePlanetaryCondition(String arg, String prefix, int connId, int maxLength, Consumer setter, + private void updatePlanetaryCondition(int value, int connId, int maxLength, Consumer setter, Function successMessage, Function errorMessage) { - var value = Integer.parseInt(arg.substring(prefix.length())); if (value >= 0 && value < maxLength) { setter.accept(value); server.sendServerChat(connId, successMessage.apply(value)); @@ -65,50 +78,47 @@ private void updatePlanetaryCondition(String arg, String prefix, int connId, int } } + private record Condition(int maxLength, Consumer setter, Function successMessage, Function errorMessage) {} /** * Run this command with the arguments supplied */ @Override - public void run(int connId, String[] args) { - if (!server.getPlayer(connId).getGameMaster()) { - server.sendServerChat(connId, "You are not a Game Master."); - return; - } - - var planetaryConditions = gameManager.getGame().getPlanetaryConditions(); + public void runAsGM(int connId, Map> args) { + var planetaryConditions = getGameManager().getGame().getPlanetaryConditions(); - if (args.length > 1) { + Map conditions = Map.of( + "fog", new Condition(Fog.values().length, value -> planetaryConditions.setFog(Fog.values()[value]), + value -> "The fog has changed.", maxLength -> "Invalid fog value. Must be between 0 and " + (maxLength - 1)), + "wind", new Condition(Wind.values().length, value -> planetaryConditions.setWind(Wind.values()[value]), + value -> "The wind strength has changed.", maxLength -> "Invalid wind value. Must be between 0 and " + (maxLength - 1)), + "winddir", new Condition(WindDirection.values().length, value -> planetaryConditions.setWindDirection(WindDirection.values()[value]), + value -> "The wind direction has changed.", maxLength -> "Invalid wind direction value. Must be between 0 and " + (maxLength - 1)), + "light", new Condition(Light.values().length, value -> planetaryConditions.setLight(Light.values()[value]), + value -> "The light has changed.", maxLength -> "Invalid light value. Must be between 0 and " + (maxLength - 1)), + "atmo", new Condition(Atmosphere.values().length, value -> planetaryConditions.setAtmosphere(Atmosphere.values()[value]), + value -> value == 0 ? "The air has vanished, put your vac suits!" : "The air is changing.", maxLength -> "Invalid atmosphere value. Must be between 0 and " + (maxLength - 1)), + "blowsand", new Condition(BlowingSand.values().length, value -> planetaryConditions.setBlowingSand(BlowingSand.values()[value]), + value -> value == 1 ? "Sand started blowing." : "The sand has settled.", maxLength -> "Invalid blowsand value. Must be between 0 and " + (maxLength - 1)), + "weather", new Condition(Weather.values().length, value -> planetaryConditions.setWeather(Weather.values()[value]), + value -> "The weather has changed.", maxLength -> "Invalid weather value. Must be between 0 and " + (maxLength - 1)), + "emi", new Condition(EMI.values().length, value -> planetaryConditions.setEMI(EMI.values()[value]), + value -> value == 1 ? "EMI is active." : "EMI is inactive.", maxLength -> "Invalid EMI value. Must be between 0 and " + (maxLength - 1)) + ); + conditions.forEach((prefix, condition) -> { + if (args.containsKey(prefix)) { + updatePlanetaryCondition( + (int) args.get(prefix).getValue(), + connId, + condition.maxLength, + condition.setter, + condition.successMessage, + condition.errorMessage); + } + }); - Map conditions = Map.of( - "fog=", new Condition(Fog.values().length, value -> planetaryConditions.setFog(Fog.values()[value]), - value -> "The fog has changed.", maxLength -> "Invalid fog value. Must be between 0 and " + (maxLength - 1)), - "wind=", new Condition(Wind.values().length, value -> planetaryConditions.setWind(Wind.values()[value]), - value -> "The wind strength has changed.", maxLength -> "Invalid wind value. Must be between 0 and " + (maxLength - 1)), - "winddir=", new Condition(WindDirection.values().length, value -> planetaryConditions.setWindDirection(WindDirection.values()[value]), - value -> "The wind direction has changed.", maxLength -> "Invalid wind direction value. Must be between 0 and " + (maxLength - 1)), - "light=", new Condition(Light.values().length, value -> planetaryConditions.setLight(Light.values()[value]), - value -> "The light has changed.", maxLength -> "Invalid light value. Must be between 0 and " + (maxLength - 1)), - "atmo=", new Condition(Atmosphere.values().length, value -> planetaryConditions.setAtmosphere(Atmosphere.values()[value]), - value -> value == 0 ? "The air has vanished, put your vac suits!" : "The air is changing.", maxLength -> "Invalid atmosphere value. Must be between 0 and " + (maxLength - 1)), - "blowsand=", new Condition(BlowingSand.values().length, value -> planetaryConditions.setBlowingSand(BlowingSand.values()[value]), - value -> value == 1 ? "Sand started blowing." : "The sand has settled.", maxLength -> "Invalid blowsand value. Must be between 0 and " + (maxLength - 1)), - "weather=", new Condition(Weather.values().length, value -> planetaryConditions.setWeather(Weather.values()[value]), - value -> "The weather has changed.", maxLength -> "Invalid weather value. Must be between 0 and " + (maxLength - 1)) - ); - - Stream.of(args) - .forEach(arg -> conditions.forEach((prefix, condition) -> { - if (arg.startsWith(prefix)) { - updatePlanetaryCondition(arg, prefix, connId, condition.maxLength, condition.setter, condition.successMessage, condition.errorMessage); - } - })); - - gameManager.getGame().setPlanetaryConditions(planetaryConditions); - } else { - // Error out; it's not a valid call. - server.sendServerChat(connId, "weather command failed. " + HELP_TEXT); - } + getGameManager().getGame().setPlanetaryConditions(planetaryConditions); } + } diff --git a/megamek/src/megamek/server/commands/DisasterCommand.java b/megamek/src/megamek/server/commands/DisasterCommand.java index 0a7137cd88b..f3fc13e354d 100644 --- a/megamek/src/megamek/server/commands/DisasterCommand.java +++ b/megamek/src/megamek/server/commands/DisasterCommand.java @@ -18,45 +18,149 @@ */ package megamek.server.commands; +import megamek.common.Coords; +import megamek.logging.MMLogger; import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.EnumArgument; import megamek.server.totalwarfare.TWGameManager; +import java.util.List; +import java.util.Map; +import java.util.Objects; + /** * @author Luana Scoppio */ -public class DisasterCommand extends ServerCommand { +public class DisasterCommand extends GamemasterServerCommand { - private final TWGameManager gameManager; + enum Disaster { + RANDOM, + HURRICANE, + LIGHTNING_STORM, + ORBITAL_BOMBARDMENT, + ORBITAL_BOMBARDMENT_2, + ORBITAL_BOMBARDMENT_3, + SANDSTORM, + HAILSTORM, + ECLIPSE, + SOLAR_FLARE, + SUPERNOVA, + SMOG, + FIRESTORM, + TRAITOR; + + public static Disaster getRandomDisaster() { + return values()[(int) (Math.random() * values().length)]; + } + } - /** Creates new DisasterCommand */ public DisasterCommand(Server server, TWGameManager gameManager) { - super(server, "disaster", "GM calls a disaster at random, arguments in square brackets are optional. Usage: /disaster [type] " + + super(server, gameManager, "gomorrah", "GM calls a disaster, arguments in square brackets are optional. " + + "Usage: /gomorrah [type] " + "if not type is passed, one is chosen at random. " + - "type= 0: hurricane, 1: lightning storm, 2: meteor shower, 3: orbital bombardment, 4: wildfire, 5: sandstorm, 6: hailstorm, " + - "7: heatwave"); - this.gameManager = gameManager; + "Type can be one of the following: hurricane, lightning, ob, ob2, ob3, sandstorm, hailstorm, eclipse, solarflare, " + + "supernova, smog, firestorm, traitor. " + + "The type ob, ob2 and ob3 are orbital bombardment with one, two or three random hit locations, at default values " + + "for damage (100) and radius (4)."); + } + + @Override + public List> defineArguments() { + return List.of(new EnumArgument<>("type", Disaster.class, Disaster.RANDOM)); + } + + private void runDisasterCommand(int connId, Disaster disaster) { + switch (disaster) { + case HURRICANE: + new ChangeWeatherCommand(server, gameManager).run(connId, new String[]{"weather", "wind=6", "winddir=6"}); + server.sendServerChat("Hurricane incoming!"); + break; + case LIGHTNING_STORM: + new ChangeWeatherCommand(server, gameManager).run(connId, new String[]{"weather", "weather=14"}); + server.sendServerChat("Lightning storm incoming!"); + break; + case ECLIPSE: + new ChangeWeatherCommand(server, gameManager).run(connId, new String[]{"weather", "light=4"}); + server.sendServerChat("The sun is being eclipsed..."); + break; + case SOLAR_FLARE: + new ChangeWeatherCommand(server, gameManager).run(connId, new String[]{"weather", "light=5", "emi=1"}); + new FirestormCommand(server, gameManager).run(connId, new String[]{"firestorm", "1", "5"}); + server.sendServerChat("Sensors warn of an imminent solar flare incoming! Expect some fires."); + break; + case SUPERNOVA: + new ChangeWeatherCommand(server, gameManager).run(connId, new String[]{"weather", "light=5", "emi=1", "atmo=2", "wind=0", "weather=0"}); + new FirestormCommand(server, gameManager).run(connId, new String[]{"firestorm", "2", "75"}); + server.sendServerChat("The star is going supernova!"); + server.sendServerChat("Everything is on fire! We are doomed!"); + break; + case ORBITAL_BOMBARDMENT_3: + orbitalBombardment(connId); + case ORBITAL_BOMBARDMENT_2: + orbitalBombardment(connId); + case ORBITAL_BOMBARDMENT: + orbitalBombardment(connId); + break; + case SANDSTORM: + new ChangeWeatherCommand(server, gameManager).run(connId, new String[]{"weather", "blowsand=1", "wind=4", "winddir=6"}); + server.sendServerChat("A sandstorm is approaching!"); + break; + case HAILSTORM: + new ChangeWeatherCommand(server, gameManager).run(connId, new String[]{"weather", "weather=13", "wind=4", "winddir=6"}); + server.sendServerChat("A hailstorm is incoming!"); + break; + case FIRESTORM: + new FirestormCommand(server, gameManager).run(connId, new String[]{"firestorm", "2", "50"}); + server.sendServerChat("A firestorm is consuming the battlefield!"); + break; + case SMOG: + new ChangeWeatherCommand(server, gameManager).run(connId, new String[]{"weather", "atmo=5", "fog=2", "light=1"}); + server.sendServerChat("A thick smog is covering the battlefield!"); + break; + case TRAITOR: + default: + { + var players = gameManager.getGame().getPlayersList(); + var randomPlayer = players.get((int) (Math.random() * players.size())); + + var units = gameManager.getGame().getPlayerEntities(randomPlayer, true); + var randomUnit = units.get((int) (Math.random() * units.size())); + + var otherPlayers = players.stream().filter(p -> p != randomPlayer).toList(); + var newOwner = otherPlayers.get((int) (Math.random() * otherPlayers.size())); + + new ChangeOwnershipCommand(server, gameManager).run(connId, + new String[]{"traitor", "" + randomUnit.getId(), "" + newOwner.getId()}); + server.sendServerChat("A traitor has been revealed!"); + } + } + } + + private Coords getRandomHexCoords() { + var board = gameManager.getGame().getBoard(); + var x = (int) (Math.random() * board.getWidth()); + var y = (int) (Math.random() * board.getHeight()); + return new Coords(x, y); + } + + private void orbitalBombardment(int connId) { + var coords = getRandomHexCoords(); + new OrbitalBombardmentCommand(server, gameManager).run(connId, new String[]{"ob", + coords.getX() + 1 + "", + coords.getY() + 1 + "" + }); } /** * Run this command with the arguments supplied */ @Override - public void run(int connId, String[] args) { - if (!server.getPlayer(connId).getGameMaster()) { - server.sendServerChat(connId, "You are not a Game Master."); - return; - } - - // Check argument integrity. - if (args.length == 1) { - // Check command - // NOT IMPLEMENTED - server.sendServerChat(connId, "Oh no..."); - } else if (args.length == 2) { - // Error out; it's not a valid call. - server.sendServerChat(connId, "Oh no..."); + protected void runAsGM(int connId, Map> args) { + if (args.get("type").getValue().equals(Disaster.RANDOM)) { + runDisasterCommand(connId, Disaster.getRandomDisaster()); } else { - server.sendServerChat(connId, "disaster command failed (1)."); + runDisasterCommand(connId, (Disaster) args.get("type").getValue()); } } } diff --git a/megamek/src/megamek/server/commands/FirestarterCommand.java b/megamek/src/megamek/server/commands/FirestarterCommand.java new file mode 100644 index 00000000000..8e4fece3254 --- /dev/null +++ b/megamek/src/megamek/server/commands/FirestarterCommand.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ +package megamek.server.commands; + +import megamek.common.Coords; +import megamek.common.Hex; +import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.IntegerArgument; +import megamek.server.totalwarfare.TWGameManager; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * The Server Command "/firestarter" that will put one hex on fire. + * + * @author Luana Scoppio + */ +public class FirestarterCommand extends GamemasterServerCommand { + + public FirestarterCommand(Server server, TWGameManager gameManager) { + super(server, + gameManager, + "firestarter", + "Starts fire in one specific hex at a specific intensity. " + + "Usage: /firestarter [] " + + "The intensity can be 1=Norma, 2=Inferno, 3=Inferno Bomb or 4=Inferno IV, default is 1."); + } + + + @Override + public List> defineArguments() { + return List.of( + new IntegerArgument("x"), + new IntegerArgument("y"), + new IntegerArgument("intensity", 1, 4, 1)); + } + + /** + * Run this command with the arguments supplied + * + * @see ServerCommand#run(int, String[]) + */ + @Override + protected void runAsGM(int connId, Map> args) { + int xArg = (int) args.get("x").getValue() - 1; + int yArg = (int) args.get("y").getValue() -1; + int fireType = (int) args.get("intensity").getValue(); + + igniteHex(new Coords(xArg, yArg), fireType); + } + + private void igniteHex(Coords coords, int fireType) { + try { + Hex hex = gameManager.getGame().getBoard().getHex(coords); + Objects.requireNonNull(hex, "Hex not found."); + gameManager.ignite(coords, fireType, gameManager.getvPhaseReport()); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to ignite hex: " + e.getMessage()); + } + } + +} diff --git a/megamek/src/megamek/server/commands/FirestormCommand.java b/megamek/src/megamek/server/commands/FirestormCommand.java new file mode 100644 index 00000000000..03d2e03d961 --- /dev/null +++ b/megamek/src/megamek/server/commands/FirestormCommand.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ +package megamek.server.commands; + +import megamek.common.Coords; +import megamek.common.Hex; +import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.IntegerArgument; +import megamek.server.totalwarfare.TWGameManager; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * The Server Command "/firestorm" that starts a blazing inferno on the board. + * + * @author Luana Scoppio + */ +public class FirestormCommand extends GamemasterServerCommand { + + public FirestormCommand(Server server, TWGameManager gameManager) { + super(server, + gameManager, + "firestorm", + "Starts fire in the entire board. " + + "Usage: /firestorm [] []" + + "The intensity can be 1=Normal, 2=Inferno, 3=Inferno Bomb or 4=Inferno IV, default is 1. " + + "The size can be a percent of the board, from 1 to 100, default is 25."); + } + + @Override + public List> defineArguments() { + return List.of(new IntegerArgument("intensity", 1, 4, 1), + new IntegerArgument("percent", 1, 100, 25)); + } + + /** + * Run this command with the arguments supplied + * + * @see ServerCommand#run(int, String[]) + */ + @Override + protected void runAsGM(int connId, Map> args) { + try { + var fireType = (int) args.get("intensity").getValue(); + var percent = (int) args.get("percent").getValue(); + var coords = getRandomCoords(numberOfCoordsFromPercent(percent)); + coords.forEach(c -> igniteHex(c, fireType)); + } catch (Exception e) { + logger.error("Failed to ignite fire.", e); + server.sendServerChat(connId, "Failed to ignite fire."); + } + } + + private int numberOfCoordsFromPercent(int percent) { + var boardHeight = gameManager.getGame().getBoard().getHeight(); + var boardWidth = gameManager.getGame().getBoard().getWidth(); + return Math.max((boardWidth * boardHeight * percent / 100), 1); + } + + private HashSet getRandomCoords(int size) { + var boardHeight = gameManager.getGame().getBoard().getHeight(); + var boardWidth = gameManager.getGame().getBoard().getWidth(); + var coordsSet = new HashSet(); + var maxTries = size * 10; + while (coordsSet.size() < size && maxTries > 0) { + var x = (int) (Math.random() * boardWidth); + var y = (int) (Math.random() * boardHeight); + coordsSet.add(new Coords(x, y)); + maxTries--; + } + + return coordsSet; + } + + private void igniteHex(Coords coords, int fireType) { + Hex hex = gameManager.getGame().getBoard().getHex(coords); + if (null == hex) { + // Just ignore null hexes... they should not happen, but I don't want to crash the command + return; + } + gameManager.ignite(coords, fireType, gameManager.getvPhaseReport()); + } +} diff --git a/megamek/src/megamek/server/commands/GamemasterServerCommand.java b/megamek/src/megamek/server/commands/GamemasterServerCommand.java new file mode 100644 index 00000000000..2c56403cc29 --- /dev/null +++ b/megamek/src/megamek/server/commands/GamemasterServerCommand.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ +package megamek.server.commands; + +import megamek.logging.MMLogger; +import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.totalwarfare.TWGameManager; + +import java.util.*; + +/** + * A ServerCommand that can only be used by Game Masters + * + * @author Luana Coppio + */ +public abstract class GamemasterServerCommand extends ServerCommand { + + private static final String EMPTY_ARGUMENT = null; + protected final TWGameManager gameManager; + protected final static MMLogger logger = MMLogger.create(GamemasterServerCommand.class); + private final String errorMsg; + /** + * Creates new ServerCommand that can only be used by Game Masters + * + * @param server instance of the server + * @param gameManager instance of the game manager + * @param name the name of the command + * @param helpText the help text for the command + */ + public GamemasterServerCommand(Server server, TWGameManager gameManager, String name, String helpText) { + super(server, name, helpText); + this.gameManager = gameManager; + this.errorMsg = "Error executing command: " + name; + } + + private boolean isGM(int connId) { + return server.getGameManager().getGame().getPlayer(connId).getGameMaster(); + } + + protected TWGameManager getGameManager() { + return gameManager; + } + + @Override + public void run(int connId, String[] args) { + if (!isGM(connId)) { + server.sendServerChat(connId, "This command can only be used by a game master."); + return; + } + + try { + Map> parsedArguments = parseArguments(args); + runAsGM(connId, parsedArguments); + } catch (IllegalArgumentException e) { + server.sendServerChat(connId, "Invalid arguments: " + e.getMessage() + "\nUsage: " + this.getHelp()); + } catch (Exception e) { + server.sendServerChat(connId, "An error occurred while executing the command. Check the log for more information"); + logger.error(errorMsg, e); + } + } + + // Method to parse arguments, to be implemented by the specific command class + public abstract List> defineArguments(); + + + // Parses the arguments using the definition + private Map> parseArguments(String[] args) { + List> argumentDefinitions = defineArguments(); + Map> parsedArguments = new HashMap<>(); + List positionalArguments = new ArrayList<>(); + + // Map argument names to definitions for easy lookup + Map> argumentMap = new HashMap<>(); + for (Argument argument : argumentDefinitions) { + argumentMap.put(argument.getName(), argument); + } + + // Separate positional arguments and named arguments + boolean namedArgumentStarted = false; + for (String arg : List.of(args)) { + String[] keyValue = arg.split("="); + + if (keyValue.length == 2) { + // Handle named arguments + namedArgumentStarted = true; + String key = keyValue[0]; + String value = keyValue[1]; + + if (!argumentMap.containsKey(key)) { + throw new IllegalArgumentException("Unknown argument: " + key); + } + + Argument argument = argumentMap.get(key); + argument.parse(value); + parsedArguments.put(key, argument); + } else { + // Handle positional arguments + if (namedArgumentStarted) { + throw new IllegalArgumentException("Positional arguments cannot come after named arguments."); + } + positionalArguments.add(arg); + } + } + + // Parse positional arguments + int index = 0; + for (Argument argument : argumentDefinitions) { + if (parsedArguments.containsKey(argument.getName())) { + continue; + } + if (index < positionalArguments.size()) { + String value = positionalArguments.get(index); + argument.parse(value); + parsedArguments.put(argument.getName(), argument); + index++; + } else { + // designed to throw an error if the arg doesn't have a default value + argument.parse(EMPTY_ARGUMENT); + parsedArguments.put(argument.getName(), argument); + } + } + + return parsedArguments; + } + + // The new method for game master commands that uses parsed arguments + protected abstract void runAsGM(int connId, Map> args); +} diff --git a/megamek/src/megamek/server/commands/IsGM.java b/megamek/src/megamek/server/commands/IsGM.java deleted file mode 100644 index 6c8fcff5545..00000000000 --- a/megamek/src/megamek/server/commands/IsGM.java +++ /dev/null @@ -1,13 +0,0 @@ -package megamek.server.commands; - -import megamek.server.totalwarfare.TWGameManager; - -public interface IsGM { - - TWGameManager getGameManager(); - - default boolean isGM(int connId) { - return getGameManager().getGame().getPlayer(connId).getGameMaster(); - } - -} diff --git a/megamek/src/megamek/server/commands/KillCommand.java b/megamek/src/megamek/server/commands/KillCommand.java index 45f2b6d7dca..3001ae0fe5a 100644 --- a/megamek/src/megamek/server/commands/KillCommand.java +++ b/megamek/src/megamek/server/commands/KillCommand.java @@ -20,59 +20,47 @@ import megamek.common.options.OptionsConstants; import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.IntegerArgument; import megamek.server.totalwarfare.TWGameManager; +import java.util.List; +import java.util.Map; + /** * @author Luana Scoppio */ -public class KillCommand extends ServerCommand implements IsGM { +public class KillCommand extends GamemasterServerCommand{ private final TWGameManager gameManager; /** Creates new KillCommand */ public KillCommand(Server server, TWGameManager gameManager) { - super(server, "kill", "Allows a GM to destroy a single unit instantly" + + super(server, gameManager, "kill", "Allows a GM to destroy a single unit instantly" + "Usage: "+ "/kill " + "where id is the units ID. The units ID can be found by hovering over the unit."); this.gameManager = gameManager; } + @Override + public List> defineArguments() { + return List.of(new IntegerArgument("unitID", 0, Integer.MAX_VALUE)); + } + /** * Run this command with the arguments supplied */ @Override - public void run(int connId, String[] args) { - - // Check to make sure gm kills are allowed! - if (!isGM(connId)) { - server.sendServerChat(connId, "You are not a Game Master."); + protected void runAsGM(int connId, Map> args) { + int unitId = (int) args.get("unitID").getValue(); + // is the unit on the board? + var unit = gameManager.getGame().getEntity(unitId); + if (unit == null) { + server.sendServerChat(connId, "Specified unit is not on the board."); return; } - // Check argument integrity. - if (args.length == 2) { - // Check command - try { - int unitId = Integer.parseInt(args[1]); - // is the unit on the board? - var unit = gameManager.getGame().getEntity(unitId); - if (unit == null) { - server.sendServerChat(connId, "Specified unit is not on the board."); - return; - } - gameManager.destroyEntity(unit, "Act of God", false, false); - server.sendServerChat(connId, unit.getDisplayName() + " has been destroyed."); - } catch (NumberFormatException e) { - server.sendServerChat(connId, "Kill command failed (2). Proper format is \"/kill \" where id is the units numerical ID"); - } - } else { - // Error out; it's not a valid call. - server.sendServerChat(connId, "Kill command failed (1). Proper format is \"/kill \" where id is the units ID"); - } - } - - @Override - public TWGameManager getGameManager() { - return gameManager; + gameManager.destroyEntity(unit, "Act of God", false, false); + server.sendServerChat(unit.getDisplayName() + " has been devastated."); } } diff --git a/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java b/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java index 5424526f93d..9a970eb2d1f 100644 --- a/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java +++ b/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java @@ -20,84 +20,66 @@ import megamek.common.options.OptionsConstants; import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.IntegerArgument; import megamek.server.props.OrbitalBombardment; import megamek.server.totalwarfare.TWGameManager; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + /** * @author Luana Scoppio */ -public class OrbitalBombardmentCommand extends ServerCommand implements IsGM { - - private final TWGameManager gameManager; +public class OrbitalBombardmentCommand extends GamemasterServerCommand { - /** Creates new NukeCommand */ public OrbitalBombardmentCommand(Server server, TWGameManager gameManager) { - super(server, "ob", "GM Drops a bomb onto the board doing of 100 damage with a 3 hex radius, to be exploded at" + - "the end of the next weapons attack phase." + - "Allowed formats:"+ - "/ob and" + - "/ob [factor=#] [radius=#]" + - "the damage at impact point is the factor times 10, default factor value is 10. " + - "and hex x, y is x=column number and y=row number (hex 0923 would be x=9 and y=23), the explosion blast radius default " + - "is equal to 4, it automatically applies a linear damage dropoff each hex away from the center." + - " All parameters in square brackets may be omitted. " + - " Example: /ob 10 10 factor=12 "); - this.gameManager = gameManager; + super(server, gameManager, "ob", "GM orders an unknown warship to strike the board doing of 100 damage with a 4 hex radius, to be exploded at" + + "the end of the next weapons attack phase." + + "Allowed formats: " + + "/ob and" + + "/ob [dmg=#] [r=#] and" + + "/ob and " + + "/ob " + + "X and Y are the hex position where is x=column number and y=row number (hex 0923 would be x=9 and y=23), " + + "dmg is the amount of damage at impact point, default is 100, the damage drops off linearly according to the radius. " + + "r is radius, defaults to 4. " + + " Example: /ob 10 10 dmg=120 r=4 and /ob 10 10 120 4 are equivalent. " + + " Parameters in square brackets may be omitted, and using named variables permits to write them out of order. " + + " Example: /ob 10 10 r=12 dmg=300"); + } + + @Override + public List> defineArguments() { + return List.of( + new IntegerArgument("x"), + new IntegerArgument("y"), + new IntegerArgument("dmg", 10, Integer.MAX_VALUE, 100), + new IntegerArgument("r", 1, 10, 4)); } /** * Run this command with the arguments supplied */ @Override - public void run(int connId, String[] args) { + protected void runAsGM(int connId, Map> args) { - // Check to make sure nuking is allowed by game options! - if (!isGM(connId)) { - server.sendServerChat(connId, "You are not a Game Master."); - return; - } + var orbitalBombardmentBuilder = new OrbitalBombardment.Builder(); - if (args.length >= 3) { - var orbitalBombardmentBuilder = new OrbitalBombardment.Builder(); - try { - int[] position = new int[2]; - for (int i = 1; i < 3; i++) { - position[i-1] = Integer.parseInt(args[i]) - 1; - } - // is the hex on the board? - if (!gameManager.getGame().getBoard().contains(position[0], position[1])) { - server.sendServerChat(connId, "Specified hex is not on the board."); - return; - } + orbitalBombardmentBuilder.x((int) args.get("x").getValue() - 1).y((int) args.get("y").getValue() - 1); + orbitalBombardmentBuilder.radius((int) args.get("r").getValue()); + orbitalBombardmentBuilder.damageFactor((int) args.get("dmg").getValue()); + var orbitalBombardment = orbitalBombardmentBuilder.build(); - orbitalBombardmentBuilder - .x(position[0]) - .y(position[1]); - - if (args.length > 3) { - for (int i = 3; i < args.length; i++) { - String[] keyValue = args[i].split("="); - if (keyValue[0].equals("factor")) { - orbitalBombardmentBuilder.damageFactor(Integer.parseInt(keyValue[1])); - } else if (keyValue[0].equals("radius")) { - orbitalBombardmentBuilder.radius(Integer.parseInt(keyValue[1])); - } - } - } - - gameManager.addScheduledOrbitalBombardment(orbitalBombardmentBuilder.build()); - server.sendServerChat(connId, "This isn't a shooting star! Take cover!"); - } catch (Exception e) { - server.sendServerChat(connId, "Orbital bombardment command failed (2). Proper format is \"/ob [factor=10] [radius=4]\" where hex x, y is x=column number and y=row number (hex 0923 would be x=9 and y=23)"); - } - } else { - // Error out; it's not a valid call. - server.sendServerChat(connId, "Orbital bombardment command failed (1). Proper format is \"/ob [factor=10] [radius=4]\" where hex x, y is x=column number and y=row number (hex 0923 would be x=9 and y=23)"); + // is the hex on the board? + if (!gameManager.getGame().getBoard().contains(orbitalBombardment.getX(), orbitalBombardment.getY())) { + server.sendServerChat(connId, "Specified hex is not on the board."); + return; } - } - @Override - public TWGameManager getGameManager() { - return gameManager; + gameManager.addScheduledOrbitalBombardment(orbitalBombardment); + server.sendServerChat("Orbital bombardment incoming!"); } + } diff --git a/megamek/src/megamek/server/commands/RemoveSmokeCommand.java b/megamek/src/megamek/server/commands/RemoveSmokeCommand.java index 5715ae19736..75373384714 100644 --- a/megamek/src/megamek/server/commands/RemoveSmokeCommand.java +++ b/megamek/src/megamek/server/commands/RemoveSmokeCommand.java @@ -22,44 +22,30 @@ import megamek.common.planetaryconditions.Fog; import megamek.common.planetaryconditions.Wind; import megamek.server.Server; +import megamek.server.commands.arguments.Argument; import megamek.server.totalwarfare.TWGameManager; +import java.util.List; +import java.util.Map; + /** * @author Luana Scoppio */ -public class RemoveSmokeCommand extends ServerCommand implements IsGM { - - private final TWGameManager gameManager; +public class RemoveSmokeCommand extends GamemasterServerCommand { /** Creates new KillCommand */ public RemoveSmokeCommand(Server server, TWGameManager gameManager) { - super(server, "nosmoke", "GM removes all smoke cloud hexes. Usage: /nosmoke"); - this.gameManager = gameManager; + super(server, gameManager, "nosmoke", "GM removes all smoke cloud hexes. Usage: /nosmoke"); } - /** - * Run this command with the arguments supplied - */ @Override - public void run(int connId, String[] args) { - if (!isGM(connId)) { - server.sendServerChat(connId, "You are not a Game Master."); - return; - } - - // Check argument integrity. - if (args.length == 1) { - // Check command - gameManager.getSmokeCloudList().forEach(gameManager::removeSmokeTerrain); - server.sendServerChat(connId, "GM cleared the smoke clouds."); - } else { - // Error out; it's not a valid call. - server.sendServerChat(connId, "nosmoke command failed (1)."); - } + public List> defineArguments() { + return List.of(); } @Override - public TWGameManager getGameManager() { - return gameManager; + protected void runAsGM(int connId, Map> args) { + gameManager.getSmokeCloudList().forEach(gameManager::removeSmokeTerrain); + server.sendServerChat(connId, "GM cleared the smoke clouds."); } } diff --git a/megamek/src/megamek/server/commands/arguments/Argument.java b/megamek/src/megamek/server/commands/arguments/Argument.java new file mode 100644 index 00000000000..39d4aa056c3 --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/Argument.java @@ -0,0 +1,22 @@ +package megamek.server.commands.arguments; + +// A generic Argument class that can be extended for different argument types +public abstract class Argument { + protected T value; + + private final String name; + + public Argument(String name) { + this.name = name; + } + + public T getValue() { + return value; + } + + public String getName() { + return name; + } + + public abstract void parse(String input) throws IllegalArgumentException; +} diff --git a/megamek/src/megamek/server/commands/arguments/EnumArgument.java b/megamek/src/megamek/server/commands/arguments/EnumArgument.java new file mode 100644 index 00000000000..5c51072579b --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/EnumArgument.java @@ -0,0 +1,44 @@ +package megamek.server.commands.arguments; + +import java.util.Arrays; + +public class EnumArgument> extends Argument { + private final Class enumType; + private final E defaultValue; + + public EnumArgument(String name, Class enumType, E defaultValue) { + super(name); + this.enumType = enumType; + this.defaultValue = defaultValue; + } + + public Class getEnumType() { + return enumType; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null && defaultValue != null) { + value = defaultValue; + return; + } else { + if (input == null) { + throw new IllegalArgumentException(getName() + " is required."); + } + } + try { + value = Enum.valueOf(enumType, input.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(getName() + " must be one of: " + + String.join(", ", Arrays.toString(enumType.getEnumConstants()))); + } + } + + @Override + public E getValue() { + if (value == null && defaultValue != null) { + return defaultValue; + } + return value; + } +} diff --git a/megamek/src/megamek/server/commands/arguments/IntegerArgument.java b/megamek/src/megamek/server/commands/arguments/IntegerArgument.java new file mode 100644 index 00000000000..26340b84879 --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/IntegerArgument.java @@ -0,0 +1,63 @@ +package megamek.server.commands.arguments; + +public class IntegerArgument extends Argument { + private final int minValue; + private final int maxValue; + private final Integer defaultValue; + + public IntegerArgument(String name) { + this(name, Integer.MIN_VALUE, Integer.MAX_VALUE, null); + } + + public IntegerArgument(String name, int minValue, int maxValue) { + this(name, minValue, maxValue, null); + } + + public IntegerArgument(String name, int minValue, int maxValue, Integer defaultValue) { + super(name); + this.minValue = minValue; + this.maxValue = maxValue; + this.defaultValue = defaultValue; + } + + @Override + public Integer getValue() { + if (value == null && defaultValue != null) { + return defaultValue; + } + return value; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null && defaultValue != null) { + value = defaultValue; + return; + } else { + if (input == null) { + throw new IllegalArgumentException(getName() + " is required."); + } + } + try { + int parsedValue = Integer.parseInt(input); + if (parsedValue < minValue || parsedValue > maxValue) { + throw new IllegalArgumentException(getName() + " must be between " + minValue + " and " + maxValue); + } + value = parsedValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException(getName() + " must be an integer."); + } + } + + public boolean hasDefaultValue() { + return defaultValue != null; + } + + public int getMinValue() { + return minValue; + } + + public int getMaxValue() { + return maxValue; + } +} diff --git a/megamek/src/megamek/server/props/OrbitalBombardment.java b/megamek/src/megamek/server/props/OrbitalBombardment.java index e49e1c965e4..0bfa383c45f 100644 --- a/megamek/src/megamek/server/props/OrbitalBombardment.java +++ b/megamek/src/megamek/server/props/OrbitalBombardment.java @@ -18,6 +18,8 @@ */ package megamek.server.props; +import megamek.common.Coords; + /** * Represents an orbital bombardment event. * x and y are board positions, damageFactor is the damage at impact point times 10, and radius is the blast radius of the explosion with @@ -31,19 +33,23 @@ public class OrbitalBombardment { private final int y; private final int damageFactor; private final int radius; - + private final Coords coords; /** * Represents an orbital bombardment event. * x and y are board positions, damageFactor is the damage at impact point times 10, and radius is the blast radius of the explosion with * regular/linear damage droppoff. * - * @param builder */ private OrbitalBombardment(Builder builder) { this.x = builder.x; this.y = builder.y; this.damageFactor = builder.damageFactor; this.radius = builder.radius; + this.coords = new Coords(x, y); + } + + public Coords getCoords() { + return coords; } public int getX() { diff --git a/megamek/src/megamek/server/totalwarfare/TWGameManager.java b/megamek/src/megamek/server/totalwarfare/TWGameManager.java index 4eb440a79a0..57c86de9f08 100644 --- a/megamek/src/megamek/server/totalwarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalwarfare/TWGameManager.java @@ -190,8 +190,11 @@ public List getCommandList(Server server) { commands.add(new CheckBVTeamCommand(server)); commands.add(new NukeCommand(server, this)); commands.add(new KillCommand(server, this)); + commands.add(new OrbitalBombardmentCommand(server, this)); commands.add(new ChangeOwnershipCommand(server, this)); commands.add(new DisasterCommand(server, this)); + commands.add(new FirestarterCommand(server, this)); + commands.add(new FirestormCommand(server, this)); commands.add(new RemoveSmokeCommand(server, this)); commands.add(new ChangeWeatherCommand(server, this)); commands.add(new TraitorCommand(server, this)); @@ -20194,8 +20197,6 @@ public void doExplosion(int[] damages, boolean autoDestroyInSameHex, Coords posi } else { range += entity.getElevation(); } - } else { - continue; } // We might need to nuke everyone in the explosion hex. If so... @@ -20394,7 +20395,35 @@ public void addScheduledNuke(int[] nuke) { * through it's builder. */ public void addScheduledOrbitalBombardment(OrbitalBombardment orbitalBombardment) { + Report r = new Report(1302); + r.indent(); + r.newlines = 0; + r.add("Unknown warship in orbit"); + r.add(orbitalBombardment.getCoords().getBoardNum()); + getvPhaseReport().addElement(r); + Report.addNewline(getvPhaseReport()); + + drawOrbitalBombardmentOnBoard(orbitalBombardment); scheduledOrbitalBombardment.add(orbitalBombardment); + getGame().setOrbitalBombardmentVector(new Vector<>(scheduledOrbitalBombardment)); + } + + private void drawOrbitalBombardmentOnBoard(OrbitalBombardment orbitalBombardment) { + + for (var coord : orbitalBombardment.getCoords().allAtDistanceOrLess(orbitalBombardment.getRadius())) { + getGame().getBoard().addSpecialHexDisplay( + coord, + new SpecialHexDisplay( + SpecialHexDisplay.Type.ORBITAL_BOMBARDMENT, + getGame().getRoundCount(), + getGame().getPlayersList().get(0), // It doesnt matter which is the player, but I dont want to cause a nullpointer. + "Orbital bombardment incoming, landing on round " + + getGame().getRoundCount() + + ", fired by an unknown warship in orbit", + SpecialHexDisplay.SHD_OBSCURED_ALL) + ); + sendChangedHex(coord); + } } /** @@ -20419,8 +20448,15 @@ void resolveScheduledNukes() { */ void resolveScheduledOrbitalBombardments() { scheduledOrbitalBombardment - .forEach(ob -> doOrbitalBombardment(new Coords(ob.getX(), ob.getY()), ob.getDamageFactor(), ob.getRadius(), vPhaseReport)); + .forEach(ob -> doOrbitalBombardment(new Coords(ob.getX(), ob.getY()), ob.getDamageFactor(), ob.getRadius())); scheduledOrbitalBombardment.clear(); + getGame().resetOrbitalBombardmentAttacks(); + + // All right. We're done. + var r = new Report(1301, Report.PUBLIC); + r.indent(); + r.newlines = 2; + getvPhaseReport().add(r); } /** @@ -20446,19 +20482,12 @@ public void doNuclearExplosion(Coords position, int nukeType, Vector vDe * @param position the position that will be hit by the orbital bombardment * @param damageFactor the factor by which the base damage will be multiplied * @param radius the radius which the damage will hit - * @param vDesc a vector that contains the output report */ - public void doOrbitalBombardment(Coords position, int damageFactor, int radius, Vector vDesc) { - // Just in case. - if (vDesc == null) { - vDesc = new Vector<>(); - } - + public void doOrbitalBombardment(Coords position, int damageFactor, int radius) { Report r = new Report(1300, Report.PUBLIC); - r.indent(); r.add(position.getBoardNum(), true); - vDesc.add(r); + getvPhaseReport().add(r); // Then, do actual blast damage. // Use the standard blast function for this. @@ -20470,41 +20499,7 @@ public void doOrbitalBombardment(Coords position, int damageFactor, int radius, doExplosion(baseDamage, degradation , false, position, true, tmpV, blastedUnitsVec, -1, true); Report.indentAll(tmpV, 2); - vDesc.addAll(tmpV); - - // Next, for whatever's left, do terrain effects - // such as clearing, roughing, and boiling off water. - boolean damageFlag = true; - - // Lastly, do secondary effects. - for (Entity entity : game.getEntitiesVector()) { - // loaded units and off board units don't have a position, - // so we don't count 'em here - if ((entity.getTransportId() != Entity.NONE) || (entity.getPosition() == null)) { - continue; - } - - // If it's already destroyed... - if ((entity.isDoomed()) || (entity.isDestroyed())) { - continue; - } - - // If it's too far away for this... - if (position.distance(entity.getPosition()) > radius) { - continue; - } - - // Actually do secondary effects against it. - // Since the effects are unit-dependant, we'll just define it in the - // entity. -// applySecondaryNuclearEffects(entity, position, vDesc); - } - - // All right. We're done. - r = new Report(1216, Report.PUBLIC); - r.indent(); - r.newlines = 2; - vDesc.add(r); + getvPhaseReport().addAll(tmpV); } /** @@ -31692,6 +31687,10 @@ void clearBombIcons() { game.getBoard().clearBombIcons(); } + void clearOrbitalBombardmentIcons() { + game.getBoard().clearOrbitalBombardmentIcons(); + } + /** * Convenience function to send a ground object update. */