diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index d93addeb0b6..4be5778f45f 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -2466,6 +2466,7 @@ MovementDisplay.ConfirmUnJamRACDlg.title=Unjam? MovementDisplay.ConfirmPilotingRoll=You must make the following piloting\nskill check(s) for your movement:\n MovementDisplay.ConfirmSprint=Are you sure you want to sprint? MovementDisplay.ConfirmSuperchargerRoll=The movement you have selected will require a roll of {0} or higher\nto avoid Supercharger failure. Do you wish to proceed? +MovementDisplay.ConfirmLandingGearDamage=Landing in a rough or rubble hex will damage the landing ger.\n\nAre you sure you want to land here? MovementDisplay.DFADialog.message=To Hit: {0} ({1}%) ({2})\nDamage to Target: {3} (in 5pt clusters){4}\nDamage to Self: {5} (in 5pt clusters) (using Kick table) MovementDisplay.DFADialog.title=D.F.A. {0}? MovementDisplay.Done=Done diff --git a/megamek/i18n/megamek/common/report-messages.properties b/megamek/i18n/megamek/common/report-messages.properties index 3a347880ccd..1de020d1164 100755 --- a/megamek/i18n/megamek/common/report-messages.properties +++ b/megamek/i18n/megamek/common/report-messages.properties @@ -1182,6 +1182,8 @@ #crash related 9700= () crashes, suffering damage! 9701= () crashes off the map! +9702= () is destroyed by landing in water. +9703= () is immobilized by landing in water. 9705= () must roll a or lower to avoid damage, rolls a : 9706=hit by crash! 9707=avoids crash. diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index be92d394ccd..1d047c751d0 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -1697,6 +1697,22 @@ && ce().getGame().getBoard().getHex(cmd.getFinalCoords()) } } + if (cmd.contains(MoveStepType.LAND) || cmd.contains(MoveStepType.VLAND)) { + Set landingPath = ((IAero) ce()).getLandingCoords(cmd.contains(MoveStepType.VLAND), + cmd.getFinalCoords(), cmd.getFinalFacing()); + if (landingPath.stream().map(c -> game().getBoard().getHex(c)).filter(Objects::nonNull) + .anyMatch(h -> h.containsTerrain(Terrains.ROUGH) || h.containsTerrain(Terrains.RUBBLE))) { + ConfirmDialog nag = new ConfirmDialog(clientgui.frame, + Messages.getString("MovementDisplay.areYourSure"), + Messages.getString("MovementDisplay.ConfirmLandingGearDamage"), + false); + nag.setVisible(true); + if (!nag.getAnswer()) { + return; + } + } + } + if (isUsingChaff) { addStepToMovePath(MoveStepType.CHAFF); isUsingChaff = false; diff --git a/megamek/src/megamek/common/Dropship.java b/megamek/src/megamek/common/Dropship.java index 36b3b8bbb31..c04f709f116 100644 --- a/megamek/src/megamek/common/Dropship.java +++ b/megamek/src/megamek/common/Dropship.java @@ -474,6 +474,18 @@ public int height() { return 4; } + @Override + public int getWalkMP(MPCalculationSetting mpCalculationSetting) { + // A grounded dropship with the center hex in level 1 water is immobile. + if ((game != null) && !game.getBoard().inSpace() && !isAirborne()) { + Hex hex = game.getBoard().getHex(getPosition()); + if ((hex != null) && (hex.containsTerrain(Terrains.WATER, 1) && !hex.containsTerrain(Terrains.ICE))) { + return 0; + } + } + return super.getWalkMP(mpCalculationSetting); + } + /* * (non-Javadoc) * diff --git a/megamek/src/megamek/common/IAero.java b/megamek/src/megamek/common/IAero.java index 3b068d163da..13179ec2995 100644 --- a/megamek/src/megamek/common/IAero.java +++ b/megamek/src/megamek/common/IAero.java @@ -15,11 +15,7 @@ package megamek.common; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import java.util.Vector; +import java.util.*; import megamek.common.MovePath.MoveStepType; import org.apache.logging.log4j.LogManager; @@ -477,20 +473,50 @@ default PilotingRollData checkLanding(EntityMovementType moveType, int velocity, roll.addModifier(+4, "no thrust"); } } - // terrain mods - boolean lightWoods = false; - boolean rough = false; - boolean heavyWoods = false; + + // Per TW p. 87, the modifier is added once for each terrain type + Set> terrains = new HashSet<>(); + Set landingPositions = getLandingCoords(isVertical, landingPos, face); + // Any hex without terrain is clear, which is a +2 modifier. boolean clear = false; - boolean paved = true; + for (Coords pos : landingPositions) { + Hex hex = ((Entity) this).getGame().getBoard().getHex(pos); + if ((hex == null) || hex.hasPavement()) { + continue; + } + if (hex.isClearHex()) { + clear = true; + } else { + for (int terrain : hex.getTerrainTypes()) { + if ((terrain == Terrains.WATER) && hex.containsTerrain(Terrains.ICE)) { + continue; + } + if (Terrains.landingModifier(terrain, hex.terrainLevel(terrain)) > 0) { + terrains.add(List.of(terrain, hex.terrainLevel(terrain))); + } + } + } + } + if (clear) { + roll.addModifier(isVertical ? 1 : 2, "Clear terrain in landing path"); + } + for (List terrain : terrains) { + int mod = Terrains.landingModifier(terrain.get(0), terrain.get(1)); + if (isVertical) { + mod = mod / 2 + mod % 2; + } + roll.addModifier(mod, Terrains.getDisplayName(terrain.get(0), terrain.get(1)) + " in landing path"); + } - Set landingPositions = new HashSet<>(); - boolean isDropship = (this instanceof Dropship); - // Vertical landing just checks the landing hex + return roll; + } + + default Set getLandingCoords(boolean isVertical, Coords landingPos, int facing) { + Set landingPositions = new HashSet(); if (isVertical) { landingPositions.add(landingPos); // Dropships must also check the adjacent 6 hexes - if (isDropship) { + if (this instanceof Dropship) { for (int i = 0; i < 6; i++) { landingPositions.add(landingPos.translated(i)); } @@ -498,49 +524,16 @@ default PilotingRollData checkLanding(EntityMovementType moveType, int velocity, // Horizontal landing requires checking whole landing strip } else { for (int i = 0; i < getLandingLength(); i++) { - Coords pos = landingPos.translated(face, i); + Coords pos = landingPos.translated(facing, i); landingPositions.add(pos); // Dropships have to check the front adjacent hexes - if (isDropship) { - landingPositions.add(pos.translated((face + 4) % 6)); - landingPositions.add(pos.translated((face + 2) % 6)); + if (this instanceof Dropship) { + landingPositions.add(pos.translated((facing + 4) % 6)); + landingPositions.add(pos.translated((facing + 2) % 6)); } } } - - for (Coords pos : landingPositions) { - Hex hex = ((Entity) this).getGame().getBoard().getHex(pos); - if (hex.containsTerrain(Terrains.ROUGH) || hex.containsTerrain(Terrains.RUBBLE)) { - rough = true; - } else if (hex.containsTerrain(Terrains.WOODS, 2)) { - heavyWoods = true; - } else if (hex.containsTerrain(Terrains.WOODS, 1)) { - lightWoods = true; - } else if (!hex.containsTerrain(Terrains.PAVEMENT) && !hex.containsTerrain(Terrains.ROAD)) { - paved = false; - // Landing in other terrains isn't allowed, so if we reach here - // it must be a clear hex - clear = true; - } - } - - if (heavyWoods) { - roll.addModifier(+5, "heavy woods in landing path"); - } - if (lightWoods) { - roll.addModifier(+4, "light woods in landing path"); - } - if (rough) { - roll.addModifier(+3, "rough/rubble in landing path"); - } - if (paved) { - roll.addModifier(+0, "paved/road landing strip"); - } - if (clear) { - roll.addModifier(+2, "clear hex in landing path"); - } - - return roll; + return landingPositions; } /** @@ -737,6 +730,14 @@ default String hasRoomForVerticalLanding() { if (!hex.isClearForLanding()) { return "Unacceptable terrain for landing"; } + // Aerospace units are destroyed by water landings except for those that have flotation hulls. + // LAMs are not. + if (hex.containsTerrain(Terrains.WATER) && !hex.containsTerrain(Terrains.ICE) + && (hex.terrainLevel(Terrains.WATER) > 0) + && (this instanceof Aero) + && !((Entity) this).hasWorkingMisc(MiscType.F_FLOTATION_HULL)) { + return "cannot land on water"; + } return null; } diff --git a/megamek/src/megamek/common/Terrains.java b/megamek/src/megamek/common/Terrains.java index c3d0fd11020..5f0561b8b6b 100644 --- a/megamek/src/megamek/common/Terrains.java +++ b/megamek/src/megamek/common/Terrains.java @@ -500,4 +500,39 @@ public static int getTerrainElevation(int terrainType, int terrainLevel, boolean } } } + + /** + * Modifier to control roll when the terrain is in the landing path. + * @param terrainType Type of terrain + * @param terrainLevel The level of the terrain + * @return The control roll modifier + */ + public static int landingModifier(int terrainType, int terrainLevel) { + switch (terrainType) { + case WOODS: + case JUNGLE: + return (terrainLevel == 3) ? 7 : terrainLevel + 3; + case WATER: + return (terrainLevel > 0) ? 3 : 2; + case ROUGH: + return terrainLevel == 2 ? 5 : 3; + case RUBBLE: + return terrainLevel == 6 ? 5 : 3; + case SAND: + case TUNDRA: + case MAGMA: + case FIELDS: + case SWAMP: + return 2; + case BUILDING: + return terrainLevel + 1; + case SNOW: + return (terrainLevel == 2) ? 1 : 0; + case ICE: + case MUD: + return 1; + default: + return 0; + } + } } diff --git a/megamek/src/megamek/server/GameManager.java b/megamek/src/megamek/server/GameManager.java index cc63b6309d2..297eccb0be7 100644 --- a/megamek/src/megamek/server/GameManager.java +++ b/megamek/src/megamek/server/GameManager.java @@ -4033,6 +4033,40 @@ private void attemptLanding(Entity entity, PilotingRollData roll) { } } + /** + * Any aerospace unit that lands in a rough or rubble hex takes landing hear damage. + * @param aero The landing unit + * @param vertical Whether the landing is vertical + * @param touchdownPos The coordinates of the hex of touchdown + * @param finalPos The coordinates of the hex in which the unit comes to a stop + * @param facing The facing of the landing unit + * @ + */ + private void checkLandingTerrainEffects(IAero aero, boolean vertical, Coords touchdownPos, Coords finalPos, int facing) { + // Landing in a rough for rubble hex damages landing gear. + Set landingPositions = aero.getLandingCoords(vertical, touchdownPos, facing); + if (landingPositions.stream().map(c -> game.getBoard().getHex(c)).filter(Objects::nonNull) + .anyMatch(h -> h.containsTerrain(Terrains.ROUGH) || h.containsTerrain(Terrains.RUBBLE))) { + aero.setGearHit(true); + Report r = new Report(9125); + r.subject = ((Entity) aero).getId(); + addReport(r); + } + // Landing in water can destroy or immobilize the unit. + Hex hex = game.getBoard().getHex(finalPos); + if ((aero instanceof Aero) && hex.containsTerrain(Terrains.WATER) && !hex.containsTerrain(Terrains.ICE) + && (hex.terrainLevel(Terrains.WATER) > 0) + && !((Entity) aero).hasWorkingMisc(MiscType.F_FLOTATION_HULL)) { + if ((hex.terrainLevel(Terrains.WATER) > 1) || !(aero instanceof Dropship)) { + Report r = new Report(9702); + r.subject(((Entity) aero).getId()); + r.addDesc((Entity) aero); + addReport(r); + addReport(destroyEntity((Entity) aero, "landing in deep water")); + } + } + } + private boolean launchUnit(Entity unloader, Targetable unloaded, Coords pos, int facing, int velocity, int altitude, int[] moveVec, int bonus) { @@ -6146,6 +6180,8 @@ private void processMovement(Entity entity, MovePath md, Map