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();