diff --git a/megamek/data/images/hexes/StandardIncludes/StandardHazardousLiquid.tileinc b/megamek/data/images/hexes/StandardIncludes/StandardHazardousLiquid.tileinc new file mode 100644 index 00000000000..b0d381d17af --- /dev/null +++ b/megamek/data/images/hexes/StandardIncludes/StandardHazardousLiquid.tileinc @@ -0,0 +1,5 @@ +# Standard Hazard Liquid Pools + +super * "hazardous_liquid:*;water:1" "" "hazardous_liquid/green2_toxic_water.png" +super * "hazardous_liquid:*;water:*" "" "hazardous_liquid/green1_toxic_water.png" +super * "hazardous_liquid:*;" "" "hazardous_liquid/green2_toxic_water.png" \ No newline at end of file diff --git a/megamek/data/images/hexes/atmospheric.tileset b/megamek/data/images/hexes/atmospheric.tileset index 43c77a9ae67..ef5082136d0 100644 --- a/megamek/data/images/hexes/atmospheric.tileset +++ b/megamek/data/images/hexes/atmospheric.tileset @@ -49,6 +49,8 @@ include "StandardIncludes/StandardThemes.tileinc" include "StandardIncludes/StandardSpace.tileinc" +include "StandardIncludes/StandardHazardousLiquid.tileinc" + super * "water:0" "" "transparent/blue_water_1.png" super * "swamp:1" "" "swamp/swamp_0.png;swamp/swamp_1.png;swamp/swamp_2.png;swamp/swamp_3.png" diff --git a/megamek/data/images/hexes/bw_atmospheric.tileset b/megamek/data/images/hexes/bw_atmospheric.tileset index 4529ee6e128..332e4f67f19 100644 --- a/megamek/data/images/hexes/bw_atmospheric.tileset +++ b/megamek/data/images/hexes/bw_atmospheric.tileset @@ -128,6 +128,8 @@ base -6 "" "volcano" "largeTextures/textureVolcano-6.jpg" include "StandardIncludes/StandardThemes.tileinc" +include "StandardIncludes/StandardHazardousLiquid.tileinc" + super * "water:0" "" "bloodwolf/hq_boring/blue_water_0.png;bloodwolf/hq_boring/blue_water_0.png(0,72-84,-72);bloodwolf/hq_boring/blue_water_0.png(84,0--84,72);bloodwolf/hq_boring/blue_water_0.png(84,72--84,-72)" super * "swamp:1" "desert" "bloodwolf/hq_boring/swamp_0.png;bloodwolf/hq_boring/swamp_0.png(0,72-84,-72);bloodwolf/hq_boring/swamp_0.png(84,0--84,72);bloodwolf/hq_boring/swamp_0.png(84,72--84,-72)" diff --git a/megamek/data/images/hexes/classic.tileset b/megamek/data/images/hexes/classic.tileset index e7eef73cd06..1a2f3648fe4 100644 --- a/megamek/data/images/hexes/classic.tileset +++ b/megamek/data/images/hexes/classic.tileset @@ -25,6 +25,8 @@ include "StandardIncludes/StandardThemes.tileinc" include "StandardIncludes/StandardSpace.tileinc" +include "StandardIncludes/StandardHazardousLiquid.tileinc" + super * "water:0" "" "boring/blue_water_1.gif" super * "fortified:1" "" "boring/sandbags.gif" diff --git a/megamek/data/images/hexes/hazardous_liquid/green1_toxic_water.png b/megamek/data/images/hexes/hazardous_liquid/green1_toxic_water.png new file mode 100644 index 00000000000..3ab1aeff2f4 Binary files /dev/null and b/megamek/data/images/hexes/hazardous_liquid/green1_toxic_water.png differ diff --git a/megamek/data/images/hexes/hazardous_liquid/green2_toxic_water.png b/megamek/data/images/hexes/hazardous_liquid/green2_toxic_water.png new file mode 100644 index 00000000000..8b9c049f247 Binary files /dev/null and b/megamek/data/images/hexes/hazardous_liquid/green2_toxic_water.png differ diff --git a/megamek/data/images/hexes/hq_atmospheric.tileset b/megamek/data/images/hexes/hq_atmospheric.tileset index 9f4a727cb19..e8ec58842e0 100644 --- a/megamek/data/images/hexes/hq_atmospheric.tileset +++ b/megamek/data/images/hexes/hq_atmospheric.tileset @@ -52,6 +52,8 @@ include "StandardIncludes/StandardThemes.tileinc" include "StandardIncludes/StandardSpace.tileinc" +include "StandardIncludes/StandardHazardousLiquid.tileinc" + super * "water:0" "" "transparent/anim_water_1.gif" super * "swamp:1" "" "swamp/swamp_0.png;swamp/swamp_1.png;swamp/swamp_2.png;swamp/swamp_3.png" diff --git a/megamek/data/images/hexes/hq_isometric.tileset b/megamek/data/images/hexes/hq_isometric.tileset index 6ec827ba675..1cf6e5380ac 100644 --- a/megamek/data/images/hexes/hq_isometric.tileset +++ b/megamek/data/images/hexes/hq_isometric.tileset @@ -52,6 +52,8 @@ include "StandardIncludes/StandardThemes.tileinc" include "StandardIncludes/StandardSpace.tileinc" +include "StandardIncludes/StandardHazardousLiquid.tileinc" + super * "water:0" "" "transparent/anim_water_1.gif" super * "swamp:1" "" "swamp/swamp_0.png;swamp/swamp_1.png;swamp/swamp_2.png;swamp/swamp_3.png" diff --git a/megamek/data/images/hexes/hq_saxarba.tileset b/megamek/data/images/hexes/hq_saxarba.tileset index a822d35c0d2..5c0502d5f15 100644 --- a/megamek/data/images/hexes/hq_saxarba.tileset +++ b/megamek/data/images/hexes/hq_saxarba.tileset @@ -87,6 +87,8 @@ super 8 "sand:*" "" "saxarba/base/base_sand_8.png" super 9 "sand:*" "" "saxarba/base/base_sand_9.png" super 10 "sand:*" "" "saxarba/base/base_sand_10.png" +include "StandardIncludes/StandardHazardousLiquid.tileinc" + #MARS THEME WATER super * "water:0" "mars" "saxarba/theme_mars/water_anim_mars_0.gif" diff --git a/megamek/data/images/hexes/isometric.tileset b/megamek/data/images/hexes/isometric.tileset index 353f34b10a0..0e8e455621c 100644 --- a/megamek/data/images/hexes/isometric.tileset +++ b/megamek/data/images/hexes/isometric.tileset @@ -49,6 +49,8 @@ include "StandardIncludes/StandardThemes.tileinc" include "StandardIncludes/StandardSpace.tileinc" +include "StandardIncludes/StandardHazardousLiquid.tileinc" + super * "water:0" "" "transparent/blue_water_1.png" super * "swamp:1" "" "swamp/swamp_0.png;swamp/swamp_1.png;swamp/swamp_2.png;swamp/swamp_3.png" diff --git a/megamek/data/images/hexes/largeTextures.tileset b/megamek/data/images/hexes/largeTextures.tileset index be9051aeb7e..0887ecab73e 100644 --- a/megamek/data/images/hexes/largeTextures.tileset +++ b/megamek/data/images/hexes/largeTextures.tileset @@ -171,6 +171,7 @@ base -6 "" "" "Ahne/iLarge_Textures/BigPlains-6.jpg" base 0 "space:1" "" "largeTextures/BigSpace.jpg" +include "StandardIncludes/StandardHazardousLiquid.tileinc" #base 1 "water:*" "" "Ahne/iLarge_Textures/BigWater.jpg(0,72-84,-72);Ahne/iLarge_Textures/BigWater.jpg(84,0--84,72);Ahne/iLarge_Textures/BigWater.jpg(84,72--84,-72)" #base -1 "water:*" "" "Ahne/iLarge_Textures/BigWater2.jpg(0,72-84,-72);Ahne/iLarge_Textures/BigWater2.jpg(84,0--84,72);Ahne/iLarge_Textures/BigWater2.jpg(84,72--84,-72)" diff --git a/megamek/data/images/hexes/saxarba.tileset b/megamek/data/images/hexes/saxarba.tileset index 9b5531c44a9..527c86a9812 100644 --- a/megamek/data/images/hexes/saxarba.tileset +++ b/megamek/data/images/hexes/saxarba.tileset @@ -90,6 +90,8 @@ super 8 "sand:*" "" "saxarba/base/base_sand_8.png" super 9 "sand:*" "" "saxarba/base/base_sand_9.png" super 10 "sand:*" "" "saxarba/base/base_sand_10.png" +include "StandardIncludes/StandardHazardousLiquid.tileinc" + #MARS THEME WATER super * "water:0" "mars" "saxarba/theme_mars/water_mars_0.png" diff --git a/megamek/docs/history.txt b/megamek/docs/history.txt index 36bf05b8d3e..eae4b5a191a 100644 --- a/megamek/docs/history.txt +++ b/megamek/docs/history.txt @@ -1,6 +1,10 @@ MEGAMEK VERSION HISTORY: ---------------- 0.50.04-SNAPSHOT ++ PR #6494: Cap splashscreen button width ++ PR #6489: PPC Capacitors must all be linked to a PPC ++ PR #6500: Issue 6499: Better nudging for aeros that fail maneuvers and scatter off board ++ PR #6476: Issue 6303: RFE - Hazardous Liquid Pool 0.50.03 (2025-02-02 2030 UTC) + PR #6335: default the directory filter to Select All in Advanced Board Search diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 91072d6c4b7..00edebd9277 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -2710,6 +2710,7 @@ MovementDisplay.ConfirmLaunch=Launching this many units will exceed this bay's l MovementDisplay.MagmaCrustMoving=Crossing magma crust will break through on 6+ on 1d6.\n MovementDisplay.MagmaCrustJumpLanding=Jumping onto magma crust will break through on 4+ on 1d6.\n MovementDisplay.MagmaLiquidMoving=Red hot molten lava melts armour and destroys non-meks!\n +MovementDisplay.HazardousLiquidMoving=Caustic hazardous liquids may damage units!\n MovementDisplay.ManeuverDialog.title=Choose Maneuver MovementDisplay.MicroliteMove.message=Microlite VTOL units must enter a new hex each turn to remain in flight.\n MovementDisplay.MicroliteMove.title=Microlite VTOL movement diff --git a/megamek/i18n/megamek/common/messages.properties b/megamek/i18n/megamek/common/messages.properties index 223321e6566..38739b75b88 100644 --- a/megamek/i18n/megamek/common/messages.properties +++ b/megamek/i18n/megamek/common/messages.properties @@ -426,6 +426,7 @@ Terrains.editorName.incline_high_top=High Incline Top Terrains.editorName.incline_high_bottom=High Incline Bottom Terrains.editorName.cliff_bottom=Cliff Bottom Terrains.editorName.deployment_zone=Deployment Zone +Terrains.editorName.hazardous_liquid=Hazardous Liquid Terrains.editorTooltip.woods=Woods Terrains.editorTooltip.water=Water @@ -477,6 +478,7 @@ Terrains.editorTooltip.ground_fluff=Special ground images Terrains.editorTooltip.water_fluff=Special water images Terrains.editorTooltip.foliage_elev=Defines how tall the woods/jungle is (required) Terrains.editorTooltip.deployment_zone=A custom deployment zone (level is group #) +Terrains.editorTooltip.hazardous_liquid=A modifier to water that damages units (level is if wind or water flow effect it) WeaponType.DirectFire=Direct fire WeaponType.BallisticCluster=Ballistic Cluster @@ -778,3 +780,9 @@ AutoResolveDialog.victory=Victory! AutoResolveDialog.defeat=Defeat! ResolveDialog.control.title=Control of Battlefield? ResolveDialog.control.message=Did your side control the battlefield at the end of the scenario? + +HazardousLiquidPoolUtil.CLASS_0.text=Class 0: Normal +HazardousLiquidPoolUtil.CLASS_1.text=Class 1: Slightly Hazardous +HazardousLiquidPoolUtil.CLASS_2.text=Class 2: Hazardous +HazardousLiquidPoolUtil.CLASS_3.text=Class 3: Extreme Danger +HazardousLiquidPoolUtil.DEADLY.text=Deadly diff --git a/megamek/i18n/megamek/common/report-messages.properties b/megamek/i18n/megamek/common/report-messages.properties index 48691a57f36..8ce920a2797 100755 --- a/megamek/i18n/megamek/common/report-messages.properties +++ b/megamek/i18n/megamek/common/report-messages.properties @@ -267,6 +267,8 @@ 2515= is dropped to the ground. Needs , rolls : 2516=success - cargo remains intact. 2517=failure - cargo destroyed! +2520= () enters hazardous liquid, rolls : +2524= () is damaged by the caustic liquid! 3000=Weapon Attack Phase------------------- 3003=Inferno fire (bombs) started in hex . diff --git a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java index fc099df39ff..4082f849121 100644 --- a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java @@ -26,6 +26,7 @@ import megamek.common.*; import megamek.common.options.OptionsConstants; import megamek.common.planetaryconditions.PlanetaryConditions; +import megamek.common.util.HazardousLiquidPoolUtil; import megamek.logging.MMLogger; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -885,6 +886,9 @@ private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, Mo // 1 in 3 chance to hit Black Ice on any given Pavement hex hazardValue += calcIceHazard(movingUnit, hex, step, movePath, jumpLanding) / 3.0; break; + case Terrains.HAZARDOUS_LIQUID: + hazardValue += calcHazardousLiquidHazard(hex, endHex, movingUnit, step); + break; } } @@ -903,7 +907,8 @@ private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, Mo Terrains.SNOW, Terrains.SWAMP, Terrains.MUD, - Terrains.TUNDRA)); + Terrains.TUNDRA, + Terrains.HAZARDOUS_LIQUID)); private static final Set HAZARDS_WITH_BLACK_ICE = new HashSet<>(); static { HAZARDS_WITH_BLACK_ICE.addAll(HAZARDS); @@ -1288,6 +1293,65 @@ private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity moving return Math.round(hazardValue * psrFactor); } + private double calcHazardousLiquidHazard(Hex hex, boolean endHex, Entity movingUnit, MoveStep step) { + logger.trace("Calculating hazardous liquid hazard."); + int unitDamageLevel = movingUnit.getDamageLevel(); + double dmg; + + // Hovers/VTOLs are unaffected _unless_ they end on the hex and are in danger of + // losing mobility. + if (EntityMovementMode.HOVER == movingUnit.getMovementMode() + || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { + if (!endHex) { + logger.trace("Hovering/VTOL while traversing hazardous liquids (0)."); + return 0; + } else { + // Estimate chance of being disabled or immobilized over open lava; this is + // fatal! + // Calc expected damage as ((current damage level [0 ~ 4]) / 4) * + // UNIT_DESTRUCTION_FACTOR + dmg = (unitDamageLevel / 4.0) * UNIT_DESTRUCTION_FACTOR; + logger.trace("Ending hover/VTOL movement over lava ({}).", dmg); + return dmg; + } + } + + dmg = (HazardousLiquidPoolUtil.AVERAGE_DAMAGE_HAZARDOUS_LIQUID_POOL * HazardousLiquidPoolUtil.getHazardousLiquidPoolDamageMultiplierForUnsealed(movingUnit)) + / (HazardousLiquidPoolUtil.getHazardousLiquidPoolDamageDivisorForInfantry(movingUnit)); + + // After all that math let's make sure we do at least 1 damage + // (.6 repeating when normalized for the HLP doing no damage 1/3 of the time) + dmg = Math.max(dmg, 2.0/3.0); + + // Factor in potential to suffer fatal damage. + // Dependent on expected average damage / exposed remaining armor * + // UNIT_DESTRUCTION_FACTOR + int exposedArmor; + double hazardValue = 0; + if (step.isProne() || (hex.containsTerrain(Terrains.WATER) && hex.terrainLevel(Terrains.WATER) > 1)) { + exposedArmor = movingUnit.getTotalArmor(); + logger.trace("Fully Submerged damage = {}, exposed armor = {}", dmg, exposedArmor); + } else if (movingUnit instanceof BipedMek) { + exposedArmor = Stream.of(Mek.LOC_LLEG, Mek.LOC_RLEG).mapToInt(movingUnit::getArmor).sum(); + logger.trace("Biped Mek damage = {}, exposed armor = {}", dmg, exposedArmor); + } else if (movingUnit instanceof TripodMek) { + exposedArmor = Stream.of(Mek.LOC_LLEG, Mek.LOC_RLEG, Mek.LOC_CLEG) + .mapToInt(movingUnit::getArmor).sum(); + logger.trace("Tripod Mek damage = {}, exposed armor = {}", dmg, exposedArmor); + } else if (movingUnit instanceof QuadMek){ + exposedArmor = Stream.of(Mek.LOC_LLEG, Mek.LOC_RLEG, Mek.LOC_LARM, Mek.LOC_RARM) + .mapToInt(movingUnit::getArmor).sum(); + logger.trace("Quad Mek damage = {}, exposed armor = {}", dmg, exposedArmor); + } else { + exposedArmor = movingUnit.getTotalArmor(); + logger.trace("Fully Submerged non-mek damage = {}, exposed armor = {}", dmg, exposedArmor); + } + hazardValue += (UNIT_DESTRUCTION_FACTOR * (dmg / Math.max(exposedArmor, 1))); + + logger.trace("Total hazard = {}", hazardValue); + return Math.round(hazardValue); + } + private double calcBogDownFactor(String name, boolean endHex, boolean jumpLanding, int pilotSkill, int modifier) { return calcBogDownFactor(name, endHex, jumpLanding, pilotSkill, modifier, true); } diff --git a/megamek/src/megamek/client/ui/SharedUtility.java b/megamek/src/megamek/client/ui/SharedUtility.java index 72e209a63d8..03f8d2f8abe 100644 --- a/megamek/src/megamek/client/ui/SharedUtility.java +++ b/megamek/src/megamek/client/ui/SharedUtility.java @@ -23,6 +23,7 @@ import megamek.common.*; import megamek.common.MovePath.MoveStepType; import megamek.common.annotations.Nullable; +import megamek.common.internationalization.Internationalization; import megamek.common.options.OptionsConstants; import megamek.logging.MMLogger; import megamek.server.totalwarfare.TWGameManager; @@ -346,6 +347,16 @@ private static Object doPSRCheck(MovePath md, boolean stringResult) { .getString("MovementDisplay.MagmaLiquidMoving")); } + // Check for Hazardous Liquid + if (curHex.containsTerrain(Terrains.HAZARDOUS_LIQUID) && (step.getElevation() <= 0) + && (moveType != EntityMovementType.MOVE_JUMP) + && (entity.getMovementMode() != EntityMovementMode.HOVER) + && (entity.getMovementMode() != EntityMovementMode.WIGE) + && !(curPos.equals(lastPos))) { + nagReport.append(Internationalization + .getTextAt("megamek.client.messages", "MovementDisplay.HazardousLiquidMoving")); + } + // check for sideslip if ((entity instanceof VTOL) || (entity.getMovementMode() == EntityMovementMode.HOVER) @@ -600,6 +611,11 @@ private static Object doPSRCheck(MovePath md, boolean stringResult) { } else if ((level == 2) && (lastElevation == 0)) { nagReport.append(Messages.getString("MovementDisplay.MagmaLiquidMoving")); } + + if ((hex.containsTerrain(Terrains.HAZARDOUS_LIQUID)) && (lastElevation == 0)) { + nagReport.append(Internationalization + .getTextAt("megamek.client.messages", "MovementDisplay.HazardousLiquidMoving")); + } } if (entity.isAirborne() && entity.isAero()) { diff --git a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java index f55bf6535a3..0ec1af35731 100644 --- a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java +++ b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java @@ -261,6 +261,11 @@ private void showMainMenu() { // the button width "look" reasonable. int maximumWidth = (int) (0.9 * scaledMonitorSize.width) - splash.getPreferredSize().width; + //no more than 50% of image width + if (maximumWidth > (int) (0.5 * splash.getPreferredSize().width)) { + maximumWidth = (int) (0.5 * splash.getPreferredSize().width); + } + Dimension minButtonDim = new Dimension((int) (maximumWidth / 1.618), 25); if (textDim.getWidth() > minButtonDim.getWidth()) { minButtonDim = textDim; diff --git a/megamek/src/megamek/common/Terrain.java b/megamek/src/megamek/common/Terrain.java index 67060e16770..a3643d820b6 100644 --- a/megamek/src/megamek/common/Terrain.java +++ b/megamek/src/megamek/common/Terrain.java @@ -706,6 +706,8 @@ public boolean isValid(@Nullable List errors) { valid = false; } else if ((type == BRIDGE_ELEV) && (level < 0)) { valid = false; + } else if ((type == HAZARDOUS_LIQUID) && (level < 0 || level > 3)) { + valid = false; } if (!valid && (errors != null)) { diff --git a/megamek/src/megamek/common/Terrains.java b/megamek/src/megamek/common/Terrains.java index 127db3a4db1..35933463f7d 100644 --- a/megamek/src/megamek/common/Terrains.java +++ b/megamek/src/megamek/common/Terrains.java @@ -18,6 +18,7 @@ import java.util.HashSet; import java.util.Hashtable; +import megamek.common.enums.HazardousLiquidPoolType; import megamek.server.SmokeCloud; public class Terrains implements Serializable { @@ -63,7 +64,6 @@ public class Terrains implements Serializable { // unimplemented // Bug Storm // Extreme Depths - // Hazardous Liquid Pools // Rail // Dirt Roads, Gravel Roads // Water Flow @@ -153,6 +153,8 @@ public class Terrains implements Serializable { public static final int DEPLOYMENT_ZONE = 57; + public static final int HAZARDOUS_LIQUID = 58; + /** * Keeps track of the different type of terrains that can have exits. */ @@ -165,7 +167,7 @@ public class Terrains implements Serializable { "fuel_tank_magn", "impassable", "elevator", "fortified", "screen", "fluff", "arms", "legs", "metal_deposit", "bldg_base_collapsed", "bldg_fluff", "road_fluff", "ground_fluff", "water_fluff", "cliff_top", "cliff_bottom", "incline_top", "incline_bottom", "incline_high_top", "incline_high_bottom", "foliage_elev", "black_ice", "sky", - "deployment_zone" }; + "deployment_zone", "hazardous_liquid" }; /** Terrains in this set are hidden in the Editor, not saved to board files and handled internally. */ public static final HashSet AUTOMATIC = new HashSet<>(Arrays.asList( @@ -397,6 +399,18 @@ public static String getDisplayName(int type, int level) { } case DEPLOYMENT_ZONE: return "Deployment Zone"; + case HAZARDOUS_LIQUID: + HazardousLiquidPoolType hazardousLiquidPoolType = HazardousLiquidPoolType.getType(level); + switch (hazardousLiquidPoolType) { + case WIND_BLOWN: + return "Hazardous Liquid (Wind Blown)"; + case FLOWS: + return "Hazardous Liquid (Flows)"; + case FLOWS_AND_WIND_BLOWN: + return "Hazardous Liquid (Flows and Wind Blown)"; + default: + return "Hazardous Liquid"; + } default: return null; } diff --git a/megamek/src/megamek/common/enums/HazardousLiquidPoolType.java b/megamek/src/megamek/common/enums/HazardousLiquidPoolType.java new file mode 100644 index 00000000000..bf2992ee8d3 --- /dev/null +++ b/megamek/src/megamek/common/enums/HazardousLiquidPoolType.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 - 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.common.enums; + +import java.util.Arrays; + +/** + * A hazardous liquid pool can be wind-blown, pushed by water flow, both, or neither. + */ +public enum HazardousLiquidPoolType { + // Wind blown hazardous is for MapPack Alien Worlds' Wind Blown Hazardous Liquid rules + // Wind blown (MP: Alien Rules) isn't implemented + // Water flow (TO:AR 47 for Hazardous Pools rules) isn't implemented + NORMAL(0), + WIND_BLOWN(1), + FLOWS(2), + FLOWS_AND_WIND_BLOWN(3); + + private final Integer terrainLevel; + + HazardousLiquidPoolType(final Integer terrainLevel) { + this.terrainLevel = terrainLevel; + } + + public int getTerrainLevel() { + return this.terrainLevel; + } + + public static HazardousLiquidPoolType getType(final int ordinal) { + return Arrays.stream(HazardousLiquidPoolType.values()).filter(type -> type.ordinal() == ordinal).findFirst().orElse(NORMAL); + } +} diff --git a/megamek/src/megamek/common/util/HazardousLiquidPoolUtil.java b/megamek/src/megamek/common/util/HazardousLiquidPoolUtil.java new file mode 100644 index 00000000000..779579a2ba8 --- /dev/null +++ b/megamek/src/megamek/common/util/HazardousLiquidPoolUtil.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2025 - 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.common.util; + +import megamek.common.*; +import megamek.common.internationalization.Internationalization; +import megamek.server.totalwarfare.TWGameManager; + +import java.util.List; +import java.util.Vector; + +/** + * Methods that implement the Hazardous Liquid Pool as described in TO:AR p. 47 + * + */ +public class HazardousLiquidPoolUtil { + + // Average Damage for Hazardous Liquid + // 2/6 chance 0 damage + // 1/6 chance 1d6/2 : 1.75 damage + // 1/6 chance 1d6 : 3.5 damage + // 1/6 chance 1d6 + 2 : 5.5 damage + // 1/6 chance 2d6 : 7 damage + // Total: 2.958333... + // Let's use 3 + public static final double AVERAGE_DAMAGE_HAZARDOUS_LIQUID_POOL = 3.0; + + private enum HazardousLiquidClass { + CLASS_0(0, 0, 1), + CLASS_1(1, 0, 2), + CLASS_2(1, 0, 1), + CLASS_3(1, 2, 1), + DEADLY(2, 0, 1); + + private final int numberOfDice; + private final int staticExtraDamage; + private final int divisor; + + HazardousLiquidClass(int numberOfDice, int staticExtraDamage, int divisor) { + this.numberOfDice = numberOfDice; + this.staticExtraDamage = staticExtraDamage; + this.divisor = divisor; + } + } + + /** + * Use this method to apply damage and generate reports for a unit moving in or starting its turn in hazardous liquid. + * @param entity Entity in the liquid + * @param eruption If this is from an eruoption + * @param depth Int depth of the liquid + * @param twGameManager Current game manager (to damage entity) + * @return reports that should be added + */ + public static List getHazardousLiquidDamage(Entity entity, boolean eruption, int depth, TWGameManager twGameManager){ + List reports = new Vector<>(); + + + // First, what flavor is the hazardous liquid at this moment? + Report hazardousLiquidClassReport = new Report (2520); + hazardousLiquidClassReport.addDesc(entity); + + Roll hazardousLiquidClassRoll = Compute.rollD6(1); + hazardousLiquidClassReport.add(hazardousLiquidClassRoll.getIntValue()); + + HazardousLiquidClass hazardousLiquidClass = switch (hazardousLiquidClassRoll.getIntValue()) { + case 6 -> HazardousLiquidClass.DEADLY; + case 5 -> HazardousLiquidClass.CLASS_3; + case 4 -> HazardousLiquidClass.CLASS_2; + case 3 -> HazardousLiquidClass.CLASS_1; + default -> HazardousLiquidClass.CLASS_0; + }; + hazardousLiquidClassReport.add(Internationalization.getText("HazardousLiquidPoolUtil." + hazardousLiquidClass.name() + ".text")); + reports.add(hazardousLiquidClassReport); + + // Class 0 does no damage, so let's return. + if (hazardousLiquidClass == HazardousLiquidClass.CLASS_0) { + return reports; + } + + Report preDamageReport = new Report(2524); + preDamageReport.addDesc(entity); + preDamageReport.subject = entity.getId(); + reports.add(preDamageReport); + + + // A standing Mek is only hit in its legs, unless it's in deep spicy juice + int toHitTable = ToHitData.HIT_NORMAL; + if ((entity instanceof Mek && !entity.isProne() && !eruption) + || depth > 1) { + toHitTable = ToHitData.HIT_BELOW; + } + + //Calculate damage per TO:AR p. 47 "HAZARDOUS LIQUID POOLS TABLE" + int totalDamage = Math.floorDiv(Compute.d6(hazardousLiquidClass.numberOfDice), hazardousLiquidClass.divisor) + hazardousLiquidClass.staticExtraDamage; + totalDamage *= getHazardousLiquidPoolDamageMultiplierForUnsealed(entity); + totalDamage = (int) (Math.floor(totalDamage / getHazardousLiquidPoolDamageDivisorForInfantry(entity))); + + // After all that math let's make sure we do at least 1 damage. + totalDamage = Math.max(totalDamage, 1); + while (totalDamage > 0) { + int damage = Math.min(totalDamage, 5); + totalDamage = (Math.max(totalDamage - 5, 0)); + HitData hitData = entity.rollHitLocation(toHitTable, ToHitData.SIDE_RANDOM); + reports.addAll((twGameManager.damageEntity(entity, hitData, damage))); + + } + + return reports; + } + + /** + * Support vehicles and industrial meks without environmental sealing take double damage + * @param entity + * @return + */ + public static int getHazardousLiquidPoolDamageMultiplierForUnsealed(Entity entity) { + // IndustrialMeks and Support Vehicles take Double Damage + // unless they have environmental sealing + if ((entity.isIndustrialMek() || entity.isSupportVehicle()) + && (!entity.hasEnvironmentalSealing())) { + return 2; + } + return 1; + } + + /** + * Infantry units take more or less damage depending on if they have XCT training and the appropriate gear + * @param entity + * @return + */ + public static double getHazardousLiquidPoolDamageDivisorForInfantry(Entity entity) { + // If infantry have XCT training and appropriate gear they take 1/3 damage + // Otherwise they take double damage. + // BA take damage as normal. + if (entity.isInfantry() && !entity.isBattleArmor() && entity instanceof Infantry inf) { + if (inf.hasSpecialization(Infantry.XCT) && inf.getArmorKit() != null && inf.getArmorKit().hasSubType(MiscType.S_TOXIC_ATMO)) { + return 3.0; + } else { + return .5; + } + } + return 1; + } +} diff --git a/megamek/src/megamek/common/verifier/TestEntity.java b/megamek/src/megamek/common/verifier/TestEntity.java index 3c9a24e27fb..a41f6ffeb8d 100755 --- a/megamek/src/megamek/common/verifier/TestEntity.java +++ b/megamek/src/megamek/common/verifier/TestEntity.java @@ -1491,6 +1491,15 @@ public boolean hasIllegalEquipmentCombinations(StringBuffer buff) { artemisP++; } else if (m.getType().hasFlag(MiscType.F_APOLLO)) { apollo++; + } else if (m.getType().hasFlag(MiscType.F_PPC_CAPACITOR)) { + if (m.getLinked() == null) { + buff + .append(m.getType().getName()) + .append(" in ") + .append(getEntity().getLocationAbbr(m.getLocation())) + .append(" has no linked PPC\n"); + illegal = true; + } } if (m.getType().hasFlag(MiscType.F_LASER_INSULATOR) && diff --git a/megamek/src/megamek/server/ServerHelper.java b/megamek/src/megamek/server/ServerHelper.java index 1cc55567f16..c3a3ee79327 100644 --- a/megamek/src/megamek/server/ServerHelper.java +++ b/megamek/src/megamek/server/ServerHelper.java @@ -514,6 +514,17 @@ public static void checkEnteringMagma(Hex hex, int elevation, Entity entity, TWG } } + /** + * Check for movement into hazardous liquid and apply damage. + */ + public static void checkEnteringHazardousLiquid(Hex hex, int elevation, Entity entity, TWGameManager gameManager) { + + if (hex.containsTerrain(Terrains.HAZARDOUS_LIQUID) && (elevation <= 0)) { + int depth = hex.containsTerrain(Terrains.WATER) ? hex.terrainLevel(Terrains.WATER) : 0; + gameManager.doHazardousLiquidDamage(entity, false, depth); + } + } + /** * Check for black ice when moving into pavement hex. */ diff --git a/megamek/src/megamek/server/totalwarfare/MovePathHandler.java b/megamek/src/megamek/server/totalwarfare/MovePathHandler.java index a333423298e..9af81f713f6 100644 --- a/megamek/src/megamek/server/totalwarfare/MovePathHandler.java +++ b/megamek/src/megamek/server/totalwarfare/MovePathHandler.java @@ -292,6 +292,15 @@ void processMovement() { tookMagmaDamageAtStart = true; } + // check for starting in hazardous liquid + if ((getGame().getBoard().getHex(entity.getPosition()) + .containsTerrain(Terrains.HAZARDOUS_LIQUID)) + && (entity.getElevation() <= 0)) { + int depth = getGame().getBoard().getHex(entity.getPosition()) + .containsTerrain(Terrains.WATER) ? getGame().getBoard().getHex(entity.getPosition()).terrainLevel(Terrains.WATER) : 0; + gameManager.doHazardousLiquidDamage(entity, false, depth); + } + // set acceleration used to default if (entity.isAero()) { ((IAero) entity).setAccLast(false); @@ -758,6 +767,7 @@ && getGame().getOptions().booleanOption(OptionsConstants.ADVAERORULES_FUEL_CONSU ServerHelper.checkAndApplyMagmaCrust(curHex, entity.getElevation(), entity, curPos, true, gameManager.getMainPhaseReport(), gameManager); ServerHelper.checkEnteringMagma(curHex, entity.getElevation(), entity, gameManager); + ServerHelper.checkEnteringHazardousLiquid(curHex, entity.getElevation(), entity, gameManager); // jumped into swamp? maybe stuck! if (curHex.getBogDownModifier(entity.getMovementMode(), @@ -1568,7 +1578,7 @@ private void processSteps() { a.setStraightMoves(a.getStraightMoves() + 1); // make sure it didn't fly off the map if (!getGame().getBoard().contains(curPos)) { - curPos = curPos.translated(step.getFacing(), -1); //Return its position to on-map so it can be targeted this turn + curPos = nudgeOntoBoard(curPos, step.getFacing()); a.setCurrentVelocity(md.getFinalVelocity()); gameManager.processLeaveMap(md, true, Compute.roundsUntilReturn(getGame(), entity)); return; @@ -2509,11 +2519,13 @@ && getGame().getOptions().booleanOption(OptionsConstants.ADVGRNDMOV_TACOPS_LEAPI } // check for breaking magma crust unless we are jumping over the hex + // Let's check for hazardous liquid damage too if (stepMoveType != EntityMovementType.MOVE_JUMP) { if (!curPos.equals(lastPos)) { ServerHelper.checkAndApplyMagmaCrust(curHex, step.getElevation(), entity, curPos, false, gameManager.getMainPhaseReport(), gameManager); ServerHelper.checkEnteringMagma(curHex, step.getElevation(), entity, gameManager); + ServerHelper.checkEnteringHazardousLiquid(curHex, step.getElevation(), entity, gameManager); } } @@ -3440,4 +3452,53 @@ else if ((step.getElevation() + entity.height()) == 0) { } } + + /** + * When something is improperly moved off board, like after a failed aero maneuver, we should move it back onto + * the board so exceptions don't get thrown. + * @param position entity's current position + * @param facing entity's current facing + * @return new coords that are on the board + */ + private Coords nudgeOntoBoard(Coords position, int facing) { + Coords newPosition = position; + + Game game = getGame(); + Board board = game.getBoard(); + + // When nudging horizontally, let's try to use the facing so we wind up in a more accurate position - + // Unless the facing is north/south, then let's just pick a facing and nudge it back onto the board + + // If we're to the left of the board, nudge right until we're on the board + while (newPosition.getX() < 0) { + if (facing == 4 || facing == 1) { + newPosition = newPosition.translated(1); + } else { + newPosition = newPosition.translated(2); + } + } + + // If we're to the right of the board, nudge left until we're on the board + while (newPosition.getX() > (board.getWidth()-1)) { + if (facing == 2 || facing == 5) { + newPosition = newPosition.translated(5); + } else { + newPosition = newPosition.translated(4); + } + } + + + // If we're above the board, nudge down until we're on the board + while (newPosition.getY() < 0) { + newPosition = newPosition.translated(3); + } + + // If we're below the board, nudge up until we're on the board + // Note that Height is 1-indexed, so we need to make it 0-indexed. + while (newPosition.getY() > (board.getHeight()-1) ) { + newPosition = newPosition.translated(0); + } + + return newPosition; + } } diff --git a/megamek/src/megamek/server/totalwarfare/TWGameManager.java b/megamek/src/megamek/server/totalwarfare/TWGameManager.java index 91564433635..d2831b3826b 100644 --- a/megamek/src/megamek/server/totalwarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalwarfare/TWGameManager.java @@ -60,6 +60,7 @@ import megamek.common.util.BoardUtilities; import megamek.common.util.C3Util; import megamek.common.util.EmailService; +import megamek.common.util.HazardousLiquidPoolUtil; import megamek.common.util.fileUtils.MegaMekFile; import megamek.common.verifier.TestEntity; import megamek.common.weapons.AreaEffectHelper; @@ -4687,6 +4688,7 @@ else if ((target instanceof Infantry) && (bldg != null)) { // otherwise, magma crust won't have a chance to break ServerHelper.checkAndApplyMagmaCrust(nextHex, nextElevation, entity, curPos, false, mainPhaseReport, this); ServerHelper.checkEnteringMagma(nextHex, nextElevation, entity, this); + ServerHelper.checkEnteringHazardousLiquid(nextHex, nextElevation, entity, this); // is the next hex a swamp? PilotingRollData rollTarget = entity.checkBogDown(step, moveType, nextHex, curPos, nextPos, @@ -8904,6 +8906,7 @@ Vector doEntityDisplacement(Entity entity, Coords src, ServerHelper.checkAndApplyMagmaCrust(destHex, entity.getElevation(), entity, dest, false, displacementReport, this); ServerHelper.checkEnteringMagma(destHex, entity.getElevation(), entity, this); + ServerHelper.checkEnteringHazardousLiquid(destHex, entity.getElevation(), entity, this); Entity violation = Compute.stackingViolation(game, entity.getId(), dest, entity.climbMode()); if (violation == null) { @@ -31177,6 +31180,23 @@ void doAllAssaultDrops() { } } + /** + * Airborne ground units aren't effected by dangerous terrain, unless it's an erupton + * @param en + * @param eruption + * @return true if the unit should be damaged by dangerous grund (magma, hazardous liquid pool) + */ + private boolean isUnitEffectedByHazardousGround(Entity en, boolean eruption) { + if ((((en.getMovementMode() == EntityMovementMode.VTOL) && (en.getElevation() > 0)) + || (en.getMovementMode() == EntityMovementMode.HOVER) + || ((en.getMovementMode() == EntityMovementMode.WIGE) + && (en.getOriginalWalkMP() > 0) && !eruption)) + && !en.isImmobile()) { + return false; + } + return true; + } + /** * do damage from magma * @@ -31186,13 +31206,10 @@ void doAllAssaultDrops() { * of an eruption */ public void doMagmaDamage(Entity en, boolean eruption) { - if ((((en.getMovementMode() == EntityMovementMode.VTOL) && (en.getElevation() > 0)) - || (en.getMovementMode() == EntityMovementMode.HOVER) - || ((en.getMovementMode() == EntityMovementMode.WIGE) - && (en.getOriginalWalkMP() > 0) && !eruption)) - && !en.isImmobile()) { + if (!isUnitEffectedByHazardousGround(en, eruption)) { return; } + Report r; boolean isMek = en instanceof Mek; if (isMek) { @@ -31217,6 +31234,26 @@ public void doMagmaDamage(Entity en, boolean eruption) { addNewLines(); } + /** + * do damage from hazardous liquids + * + * @param en the affected Entity + * @param eruption boolean indicating whether or not this is + * because + * of an eruption (geyser) + * @param depth How deep is the hazardous liquid? + */ + public void doHazardousLiquidDamage(Entity en, boolean eruption, int depth) { + if (!isUnitEffectedByHazardousGround(en, eruption)) { + return; + } + + for (Report report : HazardousLiquidPoolUtil.getHazardousLiquidDamage(en, eruption, depth, this)) { + addReport(report); + } + addNewLines(); + } + /** * Applies damage to any eligible unit hit by anti-TSM missiles or entering * a hex with green smoke. diff --git a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java index ce1aaea07d9..8f01b155fa4 100644 --- a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java +++ b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java @@ -39,6 +39,7 @@ import java.util.*; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import megamek.client.bot.princess.BotGeometry.HexLine; @@ -1599,6 +1600,319 @@ void testMagmaHazard() { assertEquals(83.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); } + /** + * The tests from most the hazard-testing-related-tests could + * probably be broken down into individual tests - and put here. + */ + @Nested + class hazardTests { + BasicPathRanker testRanker; + List testCoords; + Coords testCoordsThree; + List testHexes; + Hex mockFinalHex; + Vector stepVector; + MovePath mockPath; + + Game mockGame; + + Entity mockUnit; + Crew mockCrew; + + Building mockBuilding; + + @BeforeEach + void init() { + testRanker = spy(new BasicPathRanker(mockPrincess)); + + testCoords = setupCoords("10,7", "10,8", "10,9", "10,10"); + testCoordsThree = testCoords.get(2); + + testHexes = setupHexes(testCoords); + mockFinalHex = testHexes.get(3); + + stepVector = setupMoveStepVector(testCoords); + + mockPath = setupPath(stepVector); + mockGame = setupGame(testCoords, testHexes); + + mockUnit = mock(BipedMek.class); + + mockCrew = mock(Crew.class); + when(mockUnit.getCrew()).thenReturn(mockCrew); + when(mockCrew.getPiloting()).thenReturn(5); + + mockBuilding = mock(Building.class); + + when(mockGame.getBoard().getBuildingAt(eq(testCoordsThree))).thenReturn(mockBuilding); + when(mockBuilding.getCurrentCF(eq(testCoordsThree))).thenReturn(77); + } + + @Test + void testThreeHexShallowHazardousLiquid() { + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + + final Hex mockHexTwo = testHexes.get(1); + final Hex mockHexThree = testHexes.get(2); + final Hex mockFinalHex = testHexes.get(3); + + // Test walking through 3 hexes of shallow hazardous liquid. + when(mockHexTwo.depth()).thenReturn(1); + when(mockHexThree.depth()).thenReturn(1); + when(mockFinalHex.depth()).thenReturn(1); + when(mockPath.isJumping()).thenReturn(false); + when(mockHexTwo.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockHexTwo.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + when(mockHexThree.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + when(mockHexTwo.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockHexThree.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockHexTwo.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockHexThree.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + assertEquals(450.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + } + @Test + void testThreeHexDeepHazardousLiquid() { + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + + final Hex mockHexTwo = testHexes.get(1); + final Hex mockHexThree = testHexes.get(2); + final Hex mockFinalHex = testHexes.get(3); + + // Test walking through 3 hexes of deep hazardous liquid - this should be extremely dangerous for our test unit!. + when(mockPath.isJumping()).thenReturn(false); + when(mockHexTwo.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockHexTwo.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + when(mockHexThree.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + when(mockHexTwo.terrainLevel(Terrains.WATER)).thenReturn(2); + when(mockHexThree.terrainLevel(Terrains.WATER)).thenReturn(2); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(2); + when(mockHexTwo.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockHexThree.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + assertEquals(9000.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + } + + @Test + void testProneHazardousLiquid() { + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + + final Hex mockFinalHex = testHexes.get(3); + + final MoveStep mockFinalStep = stepVector.lastElement(); + + // Test the stupidity of going prone in shallow hazardous liquid. + // Now that hazard is inversely related to remaining armor, this is a _BIG_ + // number + when(mockPath.isJumping()).thenReturn(false); + when(mockFinalHex.depth()).thenReturn(1); + when(mockFinalStep.isProne()).thenReturn(true); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1);; + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + assertEquals(3000.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + } + + @Test + void testHazardousLiquidHazard() { + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + + // Test jumping into Hazardous Liquid. + when(mockPath.isJumping()).thenReturn(true); + when(mockUnit.getArmor(eq(Mek.LOC_LLEG))).thenReturn(24); + when(mockUnit.getArmor(eq(Mek.LOC_RLEG))).thenReturn(24); + when(mockFinalHex.depth()).thenReturn(1); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + assertEquals(63.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + } + @Test + void testHazardousLiquidWalkingHazard() { + // Test damaged 'mek walking hazard (more dangerous in deeper liquid) + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + when(mockCrew.getPiloting()).thenReturn(5); + when(mockPath.isJumping()).thenReturn(false); + when(mockFinalHex.depth()).thenReturn(1); + + assertEquals(150.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + } + + @Test + void testDeepHazardousLiquidWalkingHazard() { + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(2); + when(mockFinalHex.depth()).thenReturn(2); + + assertEquals(3000., testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + } + + @Test + void testCrippledHazardousLiquidWalkingHazard() { + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + when(mockUnit.getArmor(eq(Mek.LOC_LLEG))).thenReturn(2); + when(mockUnit.getArmor(eq(Mek.LOC_RLEG))).thenReturn(2); + + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockFinalHex.depth()).thenReturn(1); + + when(mockUnit.getDamageLevel()).thenReturn(Entity.DMG_CRIPPLED); + assertEquals(750.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + } + + @Test + void testCrippledDeepHazardousLiquidWalkingHazard() { + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(2); + + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(2); + when(mockFinalHex.depth()).thenReturn(2); + + assertEquals(3000.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + } + + + @Test + void testHazardousLiquidUnsealedIndustrialMek() { + // If this is an industrial Mek this is twice as dangerous! + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + when(mockFinalHex.depth()).thenReturn(1); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + + when(mockUnit.isIndustrialMek()).thenReturn(true); + when(mockUnit.hasEnvironmentalSealing()).thenReturn(false); + assertEquals(300.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + } + + + @Test + void testHazardousLiquidSealedIndustrialMek() { + //If it has environmental sealing though it should be normal + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + when(mockFinalHex.depth()).thenReturn(1); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + + + when(mockUnit.hasEnvironmentalSealing()).thenReturn(true); + assertEquals(150.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + when(mockUnit.isIndustrialMek()).thenReturn(false); + when(mockUnit.hasEnvironmentalSealing()).thenReturn(false); + } + + + @Test + void testHoverCraft() { + when(mockFinalHex.depth()).thenReturn(1); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + + // Check damaged Hover ending on Hazardous Liquid + // Ramps up quickly with damage state! + final Entity mockTank = mock(Tank.class); + when(mockTank.locations()).thenReturn(5); + when(mockTank.getArmor(anyInt())).thenReturn(10); + when(mockTank.getCrew()).thenReturn(mockCrew); + when(mockCrew.getPiloting()).thenReturn(5); + when(mockPath.isJumping()).thenReturn(false); + when(mockTank.getMovementMode()).thenReturn(EntityMovementMode.HOVER); + when(mockTank.getHeatCapacity()).thenReturn(Entity.DOES_NOT_TRACK_HEAT); + + when(mockTank.getDamageLevel()).thenReturn(Entity.DMG_NONE); + assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + + } + + + @Test + void testLightDamageHoverCraft() { + when(mockFinalHex.depth()).thenReturn(1); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + + // Check damaged Hover ending on Hazardous Liquid + // Ramps up quickly with damage state! + final Entity mockTank = mock(Tank.class); + when(mockTank.locations()).thenReturn(5); + when(mockTank.getArmor(anyInt())).thenReturn(10); + when(mockTank.getCrew()).thenReturn(mockCrew); + when(mockCrew.getPiloting()).thenReturn(5); + when(mockPath.isJumping()).thenReturn(false); + when(mockTank.getMovementMode()).thenReturn(EntityMovementMode.HOVER); + when(mockTank.getHeatCapacity()).thenReturn(Entity.DOES_NOT_TRACK_HEAT); + + when(mockTank.getDamageLevel()).thenReturn(Entity.DMG_LIGHT); + assertEquals(250.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + + } + + + @Test + void testHeavyDamageHoverCraft() { + when(mockFinalHex.depth()).thenReturn(1); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.HAZARDOUS_LIQUID, Terrains.WATER))); + when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(1); + when(mockFinalHex.containsTerrain(Terrains.WATER)).thenReturn(true); + when(mockFinalHex.terrainLevel(Terrains.HAZARDOUS_LIQUID)).thenReturn(1); + + // Check damaged Hover ending on Hazardous Liquid + // Ramps up quickly with damage state! + final Entity mockTank = mock(Tank.class); + when(mockTank.locations()).thenReturn(5); + when(mockTank.getArmor(anyInt())).thenReturn(10); + when(mockTank.getCrew()).thenReturn(mockCrew); + when(mockCrew.getPiloting()).thenReturn(5); + when(mockPath.isJumping()).thenReturn(false); + when(mockTank.getMovementMode()).thenReturn(EntityMovementMode.HOVER); + when(mockTank.getHeatCapacity()).thenReturn(Entity.DOES_NOT_TRACK_HEAT); + + when(mockTank.getDamageLevel()).thenReturn(Entity.DMG_MODERATE); + assertEquals(500.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + } + } + @Test void testSwampHazard() { final BasicPathRanker testRanker = spy(new BasicPathRanker(mockPrincess));