diff --git a/megamek/i18n/megamek/common/options/messages.properties b/megamek/i18n/megamek/common/options/messages.properties
index 17f1fe80a6e..b99011e2e51 100644
--- a/megamek/i18n/megamek/common/options/messages.properties
+++ b/megamek/i18n/megamek/common/options/messages.properties
@@ -140,6 +140,8 @@ GameOptionsInfo.option.allow_nukes.displayableName=Allow command-line nukes.
GameOptionsInfo.option.allow_nukes.description=This must be checked to allow players to throw nukes from the command line. If this is not checked, nukes are still available as advanced munitions.
GameOptionsInfo.option.really_allow_nukes.displayableName=REALLY allow command-line nukes.
GameOptionsInfo.option.really_allow_nukes.description=This must be checked to allow players to throw nukes from the command line. If this is not checked, nukes are still available as advanced munitions.
+GameOptionsInfo.option.gm_can_kill_units.displayableName=Allow command-line kill (GM only).
+GameOptionsInfo.option.gm_can_kill_units.description=This must be checked to allow the GM to kill units using command line. If this is not checked, the command is disabled.
GameOptionsInfo.group.advancedRules.displayableName=Advanced Rules
diff --git a/megamek/i18n/megamek/common/report-messages.properties b/megamek/i18n/megamek/common/report-messages.properties
index c7431827b9c..20be24c6ee5 100755
--- a/megamek/i18n/megamek/common/report-messages.properties
+++ b/megamek/i18n/megamek/common/report-messages.properties
@@ -55,6 +55,10 @@
1241=Blue Shield Field Dampener fails!
1242=no effect
+#1300s - Orbital Bombardment related
+1300=An orbital bombardment hit hex !!!
+1301=End of orbital bombardment resolution
+
# 1500s - ammo handling related
1500= () selected
1501= due to out of ammo!
diff --git a/megamek/src/megamek/common/Game.java b/megamek/src/megamek/common/Game.java
index 08f4a43ed56..cd106d57c9d 100644
--- a/megamek/src/megamek/common/Game.java
+++ b/megamek/src/megamek/common/Game.java
@@ -760,6 +760,7 @@ public void setLastPhase(GamePhase lastPhase) {
/**
* @return an enumeration of all the entities in the game.
+ * @deprecated Use {@link #inGameTWEntities()} instead.
*/
@Deprecated
public Iterator getEntities() {
diff --git a/megamek/src/megamek/common/options/GameOptions.java b/megamek/src/megamek/common/options/GameOptions.java
index 56875bb6c21..c780f42b731 100755
--- a/megamek/src/megamek/common/options/GameOptions.java
+++ b/megamek/src/megamek/common/options/GameOptions.java
@@ -93,6 +93,7 @@ public synchronized void initialize() {
addOption(allowed, OptionsConstants.ALLOWED_NO_CLAN_PHYSICAL, false);
addOption(allowed, OptionsConstants.ALLOWED_ALLOW_NUKES, false);
addOption(allowed, OptionsConstants.ALLOWED_REALLY_ALLOW_NUKES, false);
+ addOption(allowed, OptionsConstants.ALLOWED_GM_CAN_KILL_UNITS, false);
IBasicOptionGroup advancedRules = addGroup("advancedRules");
addOption(advancedRules, OptionsConstants.ADVANCED_MINEFIELDS, false);
diff --git a/megamek/src/megamek/common/options/OptionsConstants.java b/megamek/src/megamek/common/options/OptionsConstants.java
index 6850acaf3fb..5af5898077b 100644
--- a/megamek/src/megamek/common/options/OptionsConstants.java
+++ b/megamek/src/megamek/common/options/OptionsConstants.java
@@ -330,6 +330,7 @@ public class OptionsConstants {
public static final String ALLOWED_NO_CLAN_PHYSICAL = "no_clan_physical";
public static final String ALLOWED_ALLOW_NUKES = "allow_nukes";
public static final String ALLOWED_REALLY_ALLOW_NUKES = "really_allow_nukes";
+ public static final String ALLOWED_GM_CAN_KILL_UNITS = "gm_can_kill_units";
public static final String ADVANCED_MINEFIELDS = "minefields";
public static final String ADVANCED_HIDDEN_UNITS = "hidden_units";
public static final String ADVANCED_BLACK_ICE= "black_ice";
diff --git a/megamek/src/megamek/server/commands/AllowGameMasterCommand.java b/megamek/src/megamek/server/commands/AllowGameMasterCommand.java
index 839ea433b00..b8189ae330c 100644
--- a/megamek/src/megamek/server/commands/AllowGameMasterCommand.java
+++ b/megamek/src/megamek/server/commands/AllowGameMasterCommand.java
@@ -34,7 +34,7 @@ public class AllowGameMasterCommand extends ServerCommand {
public AllowGameMasterCommand(Server server, TWGameManager gameManager) {
super(server, "allowGM", "Allows a player become Game Master "
- + "Usage: /allowGameMaster used in respond to another " +
+ + "Usage: /allowGM used in respond to another " +
"Player's request to become Game Master. All players assigned to" +
" a team must allow the change.");
this.gameManager = gameManager;
diff --git a/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java b/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java
new file mode 100644
index 00000000000..d50836c22e2
--- /dev/null
+++ b/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java
@@ -0,0 +1,79 @@
+/*
+ * 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.Entity;
+import megamek.common.Player;
+import megamek.server.Server;
+import megamek.server.totalwarfare.TWGameManager;
+
+/**
+ * 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 ChangeOwnershipCommand(Server server, TWGameManager gameManager) {
+ super(server,
+ "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;
+ }
+
+ /**
+ * 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) {
+ }
+ }
+
+ @Override
+ public TWGameManager getGameManager() {
+ return gameManager;
+ }
+}
diff --git a/megamek/src/megamek/server/commands/ChangeWeatherCommand.java b/megamek/src/megamek/server/commands/ChangeWeatherCommand.java
new file mode 100644
index 00000000000..1e75fd97e2d
--- /dev/null
+++ b/megamek/src/megamek/server/commands/ChangeWeatherCommand.java
@@ -0,0 +1,114 @@
+/*
+ * 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.planetaryconditions.*;
+import megamek.server.Server;
+import megamek.server.totalwarfare.TWGameManager;
+
+
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * @author Luana Scoppio
+ */
+public class ChangeWeatherCommand extends ServerCommand {
+
+ private final TWGameManager gameManager;
+
+ 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. " +
+ "Usage format: /weather [fog=0-2] [wind=0-6] [winddir=0-6] [light=0-6] [atmo=0-5] [blowsand=0-1] [weather=0-14] " +
+ "light= 0: daylight, 1: dusk, 2: full moon, 3: glare, 4: moonless night, 5: solar flare, 6: pitch black " +
+ "fog= 0: none, 1: light, 2: heavy " +
+ "wind= 0: calm, 1: light gale, 2: moderate gale, 3: strong gale, 4: storm, 5: tornado F1-F3, 6: tornado F4 " +
+ "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 " +
+ "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;
+ }
+
+ private void updatePlanetaryCondition(String arg, String prefix, 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));
+ } else {
+ server.sendServerChat(connId, errorMessage.apply(maxLength));
+ }
+ }
+
+ 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();
+
+ 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))
+ );
+
+ 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);
+ }
+ }
+}
diff --git a/megamek/src/megamek/server/commands/DisasterCommand.java b/megamek/src/megamek/server/commands/DisasterCommand.java
new file mode 100644
index 00000000000..0a7137cd88b
--- /dev/null
+++ b/megamek/src/megamek/server/commands/DisasterCommand.java
@@ -0,0 +1,62 @@
+/*
+ * 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.server.Server;
+import megamek.server.totalwarfare.TWGameManager;
+
+/**
+ * @author Luana Scoppio
+ */
+public class DisasterCommand extends ServerCommand {
+
+ private final TWGameManager gameManager;
+
+ /** 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] " +
+ "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;
+ }
+
+ /**
+ * 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...");
+ } else {
+ server.sendServerChat(connId, "disaster command failed (1).");
+ }
+ }
+}
diff --git a/megamek/src/megamek/server/commands/IsGM.java b/megamek/src/megamek/server/commands/IsGM.java
new file mode 100644
index 00000000000..6c8fcff5545
--- /dev/null
+++ b/megamek/src/megamek/server/commands/IsGM.java
@@ -0,0 +1,13 @@
+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
new file mode 100644
index 00000000000..bc436c49380
--- /dev/null
+++ b/megamek/src/megamek/server/commands/KillCommand.java
@@ -0,0 +1,82 @@
+/*
+ * 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.options.OptionsConstants;
+import megamek.server.Server;
+import megamek.server.totalwarfare.TWGameManager;
+
+/**
+ * @author Luana Scoppio
+ */
+public class KillCommand extends ServerCommand implements IsGM {
+
+ 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" +
+ "Usage: "+
+ "/kill " +
+ "where id is the units ID. The units ID can be found by hovering over the unit.");
+ this.gameManager = gameManager;
+ }
+
+ /**
+ * Run this command with the arguments supplied
+ */
+ @Override
+ public void run(int connId, String[] args) {
+
+ // Check to make sure gm kills are allowed!
+ if (!(server.getGame().getOptions().booleanOption(OptionsConstants.ALLOWED_GM_CAN_KILL_UNITS))) {
+ server.sendServerChat(connId, "Command-line kill is not enabled in this game.");
+ return;
+ }
+ if (!isGM(connId)) {
+ server.sendServerChat(connId, "You are not a Game Master.");
+ 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;
+ }
+}
diff --git a/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java b/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java
new file mode 100644
index 00000000000..c3d31e2202f
--- /dev/null
+++ b/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.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.options.OptionsConstants;
+import megamek.server.Server;
+import megamek.server.props.OrbitalBombardment;
+import megamek.server.totalwarfare.TWGameManager;
+
+/**
+ * @author Luana Scoppio
+ */
+public class OrbitalBombardmentCommand extends ServerCommand implements IsGM {
+
+ private final TWGameManager gameManager;
+
+ /** 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:"+
+ "/bomb and" +
+ "/bomb [factor=10] [radius=4]" +
+ "the damage at impact point is 10 times the factor, default 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 ommited. " +
+ " Example: /ob 10 10 factor=12 ");
+ this.gameManager = gameManager;
+ }
+
+ /**
+ * Run this command with the arguments supplied
+ */
+ @Override
+ public void run(int connId, String[] args) {
+
+ // Check to make sure nuking is allowed by game options!
+ if (!isGM(connId)) {
+ server.sendServerChat(connId, "You are not a Game Master.");
+ return;
+ }
+
+ 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(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)");
+ }
+ }
+
+ @Override
+ public TWGameManager getGameManager() {
+ return gameManager;
+ }
+}
diff --git a/megamek/src/megamek/server/commands/RemoveSmokeCommand.java b/megamek/src/megamek/server/commands/RemoveSmokeCommand.java
new file mode 100644
index 00000000000..5715ae19736
--- /dev/null
+++ b/megamek/src/megamek/server/commands/RemoveSmokeCommand.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 Luana Scoppio (luana.coppio@gmail.com)
+ * 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.planetaryconditions.Fog;
+import megamek.common.planetaryconditions.Wind;
+import megamek.server.Server;
+import megamek.server.totalwarfare.TWGameManager;
+
+/**
+ * @author Luana Scoppio
+ */
+public class RemoveSmokeCommand extends ServerCommand implements IsGM {
+
+ private final TWGameManager gameManager;
+
+ /** Creates new KillCommand */
+ public RemoveSmokeCommand(Server server, TWGameManager gameManager) {
+ super(server, "nosmoke", "GM removes all smoke cloud hexes. Usage: /nosmoke");
+ this.gameManager = gameManager;
+ }
+
+ /**
+ * 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).");
+ }
+ }
+
+ @Override
+ public TWGameManager getGameManager() {
+ return gameManager;
+ }
+}
diff --git a/megamek/src/megamek/server/props/OrbitalBombardment.java b/megamek/src/megamek/server/props/OrbitalBombardment.java
new file mode 100644
index 00000000000..e49e1c965e4
--- /dev/null
+++ b/megamek/src/megamek/server/props/OrbitalBombardment.java
@@ -0,0 +1,106 @@
+/*
+ * 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.props;
+
+/**
+ * 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.
+ *
+ * @author Luana Scoppio
+ */
+public class OrbitalBombardment {
+
+ private final int x;
+ private final int y;
+ private final int damageFactor;
+ private final int radius;
+
+ /**
+ * 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;
+ }
+
+ public int getX() {
+ return x;
+ }
+
+ public int getY() {
+ return y;
+ }
+
+ public int getDamageFactor() {
+ return damageFactor;
+ }
+
+ public int getRadius() {
+ return radius;
+ }
+
+ /**
+ * Builder of 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.
+ *
+ */
+ public static class Builder {
+ private int x;
+ private int y;
+ private int damageFactor = 10;
+ private int radius = 4;
+
+ public Builder x(int x) {
+ this.x = x;
+ return this;
+ }
+
+ public Builder y(int y) {
+ this.y = y;
+ return this;
+ }
+
+ public Builder damageFactor(int damageFactor) {
+ this.damageFactor = damageFactor;
+ return this;
+ }
+
+ public Builder radius(int radius) {
+ this.radius = radius;
+ return this;
+ }
+
+ /**
+ * Builds an orbital bombardment.
+ *
+ * @return an immutable instance of an orbital bombardment.
+ */
+ public OrbitalBombardment build() {
+ return new OrbitalBombardment(this);
+ }
+ }
+}
diff --git a/megamek/src/megamek/server/totalwarfare/TWGameManager.java b/megamek/src/megamek/server/totalwarfare/TWGameManager.java
index 0b452fa59b6..4eb440a79a0 100644
--- a/megamek/src/megamek/server/totalwarfare/TWGameManager.java
+++ b/megamek/src/megamek/server/totalwarfare/TWGameManager.java
@@ -73,6 +73,7 @@
import megamek.logging.MMLogger;
import megamek.server.*;
import megamek.server.commands.*;
+import megamek.server.props.OrbitalBombardment;
import megamek.server.victory.VictoryResult;
/**
@@ -101,6 +102,7 @@ public Vector getvPhaseReport() {
private final List terrainProcessors = new ArrayList<>();
private ArrayList scheduledNukes = new ArrayList<>();
+ private ArrayList scheduledOrbitalBombardment = new ArrayList<>();
/**
* Stores a set of Coords
that have changed during this phase.
@@ -187,6 +189,11 @@ public List getCommandList(Server server) {
commands.add(new CheckBVCommand(server));
commands.add(new CheckBVTeamCommand(server));
commands.add(new NukeCommand(server, this));
+ commands.add(new KillCommand(server, this));
+ commands.add(new ChangeOwnershipCommand(server, this));
+ commands.add(new DisasterCommand(server, this));
+ commands.add(new RemoveSmokeCommand(server, this));
+ commands.add(new ChangeWeatherCommand(server, this));
commands.add(new TraitorCommand(server, this));
commands.add(new ListEntitiesCommand(server, this));
commands.add(new AssignNovaNetServerCommand(server, this));
@@ -19731,7 +19738,7 @@ public Vector damageEntity(Entity te, HitData hit, int damage,
int[] damages = { (int) Math.floor(damage_orig / 10.0),
(int) Math.floor(damage_orig / 20.0) };
doExplosion(damages, false, te.getPosition(), true, vDesc, null, 5,
- te.getId(), false);
+ te.getId(), false, false);
Report.addNewline(vDesc);
r = new Report(5410, Report.PUBLIC);
r.subject = te.getId();
@@ -20013,7 +20020,7 @@ public void doFusionEngineExplosion(int engineRating, Coords position, Vector vUnits) {
int[] myDamages = { engineRating, (engineRating / 10), (engineRating / 20),
(engineRating / 40) };
- doExplosion(myDamages, true, position, false, vDesc, vUnits, 5, -1, true);
+ doExplosion(myDamages, true, position, false, vDesc, vUnits, 5, -1, true, false);
}
/**
@@ -20021,7 +20028,7 @@ public void doFusionEngineExplosion(int engineRating, Coords position, Vector vDesc,
- Vector vUnits, int excludedUnitId) {
+ Vector vUnits, int excludedUnitId, boolean canDamageVtol) {
if (degradation < 1) {
return;
}
@@ -20037,15 +20044,16 @@ public void doExplosion(int damage, int degradation, boolean autoDestroyInSameHe
myDamages[x] = myDamages[x - 1] - degradation;
}
doExplosion(myDamages, autoDestroyInSameHex, position, allowShelter, vDesc, vUnits,
- 5, excludedUnitId, false);
+ 5, excludedUnitId, false, canDamageVtol);
}
/**
* General function to cause explosions in areas.
+ * TODO Luana: Refactor this function so it is less of a mess
*/
public void doExplosion(int[] damages, boolean autoDestroyInSameHex, Coords position,
boolean allowShelter, Vector vDesc, Vector vUnits,
- int clusterAmt, int excludedUnitId, boolean engineExplosion) {
+ int clusterAmt, int excludedUnitId, boolean engineExplosion, boolean canDamageVtol) {
if (vDesc == null) {
vDesc = new Vector<>();
}
@@ -20132,9 +20140,7 @@ public void doExplosion(int[] damages, boolean autoDestroyInSameHex, Coords posi
// Now we damage people near the explosion.
List loaded = new ArrayList<>();
- for (Iterator ents = game.getEntities(); ents.hasNext();) {
- Entity entity = ents.next();
-
+ for (var entity : game.inGameTWEntities()) {
if (entitiesHit.contains(entity)) {
continue;
}
@@ -20152,12 +20158,6 @@ public void doExplosion(int[] damages, boolean autoDestroyInSameHex, Coords posi
continue;
}
- // We are going to assume that explosions are on the ground here so
- // flying entities should be unaffected
- if (entity.isAirborne()) {
- continue;
- }
-
if ((entity instanceof MekWarrior) && !((MekWarrior) entity).hasLanded()) {
// MekWarrior is still up in the air ejecting hence safe
// from this explosion.
@@ -20173,6 +20173,7 @@ public void doExplosion(int[] damages, boolean autoDestroyInSameHex, Coords posi
}
continue;
}
+
int range = position.distance(entityPos);
if (range >= damages.length) {
@@ -20180,6 +20181,23 @@ public void doExplosion(int[] damages, boolean autoDestroyInSameHex, Coords posi
continue;
}
+ // We are going to assume that explosions are on the ground here so
+ // flying entities should be unaffected, except VTOL or WiGE
+ if (entity.isAirborne() && !canDamageVtol) {
+ continue;
+ } else if (entity.isAirborne() && canDamageVtol && entity.isAirborneVTOLorWIGE()) {
+ if (entity.getElevation() > damages.length) {
+ continue;
+ }
+ if ((range + entity.getElevation()) > damages.length) {
+ continue;
+ } else {
+ range += entity.getElevation();
+ }
+ } else {
+ continue;
+ }
+
// We might need to nuke everyone in the explosion hex. If so...
if ((range == 0) && autoDestroyInSameHex) {
// Add the reports
@@ -20262,23 +20280,54 @@ public void doExplosion(int[] damages, boolean autoDestroyInSameHex, Coords posi
r.addDesc(e);
r.add(damage);
vDesc.addElement(r);
-
- while (damage > 0) {
- int cluster = Math.min(5, damage);
- int table = ToHitData.HIT_NORMAL;
- if (e instanceof ProtoMek) {
- table = ToHitData.HIT_SPECIAL_PROTO;
- }
- HitData hit = e.rollHitLocation(table, ToHitData.SIDE_FRONT);
- vDesc.addAll(damageEntity(e, hit, cluster, false,
- DamageType.IGNORE_PASSENGER, false, true));
- damage -= cluster;
+ if (canDamageVtol) {
+ orbitalBombardmentDamage(position, vDesc, e, damage);
+ } else {
+ explosionDamage(position, vDesc, e, damage);
}
Report.addNewline(vDesc);
}
}
}
+ private void explosionDamage(Coords position, Vector vDesc, Entity e, int damage) {
+ while (damage > 0) {
+ int cluster = Math.min(5, damage);
+ int table = ToHitData.HIT_NORMAL;
+ if (e instanceof ProtoMek) {
+ table = ToHitData.HIT_SPECIAL_PROTO;
+ }
+ HitData hit = e.rollHitLocation(table, ToHitData.SIDE_FRONT);
+ vDesc.addAll(damageEntity(e, hit, cluster, false,
+ DamageType.IGNORE_PASSENGER, false, true));
+ damage -= cluster;
+ }
+ }
+
+ private void orbitalBombardmentDamage(Coords position, Vector vDesc, Entity e, int damage) {
+ while (damage > 0) {
+ int cluster = Math.min(5, damage);
+ int table = ToHitData.HIT_NORMAL;
+ int hitSide = ToHitData.SIDE_RANDOM;
+ if (e instanceof ProtoMek) {
+ table = ToHitData.HIT_SPECIAL_PROTO;
+ } else if (e instanceof Mek) {
+ table = ToHitData.HIT_ABOVE;
+ hitSide = e.sideTable(position);
+ } else if (e instanceof Tank) {
+ if (e.isAirborneVTOLorWIGE()) {
+ table = ToHitData.HIT_ABOVE;
+ }
+ hitSide = e.sideTable(position);
+ }
+
+ HitData hit = e.rollHitLocation(table, hitSide);
+ vDesc.addAll(damageEntity(e, hit, cluster, false,
+ DamageType.IGNORE_PASSENGER, false, true));
+ damage -= cluster;
+ }
+ }
+
/**
* Check if an Entity of the passed height can find shelter from a nuke blast
*
@@ -20338,6 +20387,16 @@ public void addScheduledNuke(int[] nuke) {
scheduledNukes.add(nuke);
}
+ /**
+ * add an orbital bombardment to hit the board in the next weapons attack phase
+ *
+ * @param orbitalBombardment this is an #OrbitalBombardment object, its immutable and must be constructed
+ * through it's builder.
+ */
+ public void addScheduledOrbitalBombardment(OrbitalBombardment orbitalBombardment) {
+ scheduledOrbitalBombardment.add(orbitalBombardment);
+ }
+
/**
* explode any scheduled nukes
*/
@@ -20355,6 +20414,15 @@ void resolveScheduledNukes() {
scheduledNukes.clear();
}
+ /**
+ * explode any scheduled orbital bombardments
+ */
+ void resolveScheduledOrbitalBombardments() {
+ scheduledOrbitalBombardment
+ .forEach(ob -> doOrbitalBombardment(new Coords(ob.getX(), ob.getY()), ob.getDamageFactor(), ob.getRadius(), vPhaseReport));
+ scheduledOrbitalBombardment.clear();
+ }
+
/**
* do a nuclear explosion
*
@@ -20373,6 +20441,72 @@ public void doNuclearExplosion(Coords position, int nukeType, Vector vDe
nukeStats.craterDepth, vDesc);
}
+ /**
+ * do an orbital bombardment
+ * @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<>();
+ }
+
+ Report r = new Report(1300, Report.PUBLIC);
+
+ r.indent();
+ r.add(position.getBoardNum(), true);
+ vDesc.add(r);
+
+ // Then, do actual blast damage.
+ // Use the standard blast function for this.
+ Vector tmpV = new Vector<>();
+ Vector blastedUnitsVec = new Vector<>();
+ int range = radius + 1;
+ int baseDamage = damageFactor * 10;
+ var degradation = baseDamage / range;
+ 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);
+ }
+
/**
* explode a nuke
*
@@ -20467,8 +20601,9 @@ public void doNuclearExplosion(Coords position, int baseDamage, int degradation,
// Use the standard blast function for this.
Vector tmpV = new Vector<>();
Vector blastedUnitsVec = new Vector<>();
+
doExplosion(baseDamage, degradation, true, position, true, tmpV,
- blastedUnitsVec, -1);
+ blastedUnitsVec, -1, false);
Report.indentAll(tmpV, 2);
vDesc.addAll(tmpV);
@@ -28369,7 +28504,7 @@ public Vector damageBuilding(Building bldg, int damage, String why, Coor
Vector vRep = new Vector<>();
doExplosion(((FuelTank) bldg).getMagnitude(), 10,
false, bldg.getCoords().nextElement(), true,
- vRep, null, -1);
+ vRep, null, -1, false);
Report.indentAll(vRep, 2);
vPhaseReport.addAll(vRep);
return vPhaseReport;
@@ -28566,7 +28701,7 @@ private Vector criticalGunEmplacement(Vector guns, Build
Vector vRep = new Vector<>();
doExplosion(((FuelTank) bldg).getMagnitude(), 10, false,
bldg.getCoords().nextElement(), true, vRep, null,
- -1);
+ -1, false);
Report.indentAll(vRep, 2);
vDesc.addAll(vRep);
return vPhaseReport;
diff --git a/megamek/src/megamek/server/totalwarfare/TWPhaseEndManager.java b/megamek/src/megamek/server/totalwarfare/TWPhaseEndManager.java
index fb402b691a5..4edf0209754 100644
--- a/megamek/src/megamek/server/totalwarfare/TWPhaseEndManager.java
+++ b/megamek/src/megamek/server/totalwarfare/TWPhaseEndManager.java
@@ -135,6 +135,7 @@ void managePhase() {
gameManager.assignAMS();
gameManager.handleAttacks();
gameManager.resolveScheduledNukes();
+ gameManager.resolveScheduledOrbitalBombardments();
gameManager.applyBuildingDamage();
gameManager.checkForPSRFromDamage();
gameManager.cleanupDestroyedNarcPods();