From 4e1b97437b06674f2ef9fa8302f83924ca8f1ded Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 27 Jul 2024 09:37:09 +0200 Subject: [PATCH 01/32] Issue 5790, improve combat comp heat report message --- megamek/i18n/megamek/common/report-messages.properties | 2 +- megamek/i18n/megamek/common/report-messages_de.properties | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/megamek/i18n/megamek/common/report-messages.properties b/megamek/i18n/megamek/common/report-messages.properties index 6a9c54306bc..a0cea53865f 100755 --- a/megamek/i18n/megamek/common/report-messages.properties +++ b/megamek/i18n/megamek/common/report-messages.properties @@ -621,7 +621,7 @@ 5020=Adding heat due to extreme temperatures... 5021=Adding heat due to flawed cooling system... 5025=Subtracting heat due to extreme temperatures... -5026=Subtracting heat due to combat computer... +5026=(Includes heat due to combat computer.) 5027=Subtracting heat due to RISC Emergency Coolant System... 5030=Adding heat from a fire... 5032=Adding heat from magma... diff --git a/megamek/i18n/megamek/common/report-messages_de.properties b/megamek/i18n/megamek/common/report-messages_de.properties index 5ee7c05d34f..38d6f30f1c2 100644 --- a/megamek/i18n/megamek/common/report-messages_de.properties +++ b/megamek/i18n/megamek/common/report-messages_de.properties @@ -615,3 +615,4 @@ 7100= hat die BV-Anteil Siegbedingung mit % erreicht. 7105= hat die BV-Zerst\u00f6rt Siegbedingung mit % erreicht. 7200= hat alle Siegbedingungen erreicht. +5026=(Einschließlich Hitze für den Combat Computer.) From 25f06752fb5f5323ec055cbad7cef5eea46217dc Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 27 Jul 2024 10:40:39 +0200 Subject: [PATCH 02/32] fix lobby copy paste for empty model --- .../client/ui/swing/lobby/ChatLounge.java | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java index 309a532abab..e421d7fb83a 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java +++ b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java @@ -2569,23 +2569,17 @@ public void importClipboard() { StringTokenizer lines = new StringTokenizer(result, "\n"); while (lines.hasMoreTokens()) { String line = lines.nextToken(); - StringTokenizer tabs = new StringTokenizer(line, "\t"); - String unit = ""; - if (tabs.hasMoreTokens()) { - unit = tabs.nextToken(); - } - if (tabs.hasMoreTokens()) { - unit += " " + tabs.nextToken(); - } - MechSummary ms = MechSummaryCache.getInstance().getMech(unit); - if (ms == null) { - continue; - } - Entity newEntity = new MechFileParser(ms.getSourceFile(), - ms.getEntryName()).getEntity(); - if (newEntity != null) { - newEntity.setOwner(localPlayer()); - newEntities.add(newEntity); + String[] tokens = line.split("\t"); + if (tokens.length >= 2) { + String unitName = (tokens[0] + " " + tokens[1]).trim(); + MechSummary ms = MechSummaryCache.getInstance().getMech(unitName); + if (ms != null) { + Entity newEntity = ms.loadEntity(); + if (newEntity != null) { + newEntity.setOwner(localPlayer()); + newEntities.add(newEntity); + } + } } } } catch (Exception ex) { From 593f5d1b8e56b117da16d64236f76dfa838825fb Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 27 Jul 2024 11:18:41 +0200 Subject: [PATCH 03/32] guard against nonexistent weapon --- .../client/ui/swing/boardview/FiringArcSpriteHandler.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java b/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java index 19e76935acc..eb178c63137 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java +++ b/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java @@ -67,6 +67,11 @@ public FiringArcSpriteHandler(BoardView boardView, ClientGUI clientGUI) { public void update(Entity entity, WeaponMounted weapon, @Nullable MovePath movePath) { firingEntity = entity; int weaponId = entity.getEquipmentNum(weapon); + if (weaponId == -1) { + // entities are replaced all the time by server-sent changes, must always guard + clearValues(); + return; + } // findRanges must be called before any call to testUnderWater due to usage of // global-style variables for some reason findRanges(weapon); From c87f3f77240e18a1f28227d2ac476a73f9c21dcb Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 27 Jul 2024 11:49:08 +0200 Subject: [PATCH 04/32] FiringArcSpriteHandler: more guards --- .../client/ui/swing/boardview/FiringArcSpriteHandler.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java b/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java index eb178c63137..b3b46f3ab76 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java +++ b/megamek/src/megamek/client/ui/swing/boardview/FiringArcSpriteHandler.java @@ -64,8 +64,12 @@ public FiringArcSpriteHandler(BoardView boardView, ClientGUI clientGUI) { * @param weapon the selected weapon * @param movePath planned movement in the movement phase */ - public void update(Entity entity, WeaponMounted weapon, @Nullable MovePath movePath) { + public void update(@Nullable Entity entity, @Nullable WeaponMounted weapon, @Nullable MovePath movePath) { firingEntity = entity; + if ((entity == null) || (weapon == null)) { + clearValues(); + return; + } int weaponId = entity.getEquipmentNum(weapon); if (weaponId == -1) { // entities are replaced all the time by server-sent changes, must always guard From cf02da5ecc0c2157811d81af53b1a14d67790e7c Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 27 Jul 2024 17:30:26 +0200 Subject: [PATCH 05/32] Prevent units from unloading over bay door limits, code cleanup --- .../client/ui/swing/MovementDisplay.java | 7 +- megamek/src/megamek/common/Bay.java | 9 +-- megamek/src/megamek/common/Entity.java | 79 ++++++++++--------- 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index 614d303fb17..ea5a2e6ef9b 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -1255,12 +1255,7 @@ public void clear() { updateAeroButtons(); updateLayMineButton(); - loadedUnits = ce.getLoadedUnits(); - for (Entity e : ce.getUnitsUnloadableFromBays()) { - if (!loadedUnits.contains(e)) { - loadedUnits.add(e); - } - } + loadedUnits = ce.getUnloadableUnits(); towedUnits = ce.getLoadedTrailers(); updateLoadButtons(); diff --git a/megamek/src/megamek/common/Bay.java b/megamek/src/megamek/common/Bay.java index 2c5e4774683..2b72d1ccc6e 100644 --- a/megamek/src/megamek/common/Bay.java +++ b/megamek/src/megamek/common/Bay.java @@ -42,7 +42,7 @@ public class Bay implements Transporter, ITechnology { int doors = 1; int doorsNext = 1; int currentdoors = doors; - protected int unloadedThisTurn = 0; + private int unloadedThisTurn = 0; protected int loadedThisTurn = 0; List recoverySlots = new ArrayList<>(); int bayNumber = 0; @@ -182,10 +182,8 @@ public boolean canLoad(Entity unit) { } /** - * To unload units, a bay must have more doors available than units unloaded - * this turn. Can't load, launch or recover into a damaged bay, but you can unload it - * - * @return True when further doors are available to unload units this turn + * @return True when further doors are available to unload units this turn. This method checks only + * the state of bay doors, not if it has units left to unload or the status of those. */ public boolean canUnloadUnits() { return currentdoors > unloadedThisTurn; @@ -235,6 +233,7 @@ public List getDroppableUnits() { /** @return A (possibly empty) list of units from this bay that can be unloaded on the ground. */ public List getUnloadableUnits() { // TODO: we need to handle aeros and VTOLs differently + // TODO: shouldn't this check the entity state like wasLoadedThisTurn()? It is equal to getLoadedUnits() return troops.stream().map(game::getEntity).filter(Objects::nonNull).collect(toList()); } diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index a42d4d7b6e4..18897ca74fc 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -8614,6 +8614,7 @@ public Bay getBayById(int bayNumber) { * @return the DockingCollar with the given ID or null */ @Nullable + @SuppressWarnings("unused") // Used in MHQ public DockingCollar getCollarById(int collarNumber) { return getDockingCollars().stream() .filter(dc -> dc.getCollarNumber() == collarNumber) @@ -8690,47 +8691,50 @@ public Vector getDroppableUnits() { } /** - * @return only entities in that can be unloaded on ground + * @return All Entities that can at this point be unloaded from any of the bays of this Entity. This + * does not include any units that were loaded this turn or any bays where the door capacity has + * been exceeded this turn. + * Note that the returned list may be unmodifiable. + * + * @see #wasLoadedThisTurn() + * @see Bay#canUnloadUnits() */ - public Vector getUnitsUnloadableFromBays() { - Vector result = new Vector<>(); - - // Walk through this entity's transport components; - // add all of their lists to ours. - - // I should only add entities in bays that are functional - for (Transporter next : transports) { - if ((next instanceof Bay) && (((Bay) next).canUnloadUnits())) { - Bay nextbay = (Bay) next; - for (Entity e : nextbay.getUnloadableUnits()) { - if (!e.wasLoadedThisTurn()) { - result.addElement(e); - } - } - } - } - - // Return the list. - return result; + public List getUnitsUnloadableFromBays() { + return transports.stream() + .filter(t -> t instanceof Bay).map(t -> (Bay) t) + .filter(Bay::canUnloadUnits) + .flatMap(b -> b.getUnloadableUnits().stream()) + .filter(e -> !e.wasLoadedThisTurn()) + .toList(); } - public Bay getLoadedBay(int bayID) { - - Vector bays = getFighterBays(); - for (int nbay = 0; nbay < bays.size(); nbay++) { - Bay currentBay = bays.elementAt(nbay); - Vector currentFighters = currentBay.getLoadedUnits(); - for (int nfighter = 0; nfighter < currentFighters.size(); nfighter++) { - Entity fighter = currentFighters.elementAt(nfighter); - if (fighter.getId() == bayID) { - // then we are in the right bay - return currentBay; - } - } - } - - return null; + /** + * @return All Entities that can at this point be unloaded from any transports of this Entity which are + * no Bays. This does not include any units that were loaded this turn. + * Note that the returned list may be unmodifiable. + * + * @see #wasLoadedThisTurn() + */ + public List getUnitsUnloadableFromNonBays() { + return transports.stream() + .filter(t -> !(t instanceof Bay)) + .flatMap(b -> b.getLoadedUnits().stream()) + .filter(e -> !e.wasLoadedThisTurn()) + .toList(); + } + /** + * @return All Entities that can at this point be unloaded from any transports of this Entity. This does + * not include any units that were loaded this turn nor units from bays where the door capacity has been + * exceeded this turn. + * + * @see #wasLoadedThisTurn() + * @see Bay#canUnloadUnits() + */ + public List getUnloadableUnits() { + List loadedUnits = new ArrayList<>(getUnitsUnloadableFromNonBays()); + loadedUnits.addAll(getUnitsUnloadableFromBays()); + return loadedUnits; } /** @@ -8919,6 +8923,7 @@ public boolean unload(Entity unit) { } @Override + @SuppressWarnings("unused") // Used in MHQ public void resetTransporter() { transports.forEach(Transporter::resetTransporter); } From 50747925d453bb9b933be701467dad9be7a78ae3 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 27 Jul 2024 17:45:15 +0200 Subject: [PATCH 06/32] cleanup --- .../client/ui/swing/MovementDisplay.java | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index ea5a2e6ef9b..66493f0a0d7 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -318,10 +318,7 @@ public static MoveCommand[] values(int f, GameOptions opts, boolean forwardIni) // is the shift key held? private boolean shiftheld; - /** - * A local copy of the current entity's loaded units. - */ - private List loadedUnits = null; + private List unloadableUnits = null; /** * A local copy of the current entity's towed trailers. @@ -1255,7 +1252,7 @@ public void clear() { updateAeroButtons(); updateLayMineButton(); - loadedUnits = ce.getUnloadableUnits(); + unloadableUnits = ce.getUnloadableUnits(); towedUnits = ce.getLoadedTrailers(); updateLoadButtons(); @@ -2820,7 +2817,7 @@ private void updateUnloadButton() { } if ((ce instanceof SmallCraft) || ce.isSupportVehicle()) { - setUnloadEnabled(!loadedUnits.isEmpty() && !ce.isAirborne()); + setUnloadEnabled(!unloadableUnits.isEmpty() && !ce.isAirborne()); return; } @@ -2831,13 +2828,13 @@ private void updateUnloadButton() { // A unit that has somehow exited the map is assumed to be unable to unload if (isFinalPositionOnBoard()) { - canUnloadHere = loadedUnits.stream().anyMatch(en -> en.isElevationValid(unloadEl, hex) || (en.getJumpMP() > 0)); + canUnloadHere = unloadableUnits.stream().anyMatch(en -> en.isElevationValid(unloadEl, hex) || (en.getJumpMP() > 0)); // Zip lines, TO pg 219 if (game().getOptions().booleanOption(ADVGRNDMOV_TACOPS_ZIPLINES) && (ce instanceof VTOL)) { - canUnloadHere |= loadedUnits.stream().filter(Entity::isInfantry).anyMatch(en -> !((Infantry) en).isMechanized()); + canUnloadHere |= unloadableUnits.stream().filter(Entity::isInfantry).anyMatch(en -> !((Infantry) en).isMechanized()); } } - setUnloadEnabled(legalGear && canUnloadHere && !loadedUnits.isEmpty()); + setUnloadEnabled(legalGear && canUnloadHere && !unloadableUnits.isEmpty()); } /** Updates the status of the Mount button. */ @@ -3277,21 +3274,21 @@ private Entity getUnloadedUnit() { Entity ce = ce(); Entity choice = null; // Handle error condition. - if (loadedUnits.isEmpty()) { + if (unloadableUnits.isEmpty()) { LogManager.getLogger().error("MovementDisplay#getUnloadedUnit() called without loaded units."); - } else if (loadedUnits.size() > 1) { + } else if (unloadableUnits.size() > 1) { // If we have multiple choices, display a selection dialog. String input = (String) JOptionPane.showInputDialog( clientgui.getFrame(), Messages.getString("MovementDisplay.UnloadUnitDialog.message", ce.getShortName(), ce.getUnusedString()), Messages.getString("MovementDisplay.UnloadUnitDialog.title"), JOptionPane.QUESTION_MESSAGE, null, - SharedUtility.getDisplayArray(loadedUnits), null); - choice = (Entity) SharedUtility.getTargetPicked(loadedUnits, input); + SharedUtility.getDisplayArray(unloadableUnits), null); + choice = (Entity) SharedUtility.getTargetPicked(unloadableUnits, input); } else { // Only one choice. - choice = loadedUnits.get(0); - loadedUnits.remove(0); + choice = unloadableUnits.get(0); + unloadableUnits.remove(0); } // Return the chosen unit. From 8a01be8c8a5436d007359e39a4422acd3b06423a Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 27 Jul 2024 18:36:31 +0200 Subject: [PATCH 07/32] use best hex for FoV highlighting for multi-hex units --- .../swing/boardview/FovHighlightingAndDarkening.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/boardview/FovHighlightingAndDarkening.java b/megamek/src/megamek/client/ui/swing/boardview/FovHighlightingAndDarkening.java index 6d9a1d95d33..5608c515cd0 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/FovHighlightingAndDarkening.java +++ b/megamek/src/megamek/client/ui/swing/boardview/FovHighlightingAndDarkening.java @@ -75,10 +75,14 @@ boolean draw(Graphics boardGraph, Coords c, int drawX, int drawY, boolean saveBo Coords src; boolean hasLoS = true; // in movement phase, calc LOS based on selected hex, otherwise use selected Entity - if (this.boardView1.game.getPhase().isMovement() && this.boardView1.selected != null) { - src = this.boardView1.selected; - } else if (this.boardView1.getSelectedEntity() != null) { - src = this.boardView1.getSelectedEntity().getPosition(); + if (boardView1.game.getPhase().isMovement() && this.boardView1.selected != null) { + src = boardView1.selected; + } else if (boardView1.getSelectedEntity() != null) { + Entity viewer = boardView1.getSelectedEntity(); + src = viewer.getPosition(); + // multi-hex units look from the hex closest to the target to avoid self-blocking + src = viewer.getSecondaryPositions().values().stream() + .min(Comparator.comparingInt(co -> co.distance(c))).orElse(src); } else { src = null; } From 8b06aecfc977463299155bc979716ad1241b3670 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 27 Jul 2024 18:52:14 +0200 Subject: [PATCH 08/32] add a test scenario for DS LOS/FoV --- megamek/data/scenarios/Test Setups/DS_LOS.mms | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 megamek/data/scenarios/Test Setups/DS_LOS.mms diff --git a/megamek/data/scenarios/Test Setups/DS_LOS.mms b/megamek/data/scenarios/Test Setups/DS_LOS.mms new file mode 100644 index 00000000000..bdce00a172c --- /dev/null +++ b/megamek/data/scenarios/Test Setups/DS_LOS.mms @@ -0,0 +1,15 @@ +MMSVersion: 2 +name: Grounded DS Test +planet: None +description: A grounded DS on a small map for use in range/LOS/FoV testing + +map: AGoAC Maps/16x17 Grassland 2.board + +factions: +- name: Test Player + + units: + - fullname: Intruder (3056) + at: [ 10, 9 ] + altitude: 0 + \ No newline at end of file From 1cd72491ab65832ad3b13ac6c6e4f1fd76fbd232 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Fri, 26 Jul 2024 00:56:06 -0700 Subject: [PATCH 09/32] Implement TW LOS and forest rules for Low Altitude maps --- megamek/src/megamek/common/Board.java | 4 + megamek/src/megamek/common/LosEffects.java | 365 +++++++++++--------- megamek/src/megamek/server/GameManager.java | 2 + 3 files changed, 201 insertions(+), 170 deletions(-) diff --git a/megamek/src/megamek/common/Board.java b/megamek/src/megamek/common/Board.java index 288a92cec91..0e2eb855cb7 100644 --- a/megamek/src/megamek/common/Board.java +++ b/megamek/src/megamek/common/Board.java @@ -1020,6 +1020,10 @@ public void load(InputStream is, @Nullable List errors, boolean continue args[i++] = st.ttype == StreamTokenizer.TT_NUMBER ? (int) st.nval + "" : st.sval; } int elevation = Integer.parseInt(args[1]); + if (mapType == T_ATMOSPHERE) { + // All foliage on low-altitude maps have height 1 + args[2] = args[2].replaceAll("foliage_elev:\\d{1,}", "foliage_elev:1"); + } // The coordinates in the .board file are ignored! nd[index] = new Hex(elevation, args[2], args[3], new Coords(index % nw, index / nw)); index++; diff --git a/megamek/src/megamek/common/LosEffects.java b/megamek/src/megamek/common/LosEffects.java index 36678acc997..e4d60c49361 100644 --- a/megamek/src/megamek/common/LosEffects.java +++ b/megamek/src/megamek/common/LosEffects.java @@ -33,10 +33,13 @@ public static class AttackInfo { public boolean attUnderWater; public boolean attInWater; public boolean attOnLand; + public boolean attLowAlt = false; public boolean targetUnderWater; public boolean targetInWater; public boolean targetOnLand; + public boolean targetLowAlt = false; public boolean underWaterCombat; + public boolean lowAltitude = false; public boolean targetEntity = true; public boolean targetInfantry; public boolean targetIsMech; @@ -44,25 +47,25 @@ public static class AttackInfo { public boolean attOffBoard; public Coords attackPos; public Coords targetPos; - + /** * The absolute elevation of the attacker, i.e. the number of levels * attacker is placed above a level 0 hex. */ public int attackAbsHeight; - + /** * The absolute elevation of the target, i.e. the number of levels * target is placed above a level 0 hex. */ public int targetAbsHeight; - + /** * The height of the attacker, that is, how many levels above its * elevation it is for LOS purposes. */ public int attackHeight; - + /** * The height of the target, that is, how many levels above its * elevation it is for LOS purposes. @@ -85,7 +88,7 @@ public static class AttackInfo { public static final int COVER_FULL = 0xF; // blocked (blocked) public static final int COVER_75LEFT = 0x7; // 75% cover (blocked) public static final int COVER_75RIGHT = 0xB; // 75% cover (blocked) - + public static final int DAMAGABLE_COVER_NONE = 0; public static final int DAMAGABLE_COVER_DROPSHIP = 0x1; public static final int DAMAGABLE_COVER_BUILDING = 0x2; @@ -118,13 +121,13 @@ public static class AttackInfo { int damagableCoverTypePrimary = DAMAGABLE_COVER_NONE; /** * Indicates if the secondary cover is damageable - */ + */ int damagableCoverTypeSecondary = DAMAGABLE_COVER_NONE; /** * Keeps track of the building that provides cover. This is used * to assign damage for shots that hit cover. The primary cover is used * if there is a sole piece of cover (horizontal cover, 25% cover). - * In the case of a primary and secondary, the primary cover protects the + * In the case of a primary and secondary, the primary cover protects the * right side. */ Building coverBuildingPrimary = null; @@ -132,15 +135,15 @@ public static class AttackInfo { * Keeps track of the building that provides cover. This is used * to assign damage for shots that hit cover. The secondary cover is used * if there are two buildings that provide cover, like in the case of 75% - * cover or two buildings providing 25% cover for a total of horizontal + * cover or two buildings providing 25% cover for a total of horizontal * cover. The secondary cover protects the left side. */ Building coverBuildingSecondary = null; /** * Keeps track of the grounded DropShip that provides cover. This is - * used to assign damage for shots that hit cover. The primary cover is used + * used to assign damage for shots that hit cover. The primary cover is used * if there is a sole piece of cover (horizontal cover, 25% cover). - * In the case of a primary and secondary, the primary cover protects the + * In the case of a primary and secondary, the primary cover protects the * right side. */ Entity coverDropshipPrimary = null; @@ -148,10 +151,10 @@ public static class AttackInfo { * Keeps track of the grounded DropShip that provides cover. This is * used to assign damage for shots that hit cover. The secondary cover is used * if there are two buildings that provide cover, like in the case of 75% - * cover or two buildings providing 25% cover for a total of horizontal + * cover or two buildings providing 25% cover for a total of horizontal * cover. The secondary cover protects the left side. */ - Entity coverDropshipSecondary = null; + Entity coverDropshipSecondary = null; /** * Stores the hex location of the primary cover. */ @@ -165,11 +168,11 @@ public static class AttackInfo { // Arced shots get no modifiers from any intervening terrain boolean arcedShot = false; - + public Coords getTargetPosition() { return targetLoc; } - + public int getMinimumWaterDepth() { return minimumWaterDepth; } @@ -183,18 +186,18 @@ public void add(LosEffects other) { // We need to update cover if it's present, but we don't want to // remove cover if no new cover is present // this assumes that LoS is being drawn from attacker to target - if (other.damagableCoverTypePrimary != DAMAGABLE_COVER_NONE && + if (other.damagableCoverTypePrimary != DAMAGABLE_COVER_NONE && other.targetCover >= targetCover) { damagableCoverTypePrimary = other.damagableCoverTypePrimary; coverDropshipPrimary = other.coverDropshipPrimary; coverBuildingPrimary = other.coverBuildingPrimary; coverLocPrimary = other.coverLocPrimary; - damagableCoverTypeSecondary = other.damagableCoverTypeSecondary; - coverDropshipSecondary = other.coverDropshipSecondary; - coverBuildingSecondary = other.coverBuildingSecondary; + damagableCoverTypeSecondary = other.damagableCoverTypeSecondary; + coverDropshipSecondary = other.coverDropshipSecondary; + coverBuildingSecondary = other.coverBuildingSecondary; coverLocSecondary = other.coverLocSecondary; - } - + } + blocked |= other.blocked; infProtected |= other.infProtected; plantedFields += other.plantedFields; @@ -214,7 +217,7 @@ public void add(LosEffects other) { attackerCover |= other.attackerCover; if ((null != thruBldg) && !thruBldg.equals(other.thruBldg)) { thruBldg = null; - } + } } public int getPlantedFields() { @@ -442,7 +445,7 @@ public static LosEffects calculateLOS(final Game game, final @Nullable Entity at // this will adjust the effective height of a building target by 1 if the hex contains a rooftop gun emplacement final int targetHeightAdjustment = game.hasRooftopGunEmplacement(targetHex.getCoords()) ? 1 : 0; - + final AttackInfo ai = new AttackInfo(); ai.attackerIsMech = attacker instanceof Mech; ai.attackPos = attackerPosition; @@ -455,17 +458,30 @@ public static LosEffects calculateLOS(final Game game, final @Nullable Entity at } else { ai.targetIsMech = false; } - + + // Adjust units' altitudes for low-altitude map LOS caclulations + // Revisit once we have ground units on low-alt and dual-map functional + final boolean lowAlt = game.getBoard().inAtmosphere() && !game.getBoard().onGround(); + if (attacker.isAirborne()) { + ai.attLowAlt = true; + } + if (target.isAirborne()) { + ai.targetLowAlt = true; + } + if (lowAlt) { + ai.lowAltitude = true; + } + ai.targetInfantry = target instanceof Infantry; ai.attackHeight = attacker.getHeight(); - ai.targetHeight = target.getHeight() + targetHeightAdjustment; + ai.targetHeight = (ai.targetLowAlt) ? target.getAltitude() : target.getHeight() + targetHeightAdjustment; - int attackerElevation = attacker.relHeight() + attackerHex.getLevel(); + int attackerElevation = (ai.attLowAlt) ? attacker.getAltitude() : attacker.relHeight() + attackerHex.getLevel(); // for spotting, a mast mount raises our elevation by 1 if (spotting && attacker.hasWorkingMisc(MiscType.F_MAST_MOUNT, -1)) { - attackerElevation += 1; + attackerElevation += (ai.attLowAlt) ? 0 : 1; } - final int targetElevation = target.relHeight() + targetHex.getLevel() + targetHeightAdjustment; + final int targetElevation = (ai.targetLowAlt) ? target.getAltitude() : target.relHeight() + targetHex.getLevel() + targetHeightAdjustment; ai.attackAbsHeight = attackerElevation; ai.targetAbsHeight = targetElevation; @@ -560,7 +576,7 @@ public static LosEffects calculateLos(Game game, AttackInfo ai) { los.targetLoc = ai.targetPos; return los; } - + boolean diagramLos = game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_LOS1); boolean partialCover = game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_PARTIAL_COVER); double degree = ai.attackPos.degree(ai.targetPos); @@ -570,15 +586,15 @@ public static LosEffects calculateLos(Game game, AttackInfo ai) { } else { finalLoS = LosEffects.losStraight(game, ai, diagramLos, partialCover); } - - finalLoS.hasLoS = !finalLoS.blocked && - (finalLoS.screen < 1) && - (finalLoS.plantedFields < 6) && - (finalLoS.heavyIndustrial < 3) && + + finalLoS.hasLoS = !finalLoS.blocked && + (finalLoS.screen < 1) && + (finalLoS.plantedFields < 6) && + (finalLoS.heavyIndustrial < 3) && ((finalLoS.lightWoods + finalLoS.lightSmoke) + ((finalLoS.heavyWoods + finalLoS.heavySmoke) * 2) + (finalLoS.ultraWoods * 3) < 3); - + finalLoS.targetLoc = ai.targetPos; return finalLoS; } @@ -590,7 +606,7 @@ public static LosEffects calculateLos(Game game, AttackInfo ai) { public ToHitData losModifiers(Game game) { return losModifiers(game, 0, false); } - + public ToHitData losModifiers(Game game, boolean underWaterWeapon) { return losModifiers(game, 0, underWaterWeapon); } @@ -753,7 +769,7 @@ private static LosEffects losStraight(Game game, AttackInfo ai, for (Coords c : in) { los.add(LosEffects.losForCoords(game, ai, c, los.getThruBldg(), diagramLoS, partialCover)); - } + } if ((ai.minimumWaterDepth < 1) && ai.underWaterCombat) { los.blocked = true; @@ -853,13 +869,13 @@ private static LosEffects losDivided(Game game, AttackInfo ai, boolean diagramLo rightLos.infProtected = true; } } - + // Check for advanced cover, only 'mechs can get partial cover - if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_PARTIAL_COVER) && + if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_PARTIAL_COVER) && ai.targetIsMech) { // 75% and vertical cover will have blocked LoS boolean losBlockedByCover = false; - if (leftLos.targetCover == COVER_HORIZONTAL && + if (leftLos.targetCover == COVER_HORIZONTAL && rightLos.targetCover == COVER_NONE) { // 25% cover, left leftLos.targetCover = COVER_LOWLEFT; @@ -868,7 +884,7 @@ private static LosEffects losDivided(Game game, AttackInfo ai, boolean diagramLo rightLos.setCoverDropshipPrimary(leftLos.getCoverDropshipPrimary()); rightLos.setDamagableCoverTypePrimary(leftLos.getDamagableCoverTypePrimary()); rightLos.setCoverLocPrimary(leftLos.getCoverLocPrimary()); - } else if ((leftLos.targetCover == COVER_NONE && + } else if ((leftLos.targetCover == COVER_NONE && rightLos.targetCover == COVER_HORIZONTAL)) { // 25% cover, right leftLos.targetCover = COVER_LOWRIGHT; @@ -877,7 +893,7 @@ private static LosEffects losDivided(Game game, AttackInfo ai, boolean diagramLo leftLos.setCoverDropshipPrimary(rightLos.getCoverDropshipPrimary()); leftLos.setDamagableCoverTypePrimary(rightLos.getDamagableCoverTypePrimary()); leftLos.setCoverLocPrimary(rightLos.getCoverLocPrimary()); - } else if (leftLos.targetCover == COVER_FULL && + } else if (leftLos.targetCover == COVER_FULL && rightLos.targetCover == COVER_NONE) { // vertical cover, left leftLos.targetCover = COVER_LEFT; @@ -887,7 +903,7 @@ private static LosEffects losDivided(Game game, AttackInfo ai, boolean diagramLo rightLos.setDamagableCoverTypePrimary(leftLos.getDamagableCoverTypePrimary()); rightLos.setCoverLocPrimary(leftLos.getCoverLocPrimary()); losBlockedByCover = true; - } else if (leftLos.targetCover == COVER_NONE && + } else if (leftLos.targetCover == COVER_NONE && rightLos.targetCover == COVER_FULL) { // vertical cover, right leftLos.targetCover = COVER_RIGHT; @@ -897,40 +913,40 @@ private static LosEffects losDivided(Game game, AttackInfo ai, boolean diagramLo leftLos.setDamagableCoverTypePrimary(rightLos.getDamagableCoverTypePrimary()); leftLos.setCoverLocPrimary(rightLos.getCoverLocPrimary()); losBlockedByCover = true; - } else if (leftLos.targetCover == COVER_FULL && + } else if (leftLos.targetCover == COVER_FULL && rightLos.targetCover == COVER_HORIZONTAL) { // 75% cover, left leftLos.targetCover = COVER_75LEFT; rightLos.targetCover = COVER_75LEFT; - setSecondaryCover(leftLos, rightLos); - losBlockedByCover = true; - } else if (leftLos.targetCover == COVER_HORIZONTAL && - rightLos.targetCover == COVER_FULL) { + setSecondaryCover(leftLos, rightLos); + losBlockedByCover = true; + } else if (leftLos.targetCover == COVER_HORIZONTAL && + rightLos.targetCover == COVER_FULL) { // 75% cover, right leftLos.targetCover = COVER_75RIGHT; rightLos.targetCover = COVER_75RIGHT; setSecondaryCover(leftLos, rightLos); losBlockedByCover = true; - } else if (leftLos.targetCover == COVER_HORIZONTAL && - rightLos.targetCover == COVER_HORIZONTAL) { + } else if (leftLos.targetCover == COVER_HORIZONTAL && + rightLos.targetCover == COVER_HORIZONTAL) { // 50% cover // Cover will be set properly, but we need to set secondary // cover in case there are two buildings providing 25% cover setSecondaryCover(leftLos, rightLos); } - // In the case of vertical and 75% cover, LoS will be blocked. + // In the case of vertical and 75% cover, LoS will be blocked. // We need to unblock it, unless Los is already blocked. - if (!los.blocked && (!leftLos.blocked || !rightLos.blocked) && losBlockedByCover) { + if (!los.blocked && (!leftLos.blocked || !rightLos.blocked) && losBlockedByCover) { leftLos.blocked = false; rightLos.blocked = false; - } + } } - - if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_PARTIAL_COVER) && + + if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_PARTIAL_COVER) && ai.attackerIsMech) { // 75% and vertical cover will have blocked LoS boolean losBlockedByCover = false; - if (leftLos.attackerCover == COVER_HORIZONTAL && + if (leftLos.attackerCover == COVER_HORIZONTAL && rightLos.attackerCover == COVER_NONE) { // 25% cover, left leftLos.attackerCover = COVER_LOWLEFT; @@ -965,7 +981,7 @@ private static LosEffects losDivided(Game game, AttackInfo ai, boolean diagramLo && (rightLos.attackerCover == COVER_HORIZONTAL)) { // 75% cover, left leftLos.attackerCover = COVER_75LEFT; - rightLos.attackerCover = COVER_75LEFT; + rightLos.attackerCover = COVER_75LEFT; losBlockedByCover = true; } else if ((leftLos.attackerCover == COVER_HORIZONTAL) && (rightLos.attackerCover == COVER_FULL)) { @@ -974,22 +990,22 @@ private static LosEffects losDivided(Game game, AttackInfo ai, boolean diagramLo rightLos.attackerCover = COVER_75RIGHT; losBlockedByCover = true; } - + // In the case of vertical and 75% cover, LoS will be blocked. // We need to unblock it, unless Los is already blocked. if (!los.blocked && (!leftLos.blocked || !rightLos.blocked) && - losBlockedByCover) { + losBlockedByCover) { leftLos.blocked = false; rightLos.blocked = false; - } + } } totalLeftLos.add(leftLos); - totalRightLos.add(rightLos); + totalRightLos.add(rightLos); } //Determine whether left or right is worse and update los with it int lVal = totalLeftLos.losModifiers(game).getValue(); int rVal = totalRightLos.losModifiers(game).getValue(); - if ((lVal > rVal) || + if ((lVal > rVal) || ((lVal == rVal) && totalLeftLos.isAttackerCover())) { los.add(totalLeftLos); } else { @@ -997,17 +1013,17 @@ private static LosEffects losDivided(Game game, AttackInfo ai, boolean diagramLo } return los; } - + /** * Convenience method for setting the secondary cover values. The left LoS - * has retains it's primary cover, and its secondary cover becomes the - * primary of the right los while the right los has its primary become + * has retains it's primary cover, and its secondary cover becomes the + * primary of the right los while the right los has its primary become * secondary and its primary becomes the primary of the left side. * This ensures that the primary protects the left side and the secondary * protects the right side which is important to determine which to pick * later on when damage is handled. - * - * @param leftLos The left side of the line of sight for a divided hex + * + * @param leftLos The left side of the line of sight for a divided hex * LoS computation * @param rightLos The right side of the line of sight for a dividied hex * LoS computation @@ -1017,7 +1033,7 @@ private static void setSecondaryCover(LosEffects leftLos, LosEffects rightLos) { leftLos.setDamagableCoverTypeSecondary(rightLos.getDamagableCoverTypePrimary()); leftLos.setCoverBuildingSecondary(rightLos.getCoverBuildingPrimary()); leftLos.setCoverDropshipSecondary(rightLos.getCoverDropshipPrimary()); - leftLos.setCoverLocSecondary(rightLos.getCoverLocPrimary()); + leftLos.setCoverLocSecondary(rightLos.getCoverLocPrimary()); //Set right secondary to right primary rightLos.setDamagableCoverTypeSecondary(rightLos.getDamagableCoverTypePrimary()); rightLos.setCoverBuildingSecondary(rightLos.getCoverBuildingPrimary()); @@ -1026,7 +1042,7 @@ private static void setSecondaryCover(LosEffects leftLos, LosEffects rightLos) { //Set right primary to left primary rightLos.setDamagableCoverTypePrimary(leftLos.getDamagableCoverTypePrimary()); rightLos.setCoverBuildingPrimary(leftLos.getCoverBuildingPrimary()); - rightLos.setCoverDropshipPrimary(leftLos.getCoverDropshipPrimary()); + rightLos.setCoverDropshipPrimary(leftLos.getCoverDropshipPrimary()); rightLos.setCoverLocPrimary(leftLos.getCoverLocPrimary()); } @@ -1035,7 +1051,7 @@ private static void setSecondaryCover(LosEffects leftLos, LosEffects rightLos) { * the specified coordinate. */ private static LosEffects losForCoords(Game game, AttackInfo ai, - Coords coords, Building thruBldg, + Coords coords, Building thruBldg, boolean diagramLoS, boolean partialCover) { LosEffects los = new LosEffects(); // ignore hexes not on board @@ -1085,55 +1101,57 @@ private static LosEffects losForCoords(Game game, AttackInfo ai, // Attacks thru a building are not blocked by that building. // ASSUMPTION: bridges don't block LOS. int bldgEl = 0; - if ((null == los.getThruBldg()) - && hex.containsTerrain(Terrains.BLDG_ELEV)) { - bldgEl = hex.terrainLevel(Terrains.BLDG_ELEV); - } - - if ((null == los.getThruBldg()) - && hex.containsTerrain(Terrains.FUEL_TANK_ELEV) - && hex.terrainLevel(Terrains.FUEL_TANK_ELEV) > bldgEl) { - bldgEl = hex.terrainLevel(Terrains.FUEL_TANK_ELEV); - } - boolean coveredByDropship = false; Entity coveringDropship = null; - // check for grounded dropships - treat like a building 10 elevations tall - if (bldgEl < 10) { - for (Entity inHex : game.getEntitiesVector(coords)) { - if (ai.attackerId == inHex.getId() || ai.targetId == inHex.getId()) { - continue; - } - if (inHex instanceof Dropship && !inHex.isAirborne() && !inHex.isSpaceborne()) { - bldgEl = 10; - coveredByDropship = true; - coveringDropship = inHex; + if (!ai.lowAltitude) { + if ((null == los.getThruBldg()) + && hex.containsTerrain(Terrains.BLDG_ELEV)) { + bldgEl = hex.terrainLevel(Terrains.BLDG_ELEV); + } + + if ((null == los.getThruBldg()) + && hex.containsTerrain(Terrains.FUEL_TANK_ELEV) + && hex.terrainLevel(Terrains.FUEL_TANK_ELEV) > bldgEl) { + bldgEl = hex.terrainLevel(Terrains.FUEL_TANK_ELEV); + } + + // check for grounded dropships - treat like a building 10 elevations tall + if (bldgEl < 10) { + for (Entity inHex : game.getEntitiesVector(coords)) { + if (ai.attackerId == inHex.getId() || ai.targetId == inHex.getId()) { + continue; + } + if (inHex instanceof Dropship && !inHex.isAirborne() && !inHex.isSpaceborne()) { + bldgEl = 10; + coveredByDropship = true; + coveringDropship = inHex; + } } } } - + // check for block by terrain - - - // All unit heights report as 1 less in MM than what they really are + + + // All unit heights report as 1 less in MM than what they really are // (1 for mechs, 0 for tanks...) // A level 4 hill will not block a mech on a level 3 hill - // (height of the mech in here = 3+"1" = "4"), as + // (height of the mech in here = 3+"1" = "4"), as // hill elevation is not > unit elevation (normal LOS rules) // With diagramming LOS it will block LOS as soon as the sightline // drops by 0.1 to 3.9, even though that means it would be at 4.9 in // "real" height values, so still well above the level 4 hill // Therefore we need to add 1 from the diagramming LOS elevation // to correct the calculation - // This is still hacky as Entity should simply report the real heights + // This is still hacky as Entity should simply report the real heights // and the comparison in here should follow TW/TO "higher or equal" rules. - + // The interpolated elevation for TacOps LOS diagramming - double weightedHeight = ai.targetAbsHeight * ai.attackPos.distance(coords) + double weightedHeight = ai.targetAbsHeight * ai.attackPos.distance(coords) + ai.attackAbsHeight * ai.targetPos.distance(coords); double totalDistance = ai.targetPos.distance(coords) + ai.attackPos.distance(coords); double losElevation = 1 + weightedHeight / totalDistance; - + // The higher of the attacker's height and defender's height int maxUnitHeight = Math.max(ai.attackAbsHeight, ai.targetAbsHeight); boolean attackerAdjc = ai.attackPos.distance(coords) == 1; @@ -1174,38 +1192,42 @@ private static LosEffects losForCoords(Game game, AttackInfo ai, //number of screens doesn't matter. One is enough to block los.screen++; } - //heavy industrial zones can vary in height up to 10 levels, so lets - //put all of this into a for loop - int industrialLevel = hex.terrainLevel(Terrains.INDUSTRIAL); - if (industrialLevel != Terrain.LEVEL_NONE) { - for (int level = 1; level < 11; level++) { - if ((hexEl + level > maxUnitHeight) - || ((hexEl + level > ai.attackAbsHeight) && attackerAdjc) - || ((hexEl + level > ai.targetAbsHeight) && targetAdjc)) { - // check industrial zone - if (industrialLevel == level) { - los.heavyIndustrial++; + + if (!ai.lowAltitude) { + //heavy industrial zones can vary in height up to 10 levels, so lets + //put all of this into a for loop + int industrialLevel = hex.terrainLevel(Terrains.INDUSTRIAL); + if (industrialLevel != Terrain.LEVEL_NONE) { + for (int level = 1; level < 11; level++) { + if ((hexEl + level > maxUnitHeight) + || ((hexEl + level > ai.attackAbsHeight) && attackerAdjc) + || ((hexEl + level > ai.targetAbsHeight) && targetAdjc)) { + // check industrial zone + if (industrialLevel == level) { + los.heavyIndustrial++; + } } } } - } - //planted fields only rise one level above the terrain - if (hex.containsTerrain(Terrains.FIELDS)) { - if (((hexEl + 1 > ai.attackAbsHeight) && (hexEl + 2 > ai.targetAbsHeight)) - || ((hexEl + 1 > ai.attackAbsHeight) && attackerAdjc) - || ((hexEl + 1 > ai.targetAbsHeight) && targetAdjc)) { - los.plantedFields++; + //planted fields only rise one level above the terrain + if (hex.containsTerrain(Terrains.FIELDS)) { + if (((hexEl + 1 > ai.attackAbsHeight) && (hexEl + 2 > ai.targetAbsHeight)) + || ((hexEl + 1 > ai.attackAbsHeight) && attackerAdjc) + || ((hexEl + 1 > ai.targetAbsHeight) && targetAdjc)) { + los.plantedFields++; + } } } - + + // Intervening Smoke and Woods - int smokeLevel = hex.terrainLevel(Terrains.SMOKE); int woodsLevel = hex.terrainLevel(Terrains.WOODS); int jungleLevel = hex.terrainLevel(Terrains.JUNGLE); int foliageElev = hex.terrainLevel(Terrains.FOLIAGE_ELEV); + int smokeLevel = hex.terrainLevel(Terrains.SMOKE); boolean hasFoliage = (woodsLevel != Terrain.LEVEL_NONE) || (jungleLevel != Terrain.LEVEL_NONE); - + // Check 1 level high woods and jungle if (hasFoliage && foliageElev == 1) { int terrainEl = hexEl + 1; @@ -1226,15 +1248,15 @@ private static LosEffects losForCoords(Game game, AttackInfo ai, } } } - + // Intervening smoke and elevation 2 light/heavy woods/jungle - if (smokeLevel != Terrain.LEVEL_NONE + if (smokeLevel != Terrain.LEVEL_NONE || (hasFoliage && foliageElev > 1)) { int terrainEl = hexEl + 2; if (diagramLoS) { affectsLos = terrainEl >= losElevation; } else { - affectsLos = (terrainEl > maxUnitHeight) + affectsLos = (terrainEl > maxUnitHeight) || ((terrainEl > ai.attackAbsHeight) && attackerAdjc) || ((terrainEl > ai.targetAbsHeight) && targetAdjc); } @@ -1259,13 +1281,13 @@ private static LosEffects losForCoords(Game game, AttackInfo ai, los.heavyWoods++; } } - + // Ultra woods/jungle rise 3 levels above the hex level terrainEl = hexEl + 3; if (diagramLoS) { affectsLos = terrainEl >= losElevation; } else { - affectsLos = (terrainEl > maxUnitHeight) + affectsLos = (terrainEl > maxUnitHeight) || ((terrainEl > ai.attackAbsHeight) && attackerAdjc) || ((terrainEl > ai.targetAbsHeight) && targetAdjc); } @@ -1277,48 +1299,51 @@ private static LosEffects losForCoords(Game game, AttackInfo ai, } } - // Partial Cover related code - boolean potentialCover = false; - // check for target partial cover - if (targetAdjc && ai.targetIsMech) { - if (los.blocked && partialCover) { - los.targetCover = COVER_FULL; - potentialCover = true; - } else if ((totalEl == ai.targetAbsHeight) - && (ai.attackAbsHeight <= ai.targetAbsHeight) - && (ai.targetHeight > 0)) { - los.targetCover |= COVER_HORIZONTAL; - potentialCover = true; + if (!ai.lowAltitude) { + // Partial Cover related code + boolean potentialCover = false; + // check for target partial cover + if (targetAdjc && ai.targetIsMech) { + if (los.blocked && partialCover) { + los.targetCover = COVER_FULL; + potentialCover = true; + } else if ((totalEl == ai.targetAbsHeight) + && (ai.attackAbsHeight <= ai.targetAbsHeight) + && (ai.targetHeight > 0)) { + los.targetCover |= COVER_HORIZONTAL; + potentialCover = true; + } } - } - // check for attacker partial (horizontal) cover - if (attackerAdjc && ai.attackerIsMech) { - if (los.blocked && partialCover) { - los.attackerCover = COVER_FULL; - potentialCover = true; - } else if ((totalEl == ai.attackAbsHeight) - && (ai.attackAbsHeight >= ai.targetAbsHeight) - && (ai.attackHeight > 0)) { - los.attackerCover |= COVER_HORIZONTAL; - potentialCover = true; + // check for attacker partial (horizontal) cover + if (attackerAdjc && ai.attackerIsMech) { + if (los.blocked && partialCover) { + los.attackerCover = COVER_FULL; + potentialCover = true; + } else if ((totalEl == ai.attackAbsHeight) + && (ai.attackAbsHeight >= ai.targetAbsHeight) + && (ai.attackHeight > 0)) { + los.attackerCover |= COVER_HORIZONTAL; + potentialCover = true; + } } - } - - // If there's a partial cover situation, we may need to keep track of - // damageable assets that are providing cover, so we can damage them if - // they block a shot. - if (potentialCover) { - if (coveredByDropship) { - los.setDamagableCoverTypePrimary(DAMAGABLE_COVER_DROPSHIP); - los.coverDropshipPrimary = coveringDropship; - } else if (bldg != null) { - los.setDamagableCoverTypePrimary(DAMAGABLE_COVER_BUILDING); - los.coverBuildingPrimary = bldg; - } else { - los.setDamagableCoverTypePrimary(DAMAGABLE_COVER_NONE); + + // If there's a partial cover situation, we may need to keep track of + // damageable assets that are providing cover, so we can damage them if + // they block a shot. + if (potentialCover) { + if (coveredByDropship) { + los.setDamagableCoverTypePrimary(DAMAGABLE_COVER_DROPSHIP); + los.coverDropshipPrimary = coveringDropship; + } else if (bldg != null) { + los.setDamagableCoverTypePrimary(DAMAGABLE_COVER_BUILDING); + los.coverBuildingPrimary = bldg; + } else { + los.setDamagableCoverTypePrimary(DAMAGABLE_COVER_NONE); + } + los.coverLocPrimary = coords; } - los.coverLocPrimary = coords; - } + } + return los; } @@ -1482,14 +1507,14 @@ private static boolean isDeadZone(Game game, AttackInfo ai) { } return false; } - - + + /** * Returns the text name of a particular type of cover, given its id. * TacOps partial cover is assigned from the perspective of the attacker, * so it's possible that the sides should be switched to make sense * from the perspective of the target. - * + * * @param cover The int id that represents the cover type. * @param switchSides A boolean that determines if left/right side should * be switched. This is useful since cover is given @@ -1528,7 +1553,7 @@ static public String getCoverName(int cover, boolean switchSides) { case COVER_HORIZONTAL: return Messages.getString("LosEffects.name_cover_horizontal"); case COVER_UPPER: - return Messages.getString("LosEffects.name_cover_upper"); + return Messages.getString("LosEffects.name_cover_upper"); case COVER_FULL: return Messages.getString("LosEffects.name_cover_full"); case COVER_75LEFT: @@ -1547,7 +1572,7 @@ static public String getCoverName(int cover, boolean switchSides) { return Messages.getString("LosEffects.name_cover_unknown"); } } - + public Building getCoverBuildingPrimary() { return coverBuildingPrimary; } @@ -1611,7 +1636,7 @@ public Coords getCoverLocSecondary() { public void setCoverLocSecondary(Coords coverLocSecondary) { this.coverLocSecondary = coverLocSecondary; } - + public boolean infantryProtected() { return infProtected; } diff --git a/megamek/src/megamek/server/GameManager.java b/megamek/src/megamek/server/GameManager.java index df130b2c0f4..2eca511c570 100644 --- a/megamek/src/megamek/server/GameManager.java +++ b/megamek/src/megamek/server/GameManager.java @@ -2104,6 +2104,8 @@ public void applyBoardSettings() { List rotateBoard = new ArrayList<>(); for (int i = 0; i < (mapSettings.getMapWidth() * mapSettings.getMapHeight()); i++) { sheetBoards[i] = new Board(); + // Need to set map type prior to loading to adjust foliage height, etc. + sheetBoards[i].setType(mapSettings.getMedium()); String name = mapSettings.getBoardsSelectedVector().get(i); boolean isRotated = false; if (name.startsWith(Board.BOARD_REQUEST_ROTATION)) { From 6827413d45918d4725acdb8b2342c40dabc3a532 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Sat, 27 Jul 2024 22:08:36 -0700 Subject: [PATCH 10/32] Remove map ingest munging and replace with elevation adjustments --- .../i18n/megamek/client/messages.properties | 1 + .../client/ui/swing/tooltip/HexTooltip.java | 17 +++++++- megamek/src/megamek/common/Board.java | 4 -- megamek/src/megamek/common/Hex.java | 39 ++++++++----------- megamek/src/megamek/common/LosEffects.java | 2 +- megamek/src/megamek/common/Terrains.java | 4 +- 6 files changed, 36 insertions(+), 31 deletions(-) diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 0328aa11b82..621e5822504 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -559,6 +559,7 @@ BoardView1.Tooltip.ZZ=ZZ #Buildings and Bridges BoardView1.Tooltip.Hex=Hex: {0} - Level: {1} +BoardView1.Tooltip.HexAlt=Hex: {0} - Altitude: {1} BoardView1.Tooltip.Building={1}
Height: {0}, CF: {2}
Armor: {3}; Basement: {4} BoardView1.Tooltip.BuildingLine=Height: {0}; CF: {1} Armor: {2} BoardView1.Tooltip.BldgBasementCollapsed=
(collapsed) diff --git a/megamek/src/megamek/client/ui/swing/tooltip/HexTooltip.java b/megamek/src/megamek/client/ui/swing/tooltip/HexTooltip.java index 34a7d9bbd02..994a7dd8bcf 100644 --- a/megamek/src/megamek/client/ui/swing/tooltip/HexTooltip.java +++ b/megamek/src/megamek/client/ui/swing/tooltip/HexTooltip.java @@ -22,6 +22,7 @@ import megamek.common.enums.BasementType; import megamek.common.planetaryconditions.IlluminationLevel; +import java.util.List; import java.util.Vector; import static megamek.client.ui.swing.util.UIUtil.*; @@ -193,16 +194,30 @@ public static String getOneLineSummary(BuildingTarget target, Board board) { } public static String getTerrainTip(Hex mhex, GUIPreferences GUIP, Game game) { + boolean inAtmosphere = game.getBoard().inAtmosphere(); Coords mcoords = mhex.getCoords(); String indicator = IlluminationLevel.determineIlluminationLevel(game, mcoords).getIndicator(); String illuminated = DOT_SPACER + guiScaledFontHTML(GUIP.getCautionColor()) + " " + indicator + ""; String result = ""; - StringBuilder sTerrain = new StringBuilder(Messages.getString("BoardView1.Tooltip.Hex", mcoords.getBoardNum(), mhex.getLevel()) + illuminated + "
"); + StringBuilder sTerrain = new StringBuilder( + Messages.getString( + (inAtmosphere) ? "BoardView1.Tooltip.HexAlt": "BoardView1.Tooltip.Hex", + mcoords.getBoardNum(), + mhex.getLevel() + ) + illuminated + "
" + ); + // Types that represent Elevations need converting and possibly zeroing if board is in Atmosphere (Low Alt.) + List typesThatNeedAltitudeChecked = List.of( + Terrains.INDUSTRIAL, Terrains.BLDG_ELEV, Terrains.BRIDGE_ELEV, Terrains.FOLIAGE_ELEV + ); // cycle through the terrains and report types found for (int terType: mhex.getTerrainTypes()) { int tf = mhex.getTerrain(terType).getTerrainFactor(); int ttl = mhex.getTerrain(terType).getLevel(); + if (typesThatNeedAltitudeChecked.contains(terType)) { + ttl = Terrains.getTerrainElevation(terType, ttl, inAtmosphere); + } String name = Terrains.getDisplayName(terType, ttl); if (name != null) { diff --git a/megamek/src/megamek/common/Board.java b/megamek/src/megamek/common/Board.java index 0e2eb855cb7..288a92cec91 100644 --- a/megamek/src/megamek/common/Board.java +++ b/megamek/src/megamek/common/Board.java @@ -1020,10 +1020,6 @@ public void load(InputStream is, @Nullable List errors, boolean continue args[i++] = st.ttype == StreamTokenizer.TT_NUMBER ? (int) st.nval + "" : st.sval; } int elevation = Integer.parseInt(args[1]); - if (mapType == T_ATMOSPHERE) { - // All foliage on low-altitude maps have height 1 - args[2] = args[2].replaceAll("foliage_elev:\\d{1,}", "foliage_elev:1"); - } // The coordinates in the .board file are ignored! nd[index] = new Hex(elevation, args[2], args[3], new Coords(index % nw, index / nw)); index++; diff --git a/megamek/src/megamek/common/Hex.java b/megamek/src/megamek/common/Hex.java index 81af514a622..5f7449ebf54 100644 --- a/megamek/src/megamek/common/Hex.java +++ b/megamek/src/megamek/common/Hex.java @@ -15,16 +15,9 @@ import megamek.common.annotations.Nullable; import megamek.common.enums.BasementType; -import org.apache.logging.log4j.LogManager; -import java.awt.*; -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.StringSelection; -import java.awt.datatransfer.Transferable; import java.io.Serializable; import java.util.*; -import java.util.List; import java.util.stream.Collectors; /** @@ -187,10 +180,10 @@ public void setExits(Hex other, int direction, boolean roadsAutoExit) { } cTerr.setExit(direction, cTerr.exitsTo(oTerr)); - + // Water gets a special treatment: Water at the board edge - // (hex == null) should usually look like ocean and - // therefore always gets connection to outside the board + // (hex == null) should usually look like ocean and + // therefore always gets connection to outside the board if ((cTerr.getType() == Terrains.WATER) && (other == null)) { cTerr.setExit(direction, true); } @@ -601,12 +594,12 @@ public void getUnstuckModifier(int elev, PilotingRollData rollTarget) { terrain.getUnstuckModifier(elev, rollTarget); } } - - /** + + /** * True if this hex has a clifftop towards otherHex. This hex * must have the terrain CLIFF_TOP, it must have exits * specified (exits set to active) for the CLIFF_TOP terrain, - * and must have an exit in the direction of otherHex. + * and must have an exit in the direction of otherHex. */ public boolean hasCliffTopTowards(Hex otherHex) { return containsTerrain(Terrains.CLIFF_TOP) @@ -619,10 +612,10 @@ public Coords getCoords() { return coords; } - /** + /** * Sets the coords of this hex. DO NOT USE outside board.java! - * WILL NOT MOVE THE HEX. Only the position of the hex in the - * board's data[] determines the actual location of the hex. + * WILL NOT MOVE THE HEX. Only the position of the hex in the + * board's data[] determines the actual location of the hex. */ public void setCoords(Coords c) { coords = c; @@ -685,10 +678,10 @@ && containsTerrain(Terrains.FOLIAGE_ELEV)) { int wl = terrainLevel(Terrains.WOODS); int jl = terrainLevel(Terrains.JUNGLE); int el = terrainLevel(Terrains.FOLIAGE_ELEV); - + boolean isLightOrHeavy = wl == 1 || jl == 1 || wl == 2 || jl == 2; boolean isUltra = wl == 3 || jl == 3; - + if (! ((el == 1) || (isLightOrHeavy && el == 2) || (isUltra && el == 3))) { newErrors.add("Foliage elevation is wrong, must be 1 or 2 for Light/Heavy and 1 or 3 for Ultra Woods/Jungle."); } @@ -697,9 +690,9 @@ && containsTerrain(Terrains.FOLIAGE_ELEV)) { && containsTerrain(Terrains.FOLIAGE_ELEV)) { newErrors.add("Woods and Jungle Elevation terrain present without Woods or Jungle."); } - + // Buildings must have at least BUILDING, BLDG_ELEV and BLDG_CF - if (containsAnyTerrainOf(Terrains.BUILDING, Terrains.BLDG_ELEV, Terrains.BLDG_CF, Terrains.BLDG_FLUFF, + if (containsAnyTerrainOf(Terrains.BUILDING, Terrains.BLDG_ELEV, Terrains.BLDG_CF, Terrains.BLDG_FLUFF, Terrains.BLDG_ARMOR, Terrains.BLDG_CLASS, Terrains.BLDG_BASE_COLLAPSED, Terrains.BLDG_BASEMENT_TYPE) && !containsAllTerrainsOf(Terrains.BUILDING, Terrains.BLDG_ELEV, Terrains.BLDG_CF)) { newErrors.add("Incomplete Building! A hex with any building terrain must at least contain " @@ -714,14 +707,14 @@ && containsTerrain(Terrains.FOLIAGE_ELEV)) { } // Fuel Tanks must have all of FUEL_TANK, _ELEV, _CF and _MAGN - if (containsAnyTerrainOf(Terrains.FUEL_TANK, Terrains.FUEL_TANK_CF, + if (containsAnyTerrainOf(Terrains.FUEL_TANK, Terrains.FUEL_TANK_CF, Terrains.FUEL_TANK_ELEV, Terrains.FUEL_TANK_MAGN) - && !containsAllTerrainsOf(Terrains.FUEL_TANK, Terrains.FUEL_TANK_CF, + && !containsAllTerrainsOf(Terrains.FUEL_TANK, Terrains.FUEL_TANK_CF, Terrains.FUEL_TANK_ELEV, Terrains.FUEL_TANK_MAGN)) { newErrors.add("Incomplete Fuel Tank! A hex with any fuel tank terrain must contain " + "the fuel tank type, elevation, CF and the fuel tank magnitude."); } - + if (containsAllTerrainsOf(Terrains.FUEL_TANK, Terrains.BUILDING)) { newErrors.add("A hex cannot have both a Building and a Fuel Tank."); } diff --git a/megamek/src/megamek/common/LosEffects.java b/megamek/src/megamek/common/LosEffects.java index e4d60c49361..35fa215426d 100644 --- a/megamek/src/megamek/common/LosEffects.java +++ b/megamek/src/megamek/common/LosEffects.java @@ -1224,7 +1224,7 @@ private static LosEffects losForCoords(Game game, AttackInfo ai, // Intervening Smoke and Woods int woodsLevel = hex.terrainLevel(Terrains.WOODS); int jungleLevel = hex.terrainLevel(Terrains.JUNGLE); - int foliageElev = hex.terrainLevel(Terrains.FOLIAGE_ELEV); + int foliageElev = Terrains.getTerrainElevation(Terrains.FOLIAGE_ELEV, hex.terrainLevel(Terrains.FOLIAGE_ELEV), ai.lowAltitude); int smokeLevel = hex.terrainLevel(Terrains.SMOKE); boolean hasFoliage = (woodsLevel != Terrain.LEVEL_NONE) || (jungleLevel != Terrain.LEVEL_NONE); diff --git a/megamek/src/megamek/common/Terrains.java b/megamek/src/megamek/common/Terrains.java index 41c6976cda6..e7bad47cfd5 100644 --- a/megamek/src/megamek/common/Terrains.java +++ b/megamek/src/megamek/common/Terrains.java @@ -57,7 +57,6 @@ public class Terrains implements Serializable { // LI smoke 4: Heavy LI smoke public static final int GEYSER = 21; // 1: dormant 2: active 3: magma vent // unimplemented - // Black Ice // Bug Storm // Extreme Depths // Hazardous Liquid Pools @@ -476,7 +475,8 @@ public static int getTerrainFactor(int type, int level) { /** * Returns the number of elevations or altitudes above the hex level a given - * terrainType rises. + * terrainType rises. Has to be explicit about the *_ELEV values, because _everything else_ + * that comes through here is a "level", a ranking of "denseness", not an elevation _or_ altitude. * * @param terrainType this specifies the type of terrain to get the information for * @param inAtmosphere From 36da6759690fb0cadba1fbdc2b88bedc590b5630 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Sat, 27 Jul 2024 22:48:26 -0700 Subject: [PATCH 11/32] Prevent NPE when removing forces from Force that exists but isn not registered --- megamek/src/megamek/common/force/Forces.java | 257 ++++++++++--------- 1 file changed, 131 insertions(+), 126 deletions(-) diff --git a/megamek/src/megamek/common/force/Forces.java b/megamek/src/megamek/common/force/Forces.java index ba0c09c6508..7bb754b7fa0 100644 --- a/megamek/src/megamek/common/force/Forces.java +++ b/megamek/src/megamek/common/force/Forces.java @@ -15,7 +15,7 @@ * * You should have received a copy of the GNU General Public License * along with MegaMek. If not, see . - */ + */ package megamek.common.force; @@ -36,26 +36,26 @@ /** * Manages a collection of Forces for a game. The game only needs to hold one Forces object. * Like in Campaign.java in MHQ this is mainly a map of id to Force along with many utility functions. - * Force management and changes are directed through this object. - * + * Force management and changes are directed through this object. + * * @author Simon (Juliez) */ public final class Forces implements Serializable { private static final long serialVersionUID = -1382468145554363945L; - + private final HashMap forces = new HashMap<>(); private transient IGame game; - + public Forces(IGame g) { game = g; } - - /** + + /** * Adds a top-level force with the provided name and the provided owner. Verifies the name * and the force before applying the change. - * Returns the id of the newly created Force or Force.NO_FORCE if no force was - * created. + * Returns the id of the newly created Force or Force.NO_FORCE if no force was + * created. */ public synchronized int addTopLevelForce(final Force force, final @Nullable Player owner) { if (!verifyForceName(force.getName()) || (owner == null)) { @@ -67,7 +67,7 @@ public synchronized int addTopLevelForce(final Force force, final @Nullable Play return newId; } - /** + /** * Adds the provided subforce to the provided parent. Verifies the name and the force before * applying the change. * @return the id of the newly created Force or Force.NO_FORCE if no new subforce was created. @@ -92,45 +92,45 @@ public List forcelessEntities() { .filter(e -> !e.partOfForce()) .collect(toList()); } - + /** Returns the number of top-level forces present, i.e. forces with no parent force. */ public int getTopLevelForceCount() { return getTopLevelForces().size(); } - + /** Returns a List of the top-level forces. */ public List getTopLevelForces() { return forces.values().stream().filter(Force::isTopLevel).collect(toList()); } - - /** + + /** * Returns true if the provided force is part of the present forces either - * as a top-level or a subforce. + * as a top-level or a subforce. */ public boolean contains(@Nullable Force force) { return (force != null) && forces.containsValue(force); } - - /** + + /** * Returns true if the provided forceId is part of the present forces either - * as a top-level or a subforce. + * as a top-level or a subforce. */ public boolean contains(int forceId) { return forces.containsKey(forceId); } - - /** - * Returns the force with the given force id or null if there is no force with this id. + + /** + * Returns the force with the given force id or null if there is no force with this id. */ public Force getForce(int id) { return forces.get(id); } - + /** * Adds the provided Entity to the provided force. Does nothing if the force doesn't exist * or if the entity is already in the targeted force. Removes the entity from any former force. * Returns a list of all changed forces, i.e. the former force, if any, and the new force. - * The list will be empty if no actual change occurred. + * The list will be empty if no actual change occurred. */ public ArrayList addEntity(ForceAssignable entity, int forceId) { ArrayList result = new ArrayList<>(); @@ -142,7 +142,7 @@ public ArrayList addEntity(ForceAssignable entity, int forceId) { if (formerForce == forceId) { return result; } - + forces.get(forceId).addEntity(entity); entity.setForceId(forceId); result.add(getForce(forceId)); @@ -153,10 +153,10 @@ public ArrayList addEntity(ForceAssignable entity, int forceId) { return result; } - /** + /** * Removes the provided entities from their current forces, if any. Does nothing if an entity * is already force-less (forceId == Force.NO_FORCE). Returns a list of all changed forces. - * The list will be empty if no actual change occurred. + * The list will be empty if no actual change occurred. */ public synchronized LinkedHashSet removeEntityFromForces(Collection entities) { LinkedHashSet result = new LinkedHashSet<>(); @@ -166,12 +166,12 @@ public synchronized LinkedHashSet removeEntityFromForces(Collection removeEntityFromForces(ForceAssignable entity) { ArrayList result = new ArrayList<>(); @@ -180,21 +180,26 @@ public synchronized ArrayList removeEntityFromForces(ForceAssignable enti return result; } entity.setForceId(NO_FORCE); - + if (contains(formerForce)) { - result.add(getForce(formerForce)); - getForce(formerForce).removeEntity(entity); + Force former = getForce(formerForce); + if (former != null) { + result.add(former); + former.removeEntity(entity); + } else { + LogManager.getLogger().warn("Removing entity from Force that has not yet been registered!"); + } } else { LogManager.getLogger().warn("Removed entity from non-existent force!"); } return result; } - - /** + + /** * Removes the provided entity ID from its current force, if any. Does nothing if the entity - * is already force-less (forceId == Force.NO_FORCE). + * is already force-less (forceId == Force.NO_FORCE). * Returns a list of all changed forces, i.e. the former force, if any. - * The list will be empty if no actual change occurred. + * The list will be empty if no actual change occurred. */ public synchronized ArrayList removeEntityFromForces(int entityId) { ArrayList result = new ArrayList<>(); @@ -215,8 +220,8 @@ public synchronized ArrayList removeEntityFromForces(int entityId) { } return result; } - - /** + + /** * Renames the Force with forceId to the provided name. The provided values are * fully verified before applying the change. A null name or empty name can safely * be passed. Duplicate names may be given; forces are identified via id. @@ -228,8 +233,8 @@ public void renameForce(String name, int forceId) { Force force = forces.get(forceId); force.setName(name); } - - /** + + /** * Returns true if the provided name can be used as a Force name. It cannot * be empty or contain "|" or "\". */ @@ -237,49 +242,49 @@ public boolean verifyForceName(String name) { return name != null && !name.isBlank() && !name.contains("|") && !name.contains("\\"); } - /** + /** * @return the force owner's id */ public int getOwnerId(Force force) { return force.getOwnerId(); } - /** + /** * @return the owner of this force. */ public Player getOwner(Force force) { return game.getPlayer(getOwnerId(force)); } - - /** + + /** * @return the owner of this force. */ public Player getOwner(int forceId) { return getOwner(getForce(forceId)); } - - /** + + /** * Returns the Force that the provided entity is a direct part of. * E.g., If it is part of a lance in a company, the lance will be returned. - * If it is part of no force, returns null. + * If it is part of no force, returns null. */ public @Nullable Force getForce(final ForceAssignable entity) { return forces.get(getForceId(entity.getId())); } - /** + /** * Returns the id of the force that the provided entity is a direct part of. * E.g., If it is part of a lance in a company, the lance id will be returned. - * If it is part of no force, returns Force.NO_FORCE. + * If it is part of no force, returns Force.NO_FORCE. */ public int getForceId(ForceAssignable entity) { return getForceId(entity.getId()); } - - /** + + /** * Returns the id of the force that the provided entity (id) is a direct part of. * E.g., If it is part of a lance in a company, the lance id will be returned. - * If it is part of no force, returns Force.NO_FORCE. + * If it is part of no force, returns Force.NO_FORCE. */ public int getForceId(int id) { for (Force force: forces.values()) { @@ -291,10 +296,10 @@ public int getForceId(int id) { } /** - * Parses the force string of the provided entity. + * Parses the force string of the provided entity. * Returns a List of Force stubs in the order of highest to lowest force. - * The Force stubs cannot be added to a Forces object directly! They - * contain only the name and id and have no parent and no owner and + * The Force stubs cannot be added to a Forces object directly! They + * contain only the name and id and have no parent and no owner and * the passed entity is not added to them! */ public static List parseForceString(ForceAssignable entity) { @@ -321,9 +326,9 @@ public static List parseForceString(ForceAssignable entity) { return forces; } - /** + /** * Returns a list of all subforces of the provided force, including - * subforces of subforces to any depth. + * subforces of subforces to any depth. */ public ArrayList getFullSubForces(Force force) { ArrayList result = new ArrayList<>(); @@ -335,9 +340,9 @@ public ArrayList getFullSubForces(Force force) { } return result; } - - /** - * For the given player, returns a list of forces that are his own or belong to his team. + + /** + * For the given player, returns a list of forces that are his own or belong to his team. */ public ArrayList getAvailableForces(Player player) { ArrayList result = new ArrayList<>(); @@ -349,12 +354,12 @@ public ArrayList getAvailableForces(Player player) { } return result; } - + private boolean isAvailable(Force force, Player player) { Player owner = game.getPlayer(getOwnerId(force)); return (owner != null) && !owner.isEnemyOf(player); } - + public String forceStringFor(final ForceAssignable entity) { final StringBuilder result = new StringBuilder(); for (final Force ancestor : forceChain(entity)) { @@ -366,10 +371,10 @@ public String forceStringFor(final ForceAssignable entity) { } return result.toString(); } - - /** + + /** * Returns a ArrayList of Forces that make up the chain of forces to the provided entity. - * The list starts with the top-level force containing the entity and ends with + * The list starts with the top-level force containing the entity and ends with * the force that the entity is an immediate member of. */ public ArrayList forceChain(ForceAssignable entity) { @@ -379,10 +384,10 @@ public ArrayList forceChain(ForceAssignable entity) { return new ArrayList<>(); } } - - /** + + /** * Returns a ArrayList of Forces that make up the chain of forces to the provided force. - * The list starts with the top-level force containing the provided force and ends with + * The list starts with the top-level force containing the provided force and ends with * (includes!) the provided force itself. */ public ArrayList forceChain(Force force) { @@ -393,7 +398,7 @@ public ArrayList forceChain(Force force) { result.add(force); return result; } - + /** Return a free Force id. */ private int newId() { int result = 0; @@ -402,24 +407,24 @@ private int newId() { } return result; } - - /** + + /** * Overwrites the previous force mapped to forceId with the provided force - * or adds it if no force was present with that forceId. - * Only used when the server sends force updates. + * or adds it if no force was present with that forceId. + * Only used when the server sends force updates. */ public void replace(int forceId, Force force) { forces.put(forceId, force); } - - /** + + /** * Sets the game reference to the provided Game. Used when transferring * the forces between client and server. */ public void setGame(IGame g) { game = g; } - + /** Returns a clone of this Forces object, including clones of all contained forces. */ @Override public Forces clone() { @@ -430,16 +435,16 @@ public Forces clone() { return clone; } - /** - * Returns true if this Forces object is valid. + /** + * Returns true if this Forces object is valid. * @see #isValid(Collection) */ public boolean isValid() { return isValid(new ArrayList<>()); } - - /** + + /** * Returns true if this Forces object is valid. If any updatedEntities are given, * the validity check will test these instead of the current game's. * @see #isValid() @@ -452,7 +457,7 @@ public boolean isValid(Collection updatedEntities) { if (entry.getKey() != entry.getValue().getId()) { return false; } - + // Create a copy of the game's entity list and overwrite with the given entities LinkedHashMap allEntities = new LinkedHashMap<>(); List forceRelevantGameObjects = game.getInGameObjects().stream() @@ -464,14 +469,14 @@ public boolean isValid(Collection updatedEntities) { for (ForceAssignable entity: updatedEntities) { allEntities.put(entity.getId(), entity); } - + // check if all entities exist/live // check if owner exists // check if entities match owners/team // check if no entity is contained twice // check if entity.forceId matches forceId for (int entityId: entry.getValue().getEntities()) { - if (!allEntities.containsKey(entityId) + if (!allEntities.containsKey(entityId) || game.getPlayer(getOwnerId(entry.getValue())) == null || game.getPlayer(allEntities.get(entityId).getOwnerId()).isEnemyOf(game.getPlayer(getOwnerId(entry.getValue()))) || !entIds.add(entityId) @@ -484,7 +489,7 @@ public boolean isValid(Collection updatedEntities) { // check if subforces agree on the parent // check if subforces and parents share teams for (int subforceId: entry.getValue().getSubForces()) { - if (!contains(subforceId) + if (!contains(subforceId) || !subIds.add(subforceId) || entry.getKey() != getForce(subforceId).getParentId() || getOwner(getForce(subforceId)).isEnemyOf(getOwner(entry.getValue()))) { @@ -492,7 +497,7 @@ public boolean isValid(Collection updatedEntities) { } } } - // check if no circular parents + // check if no circular parents Set forceIds = new TreeSet<>(forces.keySet()); for (Force toplevel: getTopLevelForces()) { for (Force subforce: getFullSubForces(toplevel)) { @@ -505,8 +510,8 @@ public boolean isValid(Collection updatedEntities) { } return true; } - - /** + + /** * Corrects this Forces object as much as possible. Also corrects entities * when necessary (wrong forceId). *
    @@ -576,8 +581,8 @@ public void correct() { } } } - - /** + + /** * Removes the given force from these forces if it is empty. Returns a list * of affected forces which contains the parent if the deleted force was a subforce * and is empty otherwise. @@ -595,12 +600,12 @@ public ArrayList deleteForce(int forceId) { } return result; } - - /** + + /** * Removes the given forces and all their subforces from these Forces. Returns a list * of affected surviving forces. This method does not check if the forces are empty. *

    NOTE: Any entities in the removed forces are NOT updated by this method! - * It is necessary to update any entities' forceId unless these are deleted as well. + * It is necessary to update any entities' forceId unless these are deleted as well. */ public ArrayList deleteForces(Collection delForces) { ArrayList result = new ArrayList<>(); @@ -625,8 +630,8 @@ public ArrayList deleteForces(Collection delForces) { public ArrayList getAllForces() { return new ArrayList<>(forces.values()); } - - /** + + /** * Attaches a force to a new parent. The new parent force cannot be a subforce * of the force and cannot belong to an enemy of its owner. * Returns a list of affected forces. This may be empty and may contain up to @@ -636,7 +641,7 @@ public ArrayList attachForce(Force force, Force newParent) { ArrayList result = new ArrayList<>(); Player forceOwner = game.getPlayer(getOwnerId(force)); Player parentOwner = game.getPlayer(getOwnerId(newParent)); - if (isSubForce(force, newParent) || forceOwner == null + if (isSubForce(force, newParent) || forceOwner == null || forceOwner.isEnemyOf(parentOwner) || force.getParentId() == newParent.getId()) { return result; } @@ -653,15 +658,15 @@ public ArrayList attachForce(Force force, Force newParent) { result.add(newParent); return result; } - + /** Returns true when possibleSubForce is one of the subforces (in any depth) of the given force. */ public boolean isSubForce(Force force, Force possibleSubForce) { return getFullSubForces(force).contains(possibleSubForce); } - - /** - * Promotes a force to top-level (unattaches it from its parent force if it has one). - * Returns a list of affected forces which may be empty. + + /** + * Promotes a force to top-level (unattaches it from its parent force if it has one). + * Returns a list of affected forces which may be empty. */ public ArrayList promoteForce(Force force) { ArrayList result = new ArrayList<>(); @@ -676,18 +681,18 @@ public ArrayList promoteForce(Force force) { } return result; } - - /** + + /** * Changes the owner of the given force to the given newOwner without affecting - * anything else. Will only do something if the new owner is a teammate of the - * present owner. Returns a list of affected forces containing the force if it + * anything else. Will only do something if the new owner is a teammate of the + * present owner. Returns a list of affected forces containing the force if it * was changed, empty otherwise. */ public ArrayList assignForceOnly(Force force, Player newOwner) { ArrayList result = new ArrayList<>(); if (getOwner(force).isEnemyOf(newOwner)) { LogManager.getLogger().error("Tried to reassign a force without units to an enemy."); - return result; + return result; } if (getOwnerId(force) != newOwner.getId()) { force.setOwnerId(newOwner.getId()); @@ -695,10 +700,10 @@ public ArrayList assignForceOnly(Force force, Player newOwner) { } return result; } - - /** - * Changes the owner of the given force and all subforces to the given newOwner. - * Promotes the force to top-level if the parent force is now an enemy force. + + /** + * Changes the owner of the given force and all subforces to the given newOwner. + * Promotes the force to top-level if the parent force is now an enemy force. * Returns a list of affected forces. */ public Set assignFullForces(Force force, Player newOwner) { @@ -725,17 +730,17 @@ public Set assignFullForces(Force force, Player newOwner) { } } } - return result; + return result; } - + @Override public String toString() { List forceStrings = forces.values().stream().map(Force::toString).collect(toList()); return String.join("\n", forceStrings); } - - /** - * Returns a list of all entities of the given force and all its subforces to any depth. + + /** + * Returns a list of all entities of the given force and all its subforces to any depth. */ public List getFullEntities(final @Nullable Force force) { final List result = new ArrayList<>(); @@ -750,10 +755,10 @@ public List getFullEntities(final @Nullable Force force) { } return result; } - + /** * Moves up the given entity in the list of entities of its force if possible. - * Returns true when an actual change occurred. + * Returns true when an actual change occurred. */ public ArrayList moveUp(ForceAssignable entity) { ArrayList result = new ArrayList<>(); @@ -765,10 +770,10 @@ public ArrayList moveUp(ForceAssignable entity) { } return result; } - - /** + + /** * Moves down the given entity in the list of entities of its force if possible. - * Returns true when an actual change occurred. + * Returns true when an actual change occurred. */ public ArrayList moveDown(ForceAssignable entity) { ArrayList result = new ArrayList<>(); @@ -780,10 +785,10 @@ public ArrayList moveDown(ForceAssignable entity) { } return result; } - - /** + + /** * Moves up the given subforce in the list of subforces of its parent if possible. - * Returns true when an actual change occurred. + * Returns true when an actual change occurred. */ public ArrayList moveUp(Force subForce) { ArrayList result = new ArrayList<>(); @@ -795,10 +800,10 @@ public ArrayList moveUp(Force subForce) { } return result; } - - /** + + /** * Moves down the given subforce in the list of subforces of its parent if possible. - * Returns true when an actual change occurred. + * Returns true when an actual change occurred. */ public ArrayList moveDown(Force subForce) { ArrayList result = new ArrayList<>(); From 9bdac291826de9b8cc0201c98672ababc14d0263 Mon Sep 17 00:00:00 2001 From: SJuliez Date: Sun, 28 Jul 2024 08:34:53 +0200 Subject: [PATCH 12/32] Update history.txt --- megamek/docs/history.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/megamek/docs/history.txt b/megamek/docs/history.txt index 00a5c6c5a03..d54996c426b 100644 --- a/megamek/docs/history.txt +++ b/megamek/docs/history.txt @@ -56,6 +56,13 @@ MEGAMEK VERSION HISTORY: + PR #5791: Added much-needed whitespace to some menu items of the board right click menu + PR #5798: Fix Reflective armor not appearing in MML dropdown + Fix #5803: Removes a number of duplicated infantry weapons ++ Fix #5795: Terrain altitude rules and line of sight are now implemented for Low Altitude maps ++ Fix #5790: The combat computer heat report message is now clearer, indicating that the heat bonus has already been applied in the reported values ++ Fix #5740: Lobby copy-paste will now also work for units that have no model name ++ Fix #5789: Prevent errors with showing the firing arcs ++ Fix #5793: Units are no longer able to be unloaded without regard for the bay door limits ++ Fix #5611: Prevent NPE when removing forces from Force that exists but is not registered yet (when quickly closing MM after an MHQ scenario start) + 0.49.20 (2024-06-28 2100 UTC) (THIS IS THE LAST VERSION TO SUPPORT JAVA 11) + PR #5281, #5327, #5308, #5336, #5318, #5383, #5369, #5384, #5455, #5505, #5541: Code internals: preparatory work for supporting game types such as SBF, code cleanup for string drawing, superclass change for BoardView, From c37b4d20a495db8d153e45dc6ed5059ac342a4b4 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Sun, 28 Jul 2024 00:02:00 -0700 Subject: [PATCH 13/32] Prevent NPE by using interface rather than Aero casting --- megamek/src/megamek/client/ui/swing/MovementDisplay.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index 614d303fb17..f5efe6e0421 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -4422,13 +4422,14 @@ public void computeAeroMovementEnvelope(Entity entity) { } // Increment the entity's delta-v then compute the movement envelope. - Aero ae = (Aero)entity; + // LAM and Aeros both implement this interface + IAero ae = (IAero) entity; int currentVelocity = ae.getCurrentVelocity(); ae.setCurrentVelocity(cmd.getFinalVelocity()); // Refresh the new velocity envelope on the map. try { - computeMovementEnvelope(ae); + computeMovementEnvelope(entity); updateMove(); } catch (Exception e) { LogManager.getLogger().error("An error occured trying to compute the move envelope for an Aero."); From 9fccad749c2a5dec205480385e9a8d2e59c3c74d Mon Sep 17 00:00:00 2001 From: sleet01 Date: Sun, 28 Jul 2024 01:11:40 -0700 Subject: [PATCH 14/32] Safety check for entities with no hex location, e.g. just-loaded infantry --- megamek/src/megamek/common/Entity.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index a42d4d7b6e4..2d5aeab6e41 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -10046,6 +10046,9 @@ public boolean isEligibleForPhysical() { && hasWorkingMisc(MiscType.F_TOOLS, MiscType.S_DEMOLITION_CHARGE)) { Hex hex = game.getBoard().getHex(getPosition()); + if (hex == null) { + return false; + } return hex.containsTerrain(Terrains.BUILDING); } // only mechs and protos have physical attacks (except tank charges) From d038e37f824810a1d28187981b204f47a76f9033 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Sun, 28 Jul 2024 03:24:37 -0700 Subject: [PATCH 15/32] Add safety checks to each dialog call in the load-button handling --- .../client/ui/swing/DeploymentDisplay.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java b/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java index 40955580048..920b167adcb 100644 --- a/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java +++ b/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java @@ -720,6 +720,12 @@ public void actionPerformed(ActionEvent evt) { Messages.getString("DeploymentDisplay.loadUnitDialog.message", ce().getShortName(), ce().getUnusedString()), choices); + // Abort here if no Entity was generated + if (other == null) { + return; + } + + // Otherwise continue if (!(other instanceof Infantry)) { List bayChoices = new ArrayList<>(); for (Transporter t : ce().getTransports()) { @@ -738,6 +744,12 @@ public void actionPerformed(ActionEvent evt) { String msg = Messages.getString("DeploymentDisplay.loadUnitBayNumberDialog.message", ce().getShortName()); String bayString = (String) JOptionPane.showInputDialog(clientgui.getFrame(), msg, title, JOptionPane.QUESTION_MESSAGE, null, retVal, null); + + // No choice made? Bug out. + if (bayString == null) { + return; + } + int bayNum = Integer.parseInt(bayString.substring(0, bayString.indexOf(" "))); other.setTargetBay(bayNum); // We need to update the entity here so that the server knows about our target bay @@ -761,6 +773,12 @@ public void actionPerformed(ActionEvent evt) { Messages.getString("MovementDisplay.loadProtoClampMountDialog.message", ce().getShortName()), Messages.getString("MovementDisplay.loadProtoClampMountDialog.title"), JOptionPane.QUESTION_MESSAGE, null, retVal, null); + + // No choice made? Bug out. + if (bayString == null) { + return; + } + other.setTargetBay(bayString.equals(Messages.getString("MovementDisplay.loadProtoClampMountDialog.front")) ? 0 : 1); // We need to update the entity here so that the server knows about our target bay clientgui.getClient().sendUpdateEntity(other); From e6a1a2a2b77fe655217c5bafca7682dec2a2eb18 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 28 Jul 2024 11:09:31 -0400 Subject: [PATCH 16/32] sprite scaling; unit tooltip --- .../swing/boardview/GroundObjectSprite.java | 12 ++++++++-- .../client/ui/swing/tooltip/UnitToolTip.java | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/boardview/GroundObjectSprite.java b/megamek/src/megamek/client/ui/swing/boardview/GroundObjectSprite.java index 5bce6f83c0e..a5e1947558b 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/GroundObjectSprite.java +++ b/megamek/src/megamek/client/ui/swing/boardview/GroundObjectSprite.java @@ -19,10 +19,11 @@ package megamek.client.ui.swing.boardview; import java.awt.Dimension; +import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; - +import megamek.client.ui.swing.util.UIUtil; import megamek.common.Configuration; import megamek.common.Coords; import megamek.common.util.ImageUtil; @@ -58,5 +59,12 @@ public Rectangle getBounds() { } @Override - public void prepare() { } + public void prepare() { + updateBounds(); + image = createNewHexImage(); + Graphics2D graph = (Graphics2D) image.getGraphics(); + UIUtil.setHighQualityRendering(graph); + graph.scale(bv.scale, bv.scale); + graph.drawImage(CARGO_IMAGE, 0, 0, null); + } } \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/tooltip/UnitToolTip.java b/megamek/src/megamek/client/ui/swing/tooltip/UnitToolTip.java index 55d9383ed02..71dea7b204b 100644 --- a/megamek/src/megamek/client/ui/swing/tooltip/UnitToolTip.java +++ b/megamek/src/megamek/client/ui/swing/tooltip/UnitToolTip.java @@ -164,6 +164,9 @@ private static StringBuilder getEntityTipTable(Entity entity, Player localPlayer // Carried Units result += carriedUnits(entity); + + // carried cargo + result += carriedCargo(entity); // C3 Info result += c3Info(entity, details); @@ -1843,6 +1846,25 @@ private static StringBuilder carriedUnits(Entity entity) { return new StringBuilder().append(result); } + + private static StringBuilder carriedCargo(Entity entity) { + StringBuilder sb = new StringBuilder(); + List cargoList = entity.getDistinctCarriedObjects(); + + if (!cargoList.isEmpty()) { + sb.append(guiScaledFontHTML()); + sb.append(Messages.getString("MissionRole.cargo")); + sb.append(":
      "); + + for (ICarryable cargo : entity.getDistinctCarriedObjects()) { + sb.append(cargo.toString()); + sb.append("
      "); + } + sb.append(""); + } + + return sb; + } /** Returns the full force chain the entity is in as one text line. */ private static StringBuilder forceEntry(Entity entity, Player localPlayer) { From 4c60b4654d27b8712afc2693fddcc211330d248f Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 28 Jul 2024 11:15:12 -0400 Subject: [PATCH 17/32] adjust report ordering --- megamek/src/megamek/server/GameManager.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/megamek/src/megamek/server/GameManager.java b/megamek/src/megamek/server/GameManager.java index 2abf1efd6c1..b0bb74a4424 100644 --- a/megamek/src/megamek/server/GameManager.java +++ b/megamek/src/megamek/server/GameManager.java @@ -27050,7 +27050,7 @@ else if ((null != Compute.stackingViolation(game, other.getId(), curPos, entity. } // drop cargo - dropCargo(entity, curPos); + dropCargo(entity, curPos, vDesc); // update our entity, so clients have correct data needed for MekWars stuff entityUpdate(entity.getId()); @@ -27061,7 +27061,7 @@ else if ((null != Compute.stackingViolation(game, other.getId(), curPos, entity. /** * Worker function that drops cargo from an entity at the given coordinates. */ - private void dropCargo(Entity entity, Coords coords) { + private void dropCargo(Entity entity, Coords coords, Vector vPhaseReport) { boolean cargoDropped = false; for (ICarryable cargo : entity.getDistinctCarriedObjects()) { @@ -27555,9 +27555,6 @@ else if (waterDepth > 0) { return vPhaseReport; } - // drop cargo if necessary - dropCargo(entity, fallPos); - // set how deep the mech has fallen if (entity instanceof Mech) { Mech mech = (Mech) entity; @@ -27765,6 +27762,9 @@ else if (waterDepth > 0) { } } // End dislodge-infantry + // drop cargo if necessary + dropCargo(entity, fallPos, vPhaseReport); + // clear all PSRs after a fall -- the Mek has already failed ONE and // fallen, it'd be cruel to make it fail some more! game.resetPSRs(entity); From 6b48dd43694877a1541f30031958a6d6f55a7bd9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 28 Jul 2024 11:27:35 -0400 Subject: [PATCH 18/32] InGameobject; pick up correct object when it's not first in list --- .../client/ui/swing/MovementDisplay.java | 5 ++-- megamek/src/megamek/common/Briefcase.java | 27 +++++++++++++++++++ megamek/src/megamek/common/ICarryable.java | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index 685190bf5ff..cd356bb1e26 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -5325,11 +5325,12 @@ private void processPickupCargoCommand() { // regardless of how many objects we are picking up, // we may have to choose the location with which to pick it up if (displayedOptions.size() == 1) { - Integer pickupLocation = getPickupLocation(options.get(0)); + Integer pickupLocation = getPickupLocation(displayedOptions.get(0)); if (pickupLocation != null) { Map data = new HashMap<>(); - data.put(MoveStep.CARGO_PICKUP_KEY, 0); + // we pick the only eligible object out of all the objects on the ground + data.put(MoveStep.CARGO_PICKUP_KEY, options.indexOf(displayedOptions.get(0))); data.put(MoveStep.CARGO_LOCATION_KEY, pickupLocation); addStepToMovePath(MoveStepType.PICKUP_CARGO, data); diff --git a/megamek/src/megamek/common/Briefcase.java b/megamek/src/megamek/common/Briefcase.java index d166f021142..4a5c33e0c0c 100644 --- a/megamek/src/megamek/common/Briefcase.java +++ b/megamek/src/megamek/common/Briefcase.java @@ -33,6 +33,8 @@ public class Briefcase implements ICarryable, Serializable { private double tonnage; private String name; private boolean invulnerable; + private int id; + private int ownerId; public void damage(double amount) { tonnage -= amount; @@ -70,4 +72,29 @@ public String specificName() { public String toString() { return specificName(); } + + @Override + public int getId() { + return id; + } + + @Override + public void setId(int newId) { + this.id = newId; + } + + @Override + public int getOwnerId() { + return ownerId; + } + + @Override + public void setOwnerId(int newOwnerId) { + this.ownerId = newOwnerId; + } + + @Override + public int getStrength() { + return 0; + } } diff --git a/megamek/src/megamek/common/ICarryable.java b/megamek/src/megamek/common/ICarryable.java index b0160cee950..6492c84d110 100644 --- a/megamek/src/megamek/common/ICarryable.java +++ b/megamek/src/megamek/common/ICarryable.java @@ -22,7 +22,7 @@ /** * An interface defining all the required properties of a carryable object. */ -public interface ICarryable extends BTObject { +public interface ICarryable extends InGameObject { double getTonnage(); void damage(double amount); boolean isInvulnerable(); From f797f8357164f119d9a2ec091b23bd03e5158bba Mon Sep 17 00:00:00 2001 From: sleet01 Date: Sun, 28 Jul 2024 13:08:54 -0700 Subject: [PATCH 19/32] Move existing getPosition() check to start of function --- megamek/src/megamek/common/Entity.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index 2d5aeab6e41..7fed397555c 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -10042,6 +10042,10 @@ public boolean isEligibleForPhysical() { boolean canHit = false; boolean friendlyFire = game.getOptions().booleanOption(OptionsConstants.BASE_FRIENDLY_FIRE); + if (getPosition() == null) { + return false; // not on board? + } + if ((this instanceof Infantry) && hasWorkingMisc(MiscType.F_TOOLS, MiscType.S_DEMOLITION_CHARGE)) { @@ -10051,6 +10055,7 @@ && hasWorkingMisc(MiscType.F_TOOLS, } return hex.containsTerrain(Terrains.BUILDING); } + // only mechs and protos have physical attacks (except tank charges) if (!((this instanceof Mech) || (this instanceof Protomech) || (this instanceof Infantry))) { return false; @@ -10091,10 +10096,6 @@ && getCrew().isClanPilot() && !hasINarcPodsAttached() return false; } - if (getPosition() == null) { - return false; // not on board? - } - // check if we have iNarc pods attached that can be brushed off if (hasINarcPodsAttached() && (this instanceof Mech)) { return true; From 1d51c1b026664c310457ddfb4909292d951ec0f7 Mon Sep 17 00:00:00 2001 From: Sleet01 Date: Sun, 28 Jul 2024 13:24:10 -0700 Subject: [PATCH 20/32] Update history.txt --- megamek/docs/history.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/megamek/docs/history.txt b/megamek/docs/history.txt index d54996c426b..c439c9f59ec 100644 --- a/megamek/docs/history.txt +++ b/megamek/docs/history.txt @@ -62,6 +62,9 @@ MEGAMEK VERSION HISTORY: + Fix #5789: Prevent errors with showing the firing arcs + Fix #5793: Units are no longer able to be unloaded without regard for the bay door limits + Fix #5611: Prevent NPE when removing forces from Force that exists but is not registered yet (when quickly closing MM after an MHQ scenario start) ++ Fix #5691: Prevent NPE when cancelling unit loading during carrier deployment (e.g. Space Station and loading ASFs) ++ Fix #5705: Prevent NPE when maneuvering Land-Air Meks as ASFs ++ Fix #5796: Prevent NPE when units are ineligible for Physical Attack phase due to having loaded onto a transport this round 0.49.20 (2024-06-28 2100 UTC) (THIS IS THE LAST VERSION TO SUPPORT JAVA 11) From 265e0589f5cdcbf5b489e33f016f4b730816d181 Mon Sep 17 00:00:00 2001 From: kuronekochomusuke Date: Sun, 28 Jul 2024 19:06:26 -0400 Subject: [PATCH 21/32] correct issue with minimap autodisplay using wrong function --- megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java b/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java index df67eea8062..4a7cee1f1d7 100644 --- a/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java +++ b/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java @@ -2876,7 +2876,7 @@ private JPanel getPhasePanel() { row = new ArrayList<>(); phaseLabel = new JLabel(Messages.getString("CommonSettingsDialog.nonReportPhases") + ": "); row.add(phaseLabel); - miniMapAutoDisplayNonReportCombo = createHideShowComboBox(GUIP.getMiniReportAutoDisplayNonReportPhase()); + miniMapAutoDisplayNonReportCombo = createHideShowComboBox(GUIP.getMinimapAutoDisplayNonReportPhase()); row.add(miniMapAutoDisplayNonReportCombo); comps.add(row); From f09ccdbcbef3837b18e8c48c7d6d4bda00559a09 Mon Sep 17 00:00:00 2001 From: kuronekochomusuke <116095479+kuronekochomusuke@users.noreply.github.com> Date: Sun, 28 Jul 2024 19:40:28 -0400 Subject: [PATCH 22/32] Update history.txt --- megamek/docs/history.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/megamek/docs/history.txt b/megamek/docs/history.txt index c439c9f59ec..602810603b2 100644 --- a/megamek/docs/history.txt +++ b/megamek/docs/history.txt @@ -65,6 +65,7 @@ MEGAMEK VERSION HISTORY: + Fix #5691: Prevent NPE when cancelling unit loading during carrier deployment (e.g. Space Station and loading ASFs) + Fix #5705: Prevent NPE when maneuvering Land-Air Meks as ASFs + Fix #5796: Prevent NPE when units are ineligible for Physical Attack phase due to having loaded onto a transport this round ++ Fix #5818: correct issue with minimap autodisplay using wrong function 0.49.20 (2024-06-28 2100 UTC) (THIS IS THE LAST VERSION TO SUPPORT JAVA 11) From 13ddea114dfdd11a90e8783c707247e6952da978 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Sun, 28 Jul 2024 18:12:55 -0700 Subject: [PATCH 23/32] Add Swamp, Mud hazard functions (empty) and enhance Liquid Magma ("Lava") handling --- .../client/bot/princess/BasicPathRanker.java | 98 ++++++++++++++++--- 1 file changed, 87 insertions(+), 11 deletions(-) diff --git a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java index 360905269b3..4401132099a 100644 --- a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java @@ -789,7 +789,9 @@ private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, Mo Terrains.WATER, Terrains.BUILDING, Terrains.BRIDGE, - Terrains.BLACK_ICE)); + Terrains.BLACK_ICE, + Terrains.SWAMP, + Terrains.MUD)); int[] terrainTypes = hex.getTerrainTypes(); Set hazards = new HashSet<>(); @@ -835,6 +837,12 @@ private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, Mo case Terrains.BLACK_ICE: hazardValue += calcIceHazard(movingUnit, hex, step, movePath, jumpLanding, logMsg); break; + case Terrains.SWAMP: + hazardValue += calcSwampHazard(movingUnit, hex, step, movePath, jumpLanding, logMsg); + break; + case Terrains.MUD: + hazardValue += calcMudHazard(movingUnit, hex, step, movePath, jumpLanding, logMsg); + break; } } @@ -959,6 +967,12 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa return 0; } + // Submarine units should be fine; Orca-riding Infantry goes here. + if (EntityMovementMode.SUBMARINE == movingUnit.getMovementMode()) { + logMsg.append("Submarine locomotion unit (0)."); + return 0; + } + // if we are crossing a bridge, then we'll be fine. Trust me. // 1. Determine bridge elevation // 2. If unit elevation is equal to bridge elevation, skip. @@ -970,9 +984,10 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa } } - // Most other units are automatically destroyed. + // Most other units are automatically destroyed. UMU-equipped units _may_ not drown immediately, + // but all other hazards (e.g. breaches, crush depth) still apply. if (!(movingUnit instanceof Mech || movingUnit instanceof Protomech || - movingUnit instanceof BattleArmor)) { + movingUnit instanceof BattleArmor || movingUnit.hasUMU())) { logMsg.append("Ill drown (1000)."); return UNIT_DESTRUCTION_FACTOR; } @@ -990,6 +1005,8 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa return destructionFactor; } + // TODO: implement crush depth calcs (TO:AR pg. 40) + // Find the submerged locations. Set submergedLocations = new HashSet<>(); for (int loc = 0; loc < movingUnit.locations(); loc++) { @@ -1021,7 +1038,7 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa for (int loc : submergedLocations) { logMsg.append("\n\t\t\tLocation ").append(loc).append(" is "); - // Only locations withou armor can breach in movement phase. + // Only locations without armor can breach in movement phase. if (movingUnit.getArmor(loc) > 0) { logMsg.append(" not breached (0)."); continue; @@ -1134,12 +1151,23 @@ private double calcLavaHazard(boolean endHex, Entity movingUnit, MoveStep step, StringBuilder logMsg) { logMsg.append("\n\tCalculating laval hazard: "); + double dmg; - // Hovers are unaffected. - if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || - EntityMovementMode.WIGE == movingUnit.getMovementMode()) { - logMsg.append("Hovering above lava (0)."); - return 0; + // Hovers are unaffected _unless_ they end on the hex and are in danger of losing mobility. + if (EntityMovementMode.VTOL == movingUnit.getMovementMode() + || EntityMovementMode.HOVER == movingUnit.getMovementMode() + || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { + if (!endHex) { + logMsg.append("Hovering/VTOL while traversing lava (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 = (movingUnit.getDamageLevel()/4.0) * UNIT_DESTRUCTION_FACTOR; + logMsg.append("Ending hover/VTOL movement over lava ("); + logMsg.append(LOG_DECIMAL.format(dmg)).append(")."); + return dmg; + } } // Non-mech units auto-destroyed. @@ -1149,6 +1177,22 @@ private double calcLavaHazard(boolean endHex, Entity movingUnit, } double hazardValue = 0; + double psrFactor = 1.0; + + // Adjust hazard by chance of getting stuck + if (endHex && step.isJumping()) { + // Chance of getting stuck in magma is the chance of failing one PSR. + // Factor applied to damage should also include the expected number of turns _not_ escaping. + // Former is: %chance _not_ passing PSR + // Latter is: N = log(desired failure to escape chance, e.g. 10%)/log(%chance Pass PSR) + logMsg.append("Possibly jumping onto lava hex, may get bogged down."); + double oddsPSR = (Compute.oddsAbove(movingUnit.getCrew().getPiloting()) / 100); + double oddsBogged = (1.0 - oddsPSR); + double expectedTurns = Math.log10(0.10)/Math.log10(oddsPSR); + logMsg.append("\n\t\tChance to fail piloting roll: ").append(LOG_PERCENT.format(oddsBogged)); + logMsg.append("\n\t\tExpected turns before escape: ").append(LOG_DECIMAL.format(expectedTurns)); + psrFactor = 1.0 + oddsBogged + (expectedTurns); + } // Factor in heat. double heat = endHex ? 10.0 : 5.0; @@ -1157,7 +1201,6 @@ private double calcLavaHazard(boolean endHex, Entity movingUnit, .append(LOG_DECIMAL.format(heat)).append(")."); // Factor in potential damage. - double dmg; logMsg.append("\n\t\tDamage to "); if (step.isProne()) { dmg = 7 * movingUnit.locations(); @@ -1175,7 +1218,40 @@ private double calcLavaHazard(boolean endHex, Entity movingUnit, logMsg.append(LOG_DECIMAL.format(dmg)).append(")."); hazardValue += dmg; - return hazardValue; + // Multiply total hazard value by the chance of getting stuck for 1 or more additional turns + logMsg.append("Factor applied to hazard value: ").append(LOG_DECIMAL.format(psrFactor)); + return hazardValue * psrFactor; + } + + + private double calcSwampHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath movePath, boolean jumpLanding, + StringBuilder logMsg) { + logMsg.append("\n\tCalculating Swamp hazard: "); + + // Hover units are above the surface. + if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || + EntityMovementMode.WIGE == movingUnit.getMovementMode()) { + logMsg.append("Hovering above swamp (0)."); + return 0; + } + + double hazard = 0.0; + return hazard; + } + + private double calcMudHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath movePath, boolean jumpLanding, + StringBuilder logMsg) { + logMsg.append("\n\tCalculating Mud hazard: "); + + // Hover units are above the surface. + if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || + EntityMovementMode.WIGE == movingUnit.getMovementMode()) { + logMsg.append("Hovering above Mud (0)."); + return 0; + } + + double hazard = 0.0; + return hazard; } /** From 2d4d5b6441f71dbc615f57d6d62e02d392a23ee1 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Mon, 29 Jul 2024 11:46:22 -0700 Subject: [PATCH 24/32] Further updates and unit tests --- .../client/bot/princess/BasicPathRanker.java | 150 +++-- .../bot/princess/BasicPathRankerTest.java | 532 +++++++++++++----- 2 files changed, 510 insertions(+), 172 deletions(-) diff --git a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java index 4401132099a..99a6636d586 100644 --- a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java @@ -838,10 +838,10 @@ private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, Mo hazardValue += calcIceHazard(movingUnit, hex, step, movePath, jumpLanding, logMsg); break; case Terrains.SWAMP: - hazardValue += calcSwampHazard(movingUnit, hex, step, movePath, jumpLanding, logMsg); + hazardValue += calcSwampHazard(hex, endHex, movingUnit, jumpLanding, step, logMsg); break; case Terrains.MUD: - hazardValue += calcMudHazard(movingUnit, hex, step, movePath, jumpLanding, logMsg); + hazardValue += calcMudHazard(endHex, movingUnit, logMsg); break; } } @@ -1110,9 +1110,9 @@ private double calcMagmaHazard(Hex hex, boolean endHex, Entity movingUnit, StringBuilder logMsg) { logMsg.append("\n\tCalculating magma hazard: "); - // Hovers are unaffected. - if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || - EntityMovementMode.WIGE == movingUnit.getMovementMode()) { + // Hovers / WiGE are normally unaffected. + if ((EntityMovementMode.HOVER == movingUnit.getMovementMode() || + EntityMovementMode.WIGE == movingUnit.getMovementMode()) && !endHex) { logMsg.append("Hovering above magma (0)."); return 0; } @@ -1122,14 +1122,14 @@ private double calcMagmaHazard(Hex hex, boolean endHex, Entity movingUnit, // Liquid magma. if (magmaLevel == 2) { - return calcLavaHazard(endHex, movingUnit, step, logMsg); + return calcLavaHazard(endHex, jumpLanding, movingUnit, step, logMsg); } else { double breakThroughMod = jumpLanding ? 0.5 : 0.1667; logMsg.append("\n\t\tChance to break through crust = ") .append(LOG_PERCENT.format(breakThroughMod)); // Factor in the chance to break through. - double lavalHazard = calcLavaHazard(endHex, movingUnit, step, + double lavalHazard = calcLavaHazard(endHex, jumpLanding, movingUnit, step, logMsg) * breakThroughMod; logMsg.append("\n\t\t\tLava hazard (") .append(LOG_DECIMAL.format(lavalHazard)).append(")."); @@ -1147,15 +1147,14 @@ private double calcMagmaHazard(Hex hex, boolean endHex, Entity movingUnit, return hazardValue; } - private double calcLavaHazard(boolean endHex, Entity movingUnit, + private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity movingUnit, MoveStep step, StringBuilder logMsg) { logMsg.append("\n\tCalculating laval hazard: "); double dmg; - // Hovers are unaffected _unless_ they end on the hex and are in danger of losing mobility. - if (EntityMovementMode.VTOL == movingUnit.getMovementMode() - || EntityMovementMode.HOVER == movingUnit.getMovementMode() + // 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) { logMsg.append("Hovering/VTOL while traversing lava (0)."); @@ -1180,16 +1179,18 @@ private double calcLavaHazard(boolean endHex, Entity movingUnit, double psrFactor = 1.0; // Adjust hazard by chance of getting stuck - if (endHex && step.isJumping()) { + if (endHex && jumpLanding) { // Chance of getting stuck in magma is the chance of failing one PSR. // Factor applied to damage should also include the expected number of turns _not_ escaping. // Former is: %chance _not_ passing PSR - // Latter is: N = log(desired failure to escape chance, e.g. 10%)/log(%chance Pass PSR) + // Latter is: N = log(desired failure to escape chance, e.g. 10%)/log(%chance Fail PSR) logMsg.append("Possibly jumping onto lava hex, may get bogged down."); - double oddsPSR = (Compute.oddsAbove(movingUnit.getCrew().getPiloting()) / 100); + int pilotSkill = movingUnit.getCrew().getPiloting(); + double oddsPSR = Compute.oddsAbove(pilotSkill) / 100; double oddsBogged = (1.0 - oddsPSR); - double expectedTurns = Math.log10(0.10)/Math.log10(oddsPSR); - logMsg.append("\n\t\tChance to fail piloting roll: ").append(LOG_PERCENT.format(oddsBogged)); + double expectedTurns = Math.log10(0.10)/Math.log10(oddsBogged); + logMsg.append("\n\t\tEffective Piloting Skill: ").append(LOG_INT.format(pilotSkill)); + logMsg.append("\n\t\tChance to bog down: ").append(LOG_PERCENT.format(oddsBogged)); logMsg.append("\n\t\tExpected turns before escape: ").append(LOG_DECIMAL.format(expectedTurns)); psrFactor = 1.0 + oddsBogged + (expectedTurns); } @@ -1197,8 +1198,7 @@ private double calcLavaHazard(boolean endHex, Entity movingUnit, // Factor in heat. double heat = endHex ? 10.0 : 5.0; hazardValue += heat; - logMsg.append("\n\t\tHeat gain (").append(heat) - .append(LOG_DECIMAL.format(heat)).append(")."); + logMsg.append("\n\t\tHeat gain (").append(LOG_DECIMAL.format(heat)).append(")."); // Factor in potential damage. logMsg.append("\n\t\tDamage to "); @@ -1219,39 +1219,125 @@ private double calcLavaHazard(boolean endHex, Entity movingUnit, hazardValue += dmg; // Multiply total hazard value by the chance of getting stuck for 1 or more additional turns - logMsg.append("Factor applied to hazard value: ").append(LOG_DECIMAL.format(psrFactor)); - return hazardValue * psrFactor; + logMsg.append("\nFactor applied to hazard value: ").append(LOG_DECIMAL.format(psrFactor)); + return Math.round(hazardValue * psrFactor); } + private double calcBogDownFactor(String name, boolean endHex, boolean jumpLanding, int pilotSkill, + int modifier, StringBuilder logMsg) { + return calcBogDownFactor(name, endHex, jumpLanding, pilotSkill, modifier, true, logMsg); + } + /** + * Calculate a PSR-related factor for increasing the hazard of terrain where bogging down is + * possible + * @param name Name of terrain type, for logging. + * @param endHex If this is checking the final hex of a movement path or not. + * @param jumpLanding Whether unit will be jumping into the end hex or not. + * @param pilotSkill base pilot/driver/etc. skill used for the PSR checks to escape bogging down. + * @param modifier Modifier, based on unit type and terrain type + * @param bogPossible whether the unit can actually get bogged own in this terrain type, or just calculating + * @param logMsg Ref to StringBuilder used for logging. + * @return double Factor to multiply by terrain hazards. + */ + private double calcBogDownFactor(String name, boolean endHex, boolean jumpLanding, int pilotSkill, + int modifier, boolean bogPossible, StringBuilder logMsg) { + double factor; + int effectiveSkill = pilotSkill + modifier; + double oddsPSR = Math.max((Compute.oddsAbove(effectiveSkill) / 100.0), 0.0); + double oddsBogged = 0.0; - private double calcSwampHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath movePath, boolean jumpLanding, - StringBuilder logMsg) { + // Adjust hazard by chance of getting stuck + if (endHex && jumpLanding) { + // Chance of getting stuck in swamp/mud is the chance of failing one PSR, or 100% if jumping. + logMsg.append("\nJumping onto "); + logMsg.append(name); + logMsg.append((bogPossible) ? " hex, would get bogged down." : " hex but cannot bog down."); + oddsBogged = 1.0; + } else if (!jumpLanding) { + logMsg.append("\nEntering "); + logMsg.append(name); + logMsg.append((bogPossible) ? " hex, may get bogged down." : " hex but cannot bog down."); + oddsBogged = 1.0 - oddsPSR; + } + // (Reuse PSR odds to avoid infinite trapped time on turns when jumping into terrain causes 100% bog-down) + double expectedTurns = ((1 - oddsPSR) < 1.0) ? Math.log10(0.10)/Math.log10(1 - oddsPSR) : UNIT_DESTRUCTION_FACTOR; + + logMsg.append("\n\t\tEffective Piloting Skill: ").append(LOG_INT.format(effectiveSkill)); + if (bogPossible) { + logMsg.append("\n\t\tChance to bog down: ").append(LOG_PERCENT.format(oddsBogged)); + logMsg.append("\n\t\tExpected turns before escape: ").append(LOG_DECIMAL.format(expectedTurns)); + } + factor = 1.0 + oddsBogged + (expectedTurns); + + return factor; + } + + private double calcSwampHazard(Hex hex, boolean endHex, Entity movingUnit, + boolean jumpLanding, MoveStep step, + StringBuilder logMsg) { logMsg.append("\n\tCalculating Swamp hazard: "); // Hover units are above the surface. - if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || - EntityMovementMode.WIGE == movingUnit.getMovementMode()) { + if (EntityMovementMode.HOVER == movingUnit.getMovementMode() + || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { logMsg.append("Hovering above swamp (0)."); return 0; } - double hazard = 0.0; - return hazard; + // Base hazard is the chance of becoming Quicksand and destroying this unit + // If currently Swamp, could become Quicksand if 12 on 2d6 is rolled. + // If already Quicksand... + boolean quicksand = hex.terrainLevel(Terrains.SWAMP) > 1; + String type = (quicksand) ? "Quicksand" : "Swamp"; + double quicksandChance = (quicksand) ? 1.0 : 1/36.0; + // Height + 1 turns to fully sink and be destroyed + double hazard = quicksandChance * UNIT_DESTRUCTION_FACTOR/(1 + movingUnit.getHeight()); + logMsg.append("\nBase hazard value: ").append(LOG_DECIMAL.format(hazard)); + + // Mod is to difficulty, not to PSR roll results + // Quicksand makes PSRs an additional +3! + int psrMod = (movingUnit instanceof Mech) ? +1 : +2; + psrMod += (quicksand) ? +3 : 0; + + int pilotSkill = movingUnit.getCrew().getPiloting(); + + double factor = calcBogDownFactor(type, endHex, jumpLanding, pilotSkill, psrMod, logMsg); + logMsg.append("\nFactor applied to hazard value: ").append(LOG_DECIMAL.format(factor)); + + // The danger is increased if pilot skill is low, as the chance of succumbing or getting + // permanently stuck increases! + hazard = hazard * factor; + + return Math.round(hazard); } - private double calcMudHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath movePath, boolean jumpLanding, - StringBuilder logMsg) { + private double calcMudHazard(boolean endHex, Entity movingUnit, StringBuilder logMsg) { logMsg.append("\n\tCalculating Mud hazard: "); // Hover units are above the surface. - if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || - EntityMovementMode.WIGE == movingUnit.getMovementMode()) { + if (EntityMovementMode.HOVER == movingUnit.getMovementMode() + || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { logMsg.append("Hovering above Mud (0)."); return 0; } - double hazard = 0.0; - return hazard; + int psrMod = +1; + int pilotSkill = movingUnit.getCrew().getPiloting(); + double hazard; + + if (movingUnit instanceof Mech) { + // The only hazard is the +1 to PSRs, which are difficult to quantify + // Even jumping mechs cannot bog down in mud. + hazard = calcBogDownFactor( + "Mud", endHex, false, pilotSkill, psrMod, false, logMsg); + } else { + // Mud is more dangerous for units that can actually bog down + // Base hazard is arbitrarily set to 10 + hazard = 10 * calcBogDownFactor( + "Mud", endHex, false, pilotSkill, psrMod, logMsg); + } + logMsg.append("\nBase hazard value: ").append(LOG_DECIMAL.format(hazard)); + return Math.round(hazard); } /** diff --git a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java index e6f234a21eb..98ab64a53ea 100644 --- a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java +++ b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java @@ -89,7 +89,7 @@ public void beforeEach() { final UnitBehavior mockBehaviorTracker = mock(UnitBehavior.class); when(mockBehaviorTracker.getBehaviorType(any(Entity.class), any(Princess.class))) - .thenReturn(BehaviorType.Engaged); + .thenReturn(BehaviorType.Engaged); mockPrincess = mock(Princess.class); when(mockPrincess.getBehaviorSettings()).thenReturn(mockBehavior); @@ -202,17 +202,17 @@ public void testEvaluateUnmovedEnemy() { when(mockEnemyMech.getWeight()).thenReturn(50.0); when(mockEnemyMech.getId()).thenReturn(enemyMechId); doReturn(enemyCoords) - .when(testRanker) - .getClosestCoordsTo(eq(enemyMechId), eq(testCoords)); + .when(testRanker) + .getClosestCoordsTo(eq(enemyMechId), eq(testCoords)); doReturn(true) - .when(testRanker) - .isInMyLoS(eq(mockEnemyMech), any(HexLine.class), any(HexLine.class)); + .when(testRanker) + .isInMyLoS(eq(mockEnemyMech), any(HexLine.class), any(HexLine.class)); doReturn(8.5) - .when(testRanker) - .getMaxDamageAtRange(nullable(FireControl.class), eq(mockEnemyMech), anyInt(), anyBoolean(), anyBoolean()); + .when(testRanker) + .getMaxDamageAtRange(nullable(FireControl.class), eq(mockEnemyMech), anyInt(), anyBoolean(), anyBoolean()); doReturn(false) - .when(testRanker) - .canFlankAndKick(eq(mockEnemyMech), any(Coords.class), any(Coords.class), any(Coords.class), anyInt()); + .when(testRanker) + .canFlankAndKick(eq(mockEnemyMech), any(Coords.class), any(Coords.class), any(Coords.class), anyInt()); expected = new EntityEvaluationResponse(); expected.setEstimatedEnemyDamage(2.125); expected.setMyEstimatedDamage(2.5); @@ -226,17 +226,17 @@ public void testEvaluateUnmovedEnemy() { when(mockEnemyMech.getWeight()).thenReturn(50.0); when(mockEnemyMech.getId()).thenReturn(enemyMechId); doReturn(enemyCoords) - .when(testRanker) - .getClosestCoordsTo(eq(enemyMechId), eq(testCoords)); + .when(testRanker) + .getClosestCoordsTo(eq(enemyMechId), eq(testCoords)); doReturn(false) - .when(testRanker) - .isInMyLoS(eq(mockEnemyMech), any(HexLine.class), any(HexLine.class)); + .when(testRanker) + .isInMyLoS(eq(mockEnemyMech), any(HexLine.class), any(HexLine.class)); doReturn(8.5) - .when(testRanker) - .getMaxDamageAtRange(nullable(FireControl.class), eq(mockEnemyMech), anyInt(), anyBoolean(), anyBoolean()); + .when(testRanker) + .getMaxDamageAtRange(nullable(FireControl.class), eq(mockEnemyMech), anyInt(), anyBoolean(), anyBoolean()); doReturn(false) - .when(testRanker) - .canFlankAndKick(eq(mockEnemyMech), any(Coords.class), any(Coords.class), any(Coords.class), anyInt()); + .when(testRanker) + .canFlankAndKick(eq(mockEnemyMech), any(Coords.class), any(Coords.class), any(Coords.class), anyInt()); expected = new EntityEvaluationResponse(); expected.setEstimatedEnemyDamage(2.125); expected.setMyEstimatedDamage(0.0); @@ -250,17 +250,17 @@ public void testEvaluateUnmovedEnemy() { when(mockEnemyMech.getWeight()).thenReturn(50.0); when(mockEnemyMech.getId()).thenReturn(enemyMechId); doReturn(enemyCoords) - .when(testRanker) - .getClosestCoordsTo(eq(enemyMechId), eq(testCoords)); + .when(testRanker) + .getClosestCoordsTo(eq(enemyMechId), eq(testCoords)); doReturn(false) - .when(testRanker) - .isInMyLoS(eq(mockEnemyMech), any(HexLine.class), any(HexLine.class)); + .when(testRanker) + .isInMyLoS(eq(mockEnemyMech), any(HexLine.class), any(HexLine.class)); doReturn(8.5) - .when(testRanker) - .getMaxDamageAtRange(nullable(FireControl.class), eq(mockEnemyMech), anyInt(), anyBoolean(), anyBoolean()); + .when(testRanker) + .getMaxDamageAtRange(nullable(FireControl.class), eq(mockEnemyMech), anyInt(), anyBoolean(), anyBoolean()); doReturn(true) - .when(testRanker) - .canFlankAndKick(eq(mockEnemyMech), any(Coords.class), any(Coords.class), any(Coords.class), anyInt()); + .when(testRanker) + .canFlankAndKick(eq(mockEnemyMech), any(Coords.class), any(Coords.class), any(Coords.class), anyInt()); expected = new EntityEvaluationResponse(); expected.setEstimatedEnemyDamage(4.625); expected.setMyEstimatedDamage(0.0); @@ -295,23 +295,23 @@ public void testEvaluateMovedEnemy() { when(mockEnemyMech.getCrew()).thenReturn(mockCrew); doReturn(15.0) - .when(testRanker) - .calculateDamagePotential(eq(mockEnemyMech), any(EntityState.class), - any(MovePath.class), any(EntityState.class), anyInt(), any(Game.class)); + .when(testRanker) + .calculateDamagePotential(eq(mockEnemyMech), any(EntityState.class), + any(MovePath.class), any(EntityState.class), anyInt(), any(Game.class)); doReturn(10.0) - .when(testRanker) - .calculateKickDamagePotential(eq(mockEnemyMech), any(MovePath.class), any(Game.class)); + .when(testRanker) + .calculateKickDamagePotential(eq(mockEnemyMech), any(MovePath.class), any(Game.class)); doReturn(14.5) - .when(testRanker) - .calculateMyDamagePotential(any(MovePath.class), eq(mockEnemyMech), anyInt(), any(Game.class)); + .when(testRanker) + .calculateMyDamagePotential(any(MovePath.class), eq(mockEnemyMech), anyInt(), any(Game.class)); doReturn(8.0) - .when(testRanker) - .calculateMyKickDamagePotential(any(MovePath.class), eq(mockEnemyMech), any(Game.class)); + .when(testRanker) + .calculateMyKickDamagePotential(any(MovePath.class), eq(mockEnemyMech), any(Game.class)); final Map testBestDamageByEnemies = new TreeMap<>(); testBestDamageByEnemies.put(mockEnemyMechId, 0.0); doReturn(testBestDamageByEnemies) - .when(testRanker) - .getBestDamageByEnemies(); + .when(testRanker) + .getBestDamageByEnemies(); final EntityEvaluationResponse expected = new EntityEvaluationResponse(); expected.setMyEstimatedDamage(14.5); expected.setMyEstimatedPhysicalDamage(8.0); @@ -339,20 +339,20 @@ private void assertEntityEvaluationResponseEquals(final EntityEvaluationResponse public void testRankPath() { final BasicPathRanker testRanker = spy(new BasicPathRanker(mockPrincess)); doReturn(1.0) - .when(testRanker) - .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); + .when(testRanker) + .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); doReturn(5) - .when(testRanker) - .distanceToClosestEdge(any(Coords.class), any(Game.class)); + .when(testRanker) + .distanceToClosestEdge(any(Coords.class), any(Game.class)); doReturn(20) - .when(testRanker) - .distanceToHomeEdge(any(Coords.class), any(CardinalEdge.class), any(Game.class)); + .when(testRanker) + .distanceToHomeEdge(any(Coords.class), any(CardinalEdge.class), any(Game.class)); doReturn(12.0) - .when(testRanker) - .distanceToClosestEnemy(any(Entity.class), any(Coords.class), any(Game.class)); + .when(testRanker) + .distanceToClosestEnemy(any(Entity.class), any(Coords.class), any(Game.class)); doReturn(0.0) - .when(testRanker) - .checkPathForHazards(any(MovePath.class), any(Entity.class), any(Game.class)); + .when(testRanker) + .checkPathForHazards(any(MovePath.class), any(Entity.class), any(Game.class)); final Entity mockMover = mock(BipedMech.class); when(mockMover.isClan()).thenReturn(false); @@ -376,8 +376,8 @@ public void testRankPath() { final Coords boardCenter = spy(new Coords(8, 8)); when(mockBoard.getCenter()).thenReturn(boardCenter); doReturn(3) - .when(boardCenter) - .direction(nullable(Coords.class)); + .when(boardCenter) + .direction(nullable(Coords.class)); final GameOptions mockGameOptions = mock(GameOptions.class); when(mockGameOptions.booleanOption(eq(OptionsConstants.ALLOWED_NO_CLAN_PHYSICAL))).thenReturn(false); @@ -395,8 +395,8 @@ public void testRankPath() { final Coords enemyMech1Position = spy(new Coords(10, 10)); doReturn(3) - .when(enemyMech1Position) - .direction(nullable(Coords.class)); + .when(enemyMech1Position) + .direction(nullable(Coords.class)); final Entity mockEnemyMech1 = mock(BipedMech.class); when(mockEnemyMech1.isOffBoard()).thenReturn(false); when(mockEnemyMech1.getPosition()).thenReturn(enemyMech1Position); @@ -408,12 +408,12 @@ public void testRankPath() { evalForMockEnemyMech.setMyEstimatedPhysicalDamage(8.0); evalForMockEnemyMech.setEstimatedEnemyDamage(25.0); doReturn(evalForMockEnemyMech) - .when(testRanker) - .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); + .when(testRanker) + .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); testEnemies.add(mockEnemyMech1); doReturn(mockEnemyMech1) - .when(testRanker) - .findClosestEnemy(eq(mockMover), nullable(Coords.class), any(Game.class)); + .when(testRanker) + .findClosestEnemy(eq(mockMover), nullable(Coords.class), any(Game.class)); final Entity mockEnemyMech2 = mock(BipedMech.class); when(mockEnemyMech2.isOffBoard()).thenReturn(false); @@ -426,8 +426,8 @@ public void testRankPath() { evalForMockEnemyMech2.setMyEstimatedPhysicalDamage(0.0); evalForMockEnemyMech2.setEstimatedEnemyDamage(15.0); doReturn(evalForMockEnemyMech2) - .when(testRanker) - .evaluateUnmovedEnemy(eq(mockEnemyMech2), any(MovePath.class), anyBoolean(), anyBoolean()); + .when(testRanker) + .evaluateUnmovedEnemy(eq(mockEnemyMech2), any(MovePath.class), anyBoolean(), anyBoolean()); testEnemies.add(mockEnemyMech2); Coords friendsCoords = new Coords(10, 10); @@ -452,8 +452,8 @@ public void testRankPath() { // Change the move path success probability. doReturn(0.5) - .when(testRanker) - .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); + .when(testRanker) + .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); expected = new RankedPath(-298.125, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(250) + " = " + LOG_DECIMAL.format(0.5) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" @@ -473,8 +473,8 @@ public void testRankPath() { fail("Higher chance to fall should mean lower rank."); } doReturn(0.75) - .when(testRanker) - .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); + .when(testRanker) + .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); expected = new RankedPath(-174.6875, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(125) + " = " + LOG_DECIMAL.format(0.25) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" @@ -494,8 +494,8 @@ public void testRankPath() { fail("Higher chance to fall should mean lower rank."); } doReturn(1.0) - .when(testRanker) - .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); + .when(testRanker) + .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); // Change the damage to enemy mech 1. evalForMockEnemyMech = new EntityEvaluationResponse(); @@ -503,8 +503,8 @@ public void testRankPath() { evalForMockEnemyMech.setMyEstimatedPhysicalDamage(8.0); evalForMockEnemyMech.setEstimatedEnemyDamage(25.0); doReturn(evalForMockEnemyMech) - .when(testRanker) - .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); + .when(testRanker) + .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); expected = new RankedPath(-51.25, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(0) + " = " + LOG_DECIMAL.format(0) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" @@ -527,8 +527,8 @@ public void testRankPath() { evalForMockEnemyMech.setMyEstimatedPhysicalDamage(8.0); evalForMockEnemyMech.setEstimatedEnemyDamage(25.0); doReturn(evalForMockEnemyMech) - .when(testRanker) - .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); + .when(testRanker) + .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); expected = new RankedPath(-61.0, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(0) + " = " + LOG_DECIMAL.format(0) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-16) @@ -551,8 +551,8 @@ public void testRankPath() { evalForMockEnemyMech.setMyEstimatedPhysicalDamage(8.0); evalForMockEnemyMech.setEstimatedEnemyDamage(25.0); doReturn(evalForMockEnemyMech) - .when(testRanker) - .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); + .when(testRanker) + .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); // Change the damage done by enemy mech 1. evalForMockEnemyMech = new EntityEvaluationResponse(); @@ -560,8 +560,8 @@ public void testRankPath() { evalForMockEnemyMech.setMyEstimatedPhysicalDamage(8.0); evalForMockEnemyMech.setEstimatedEnemyDamage(35.0); doReturn(evalForMockEnemyMech) - .when(testRanker) - .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); + .when(testRanker) + .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); expected = new RankedPath(-61.25, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(0) + " = " + LOG_DECIMAL.format(0) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-16.25) @@ -584,8 +584,8 @@ public void testRankPath() { evalForMockEnemyMech.setMyEstimatedPhysicalDamage(8.0); evalForMockEnemyMech.setEstimatedEnemyDamage(15.0); doReturn(evalForMockEnemyMech) - .when(testRanker) - .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); + .when(testRanker) + .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); expected = new RankedPath(-41.25, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(0) + " = " + LOG_DECIMAL.format(0) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(3.75) @@ -608,19 +608,19 @@ public void testRankPath() { evalForMockEnemyMech.setMyEstimatedPhysicalDamage(8.0); evalForMockEnemyMech.setEstimatedEnemyDamage(25.0); doReturn(evalForMockEnemyMech) - .when(testRanker) - .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); + .when(testRanker) + .evaluateMovedEnemy(eq(mockEnemyMech1), any(MovePath.class), any(Game.class)); // Change the distance to the enemy. doReturn(2.0) - .when(testRanker) - .distanceToClosestEnemy(any(Entity.class), any(Coords.class), any(Game.class)); + .when(testRanker) + .distanceToClosestEnemy(any(Entity.class), any(Coords.class), any(Game.class)); expected = new RankedPath(-26.25, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(0) + " = " + LOG_DECIMAL.format(0) + " * " + LOG_DECIMAL.format(500) + "] + " + "braveryMod [" + LOG_DECIMAL.format(-6.25) + " = " + LOG_PERCENT.format(1) + " * ((" + LOG_DECIMAL.format(22.5) + " * " + LOG_DECIMAL.format(1.5) + ") - " - + LOG_DECIMAL.format(40) + "] - " +"aggressionMod [" + LOG_DECIMAL.format(5) + + LOG_DECIMAL.format(40) + "] - " + "aggressionMod [" + LOG_DECIMAL.format(5) + " = " + LOG_DECIMAL.format(2) + " * " + LOG_DECIMAL.format(2.5) + "] - " + "herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) @@ -633,8 +633,8 @@ public void testRankPath() { fail("The closer I am to the enemy, the higher the path rank should be."); } doReturn(22.0) - .when(testRanker) - .distanceToClosestEnemy(any(Entity.class), any(Coords.class), any(Game.class)); + .when(testRanker) + .distanceToClosestEnemy(any(Entity.class), any(Coords.class), any(Game.class)); expected = new RankedPath(-76.25, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(0) + " = " + LOG_DECIMAL.format(0) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-6.25) @@ -653,8 +653,8 @@ public void testRankPath() { fail("The further I am from the enemy, the lower the path rank should be."); } doReturn(12.0) - .when(testRanker) - .distanceToClosestEnemy(any(Entity.class), any(Coords.class), any(Game.class)); + .when(testRanker) + .distanceToClosestEnemy(any(Entity.class), any(Coords.class), any(Game.class)); // Change the distance to my friends. friendsCoords = new Coords(0, 10); @@ -696,7 +696,7 @@ public void testRankPath() { expected = new RankedPath(-36.25, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(0) + " = " + LOG_DECIMAL.format(0) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-6.25) - + " = " + LOG_PERCENT.format(1) + " * ((" + LOG_DECIMAL .format(22.5) + + " = " + LOG_PERCENT.format(1) + " * ((" + LOG_DECIMAL.format(22.5) + " * " + LOG_DECIMAL.format(1.5) + ") - " + LOG_DECIMAL.format(40) + "] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) @@ -726,8 +726,8 @@ public void testRankPath() { actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); assertRankedPathEquals(expected, actual); doReturn(10) - .when(testRanker) - .distanceToHomeEdge(any(Coords.class), any(CardinalEdge.class), any(Game.class)); + .when(testRanker) + .distanceToHomeEdge(any(Coords.class), any(CardinalEdge.class), any(Game.class)); expected = new RankedPath(-51.25, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(0) + " = " + LOG_DECIMAL.format(0) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-6.25) @@ -746,13 +746,13 @@ public void testRankPath() { fail("The closer I am to my home edge when fleeing, the higher the path rank should be."); } doReturn(30) - .when(testRanker) - .distanceToHomeEdge(any(Coords.class), any(CardinalEdge.class), any(Game.class)); + .when(testRanker) + .distanceToHomeEdge(any(Coords.class), any(CardinalEdge.class), any(Game.class)); expected = new RankedPath(-51.25, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(0) + " = " + LOG_DECIMAL.format(0) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-6.25) + " = " + LOG_PERCENT.format(1) + " * ((" + LOG_DECIMAL.format(22.5) - + " * " + LOG_DECIMAL.format(1.5) + ") - "+ LOG_DECIMAL.format(40) + + " * " + LOG_DECIMAL.format(1.5) + ") - " + LOG_DECIMAL.format(40) + "] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " @@ -765,8 +765,8 @@ public void testRankPath() { fail("The further I am from my home edge when fleeing, the lower the path rank should be."); } doReturn(20) - .when(testRanker) - .distanceToHomeEdge(nullable(Coords.class), any(CardinalEdge.class), any(Game.class)); + .when(testRanker) + .distanceToHomeEdge(nullable(Coords.class), any(CardinalEdge.class), any(Game.class)); when(mockPrincess.wantsToFallBack(eq(mockMover))).thenReturn(false); when(mockMover.isCrippled()).thenReturn(false); @@ -829,8 +829,8 @@ public void testRankPath() { // Test not being able to find an enemy. doReturn(null) - .when(testRanker) - .findClosestEnemy(eq(mockMover), nullable(Coords.class), any(Game.class)); + .when(testRanker) + .findClosestEnemy(eq(mockMover), nullable(Coords.class), any(Game.class)); expected = new RankedPath(-51.25, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(0) + " = " + LOG_DECIMAL.format(0) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-6.25) @@ -846,8 +846,8 @@ public void testRankPath() { actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); assertRankedPathEquals(expected, actual); doReturn(mockEnemyMech1) - .when(testRanker) - .findClosestEnemy(eq(mockMover), nullable(Coords.class), any(Game.class)); + .when(testRanker) + .findClosestEnemy(eq(mockMover), nullable(Coords.class), any(Game.class)); } @Test @@ -1009,7 +1009,7 @@ public void testCalculateDamagePotential() { final FiringPlan mockFiringPlan = mock(FiringPlan.class); when(mockFiringPlan.getUtility()).thenReturn(12.5); when(mockFireControl.determineBestFiringPlan(any(FiringPlanCalculationParameters.class))) - .thenReturn(mockFiringPlan); + .thenReturn(mockFiringPlan); final EntityState mockShooterState = mock(EntityState.class); final Coords mockEnemyPosition = mockEnemy.getPosition(); @@ -1055,7 +1055,7 @@ public void testCalculateMyDamagePotential() { final FiringPlan mockFiringPlan = mock(FiringPlan.class); when(mockFiringPlan.getUtility()).thenReturn(25.2); when(mockFireControl.determineBestFiringPlan(any(FiringPlanCalculationParameters.class))) - .thenReturn(mockFiringPlan); + .thenReturn(mockFiringPlan); // Test being in range and LoS. double expected = 25.2; @@ -1097,6 +1097,7 @@ private Board generateMockBoard() { * Final path facing: straight north * No SPAs * Default crew + * * @return */ private Entity generateMockEntity(int x, int y) { @@ -1134,6 +1135,7 @@ private MovePath generateMockPath(int x, int y, Entity mockEntity) { /** * Generates a mock game object. * Sets up some values for the passed-in entities as well (game IDs, and the game object itself) + * * @param entities * @return */ @@ -1155,69 +1157,90 @@ private Game generateMockGame(List entities, Board mockBoard) { return mockGame; } + public List setupCoords(String... pairs) { + List coords = new ArrayList(); + for (String pair : pairs) { + String[] xyPair = pair.split(","); + int x = Integer.parseInt(xyPair[0].strip()); + int y = Integer.parseInt(xyPair[1].strip()); + coords.add(new Coords(x, y)); + } + return coords; + } + + public List setupHexes(List coords) { + List hexes = new ArrayList(); + for (Coords c : coords) { + Hex mockHex = mock(Hex.class); + when(mockHex.getTerrainTypes()).thenReturn(new int[0]); + when(mockHex.getCoords()).thenReturn(c); + hexes.add(mockHex); + } + return hexes; + } + + public Vector setupMoveStepVector(List coords) { + Vector moves = new Vector(); + for (Coords c : coords) { + MoveStep mockStep = mock(MoveStep.class); + when(mockStep.getPosition()).thenReturn(c); + moves.add(mockStep); + } + return moves; + } + + public MovePath setupPath(Vector steps) { + Coords finalCoords = steps.lastElement().getPosition(); + MovePath mockPath = mock(MovePath.class); + when(mockPath.getLastStep()).thenReturn(steps.lastElement()); + when(mockPath.getFinalCoords()).thenReturn(finalCoords); + when(mockPath.getStepVector()).thenReturn(steps); + + return mockPath; + } + + public Game setupGame(List coords, List hexes) { + Game mockGame = mock(Game.class); + Board mockBoard = mock(Board.class); + when(mockGame.getBoard()).thenReturn(mockBoard); + for (Coords c : coords) { + when(mockBoard.getHex(eq(c))).thenReturn(hexes.get(coords.indexOf(c))); + } + return mockGame; + } + @Test public void testCheckPathForHazards() { final BasicPathRanker testRanker = spy(new BasicPathRanker(mockPrincess)); - final Coords testCoordsOne = new Coords(10, 7); - final Coords testCoordsTwo = new Coords(10, 8); - final Coords testCoordsThree = new Coords(10, 9); - final Coords testFinalCoords = new Coords(10, 10); + final List testCoords = setupCoords("10,7", "10,8", "10,9", "10,10"); + final Coords testCoordsThree = testCoords.get(2); - final Hex mockHexOne = mock(Hex.class); - final Hex mockHexTwo = mock(Hex.class); - final Hex mockHexThree = mock(Hex.class); - final Hex mockFinalHex = mock(Hex.class); - when(mockHexOne.getTerrainTypes()).thenReturn(new int[0]); - when(mockHexTwo.getTerrainTypes()).thenReturn(new int[0]); - when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[0]); - when(mockHexOne.getCoords()).thenReturn(testCoordsOne); - when(mockHexTwo.getCoords()).thenReturn(testCoordsTwo); - when(mockHexThree.getCoords()).thenReturn(testCoordsThree); - when(mockFinalHex.getCoords()).thenReturn(testFinalCoords); - - final MoveStep mockStepOne = mock(MoveStep.class); - final MoveStep mockStepTwo = mock(MoveStep.class); - final MoveStep mockStepThree = mock(MoveStep.class); - final MoveStep mockFinalStep = mock(MoveStep.class); - when(mockStepOne.getPosition()).thenReturn(testCoordsOne); - when(mockStepTwo.getPosition()).thenReturn(testCoordsTwo); - when(mockStepThree.getPosition()).thenReturn(testCoordsThree); - when(mockFinalStep.getPosition()).thenReturn(testFinalCoords); - final Vector stepVector = new Vector<>(); - stepVector.add(mockStepOne); - stepVector.add(mockStepTwo); - stepVector.add(mockStepThree); - stepVector.add(mockFinalStep); + final List testHexes = setupHexes(testCoords); + final Hex mockHexTwo = testHexes.get(1); + final Hex mockHexThree = testHexes.get(2); + final Hex mockFinalHex = testHexes.get(3); - final MovePath mockPath = mock(MovePath.class); - when(mockPath.getLastStep()).thenReturn(mockFinalStep); - when(mockPath.getFinalCoords()).thenReturn(testFinalCoords); - when(mockPath.getStepVector()).thenReturn(stepVector); + final Vector stepVector = setupMoveStepVector(testCoords); + final MoveStep mockFinalStep = stepVector.lastElement(); + + final MovePath mockPath = setupPath(stepVector); final Entity mockUnit = mock(BipedMech.class); when(mockUnit.locations()).thenReturn(8); when(mockUnit.getArmor(anyInt())).thenReturn(10); - final Game mockGame = mock(Game.class); - - final Board mockBoard = mock(Board.class); - when(mockGame.getBoard()).thenReturn(mockBoard); - when(mockBoard.getHex(eq(testFinalCoords))).thenReturn(mockFinalHex); - when(mockBoard.getHex(eq(testCoordsOne))).thenReturn(mockHexOne); - when(mockBoard.getHex(eq(testCoordsTwo))).thenReturn(mockHexTwo); - when(mockBoard.getHex(eq(testCoordsThree))).thenReturn(mockHexThree); + final Game mockGame = setupGame(testCoords, testHexes); final Crew mockCrew = mock(Crew.class); when(mockUnit.getCrew()).thenReturn(mockCrew); when(mockCrew.getPiloting()).thenReturn(5); final Building mockBuilding = mock(Building.class); - when(mockBoard.getBuildingAt(eq(testCoordsThree))).thenReturn(mockBuilding); + when(mockGame.getBoard().getBuildingAt(eq(testCoordsThree))).thenReturn(mockBuilding); when(mockBuilding.getCurrentCF(eq(testCoordsThree))).thenReturn(77); - // Test waking fire-resistant BA through a burning building. + // Test walking fire-resistant BA through a burning building. final BattleArmor mockBA = mock(BattleArmor.class); when(mockBA.locations()).thenReturn(5); when(mockBA.getArmor(anyInt())).thenReturn(5); @@ -1348,7 +1371,9 @@ public void testCheckPathForHazards() { when(mockFinalHex.depth()).thenReturn(0); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MAGMA}); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); - assertEquals(14.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(32.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + assertEquals(59.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(0); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.WOODS, Terrains.FIRE}); assertEquals(5.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); @@ -1359,4 +1384,231 @@ public void testCheckPathForHazards() { when(mockPath.getLastStepMovementType()).thenReturn(EntityMovementType.MOVE_FLYING); assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); } + + @Test + public void testMagmaHazard() { + final BasicPathRanker testRanker = spy(new BasicPathRanker(mockPrincess)); + + final List testCoords = setupCoords("10,7", "10,8", "10,9", "10,10"); + final Coords testCoordsThree = testCoords.get(2); + + final List testHexes = setupHexes(testCoords); + final Hex mockFinalHex = testHexes.get(3); + + final Vector stepVector = setupMoveStepVector(testCoords); + + final MovePath mockPath = setupPath(stepVector); + + final Entity mockUnit = mock(BipedMech.class); + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + + final Game mockGame = setupGame(testCoords, testHexes); + + final Crew mockCrew = mock(Crew.class); + when(mockUnit.getCrew()).thenReturn(mockCrew); + when(mockCrew.getPiloting()).thenReturn(5); + + final Building mockBuilding = mock(Building.class); + when(mockGame.getBoard().getBuildingAt(eq(testCoordsThree))).thenReturn(mockBuilding); + when(mockBuilding.getCurrentCF(eq(testCoordsThree))).thenReturn(77); + + // Test jumping onto Magma Crust. + when(mockPath.isJumping()).thenReturn(true); + when(mockUnit.getArmor(eq(Mech.LOC_LLEG))).thenReturn(10); + when(mockFinalHex.depth()).thenReturn(0); + when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MAGMA}); + // Only 50% chance to break through Crust, but must make PSR to avoid getting bogged down. + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); + assertEquals(32.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + // 100% chance to take damage when Magma is Liquid (aka Lava) and PSR chance to get stuck. + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + assertEquals(59.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + + // Test jumping with worse piloting score (hazard should increase quickly) + when(mockPath.isJumping()).thenReturn(true); + when(mockUnit.getArmor(eq(Mech.LOC_LLEG))).thenReturn(10); + when(mockFinalHex.depth()).thenReturn(0); + when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MAGMA}); + // Only 50% chance to break through Crust + when(mockCrew.getPiloting()).thenReturn(6); + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); + assertEquals(39.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + // 100% chance to take damage when Magma is Liquid (aka Lava) + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + assertEquals(74.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + // Only 50% chance to break through Crust + when(mockCrew.getPiloting()).thenReturn(7); + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); + assertEquals(51.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + // 100% chance to take damage when Magma is Liquid (aka Lava) + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + assertEquals(97.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + + // Check damaged Hover ending on Magma crust + // 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(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + when(mockTank.getDamageLevel()).thenReturn(0); + assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + + when(mockTank.getDamageLevel()).thenReturn(1); + assertEquals(250.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + + when(mockTank.getDamageLevel()).thenReturn(2); + assertEquals(500.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + + // Not as severe over Crust + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); + when(mockTank.getDamageLevel()).thenReturn(0); + assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + + when(mockTank.getDamageLevel()).thenReturn(1); + assertEquals(41.675, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + + when(mockTank.getDamageLevel()).thenReturn(2); + assertEquals(83.350, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + } + + + @Test + public void testSwampHazard() { + final BasicPathRanker testRanker = spy(new BasicPathRanker(mockPrincess)); + + final List testCoords = setupCoords("10,7", "10,8", "10,9", "10,10"); + final Coords testCoordsThree = testCoords.get(2); + + final List testHexes = setupHexes(testCoords); + final Hex mockFinalHex = testHexes.get(3); + + final Vector stepVector = setupMoveStepVector(testCoords); + + final MovePath mockPath = setupPath(stepVector); + + final Entity mockUnit = mock(BipedMech.class); + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + when(mockUnit.getHeight()).thenReturn(2); + + final Game mockGame = setupGame(testCoords, testHexes); + + final Crew mockCrew = mock(Crew.class); + when(mockUnit.getCrew()).thenReturn(mockCrew); + when(mockCrew.getPiloting()).thenReturn(5); + + final Building mockBuilding = mock(Building.class); + when(mockGame.getBoard().getBuildingAt(eq(testCoordsThree))).thenReturn(mockBuilding); + when(mockBuilding.getCurrentCF(eq(testCoordsThree))).thenReturn(77); + + // Test jumping onto Swamp, Swamp-turned-Quicksand, and Quicksand. + // Hazard for Quicksand is _very_ high due to PSR mod of +3 and height+1 turns to total destruction. + when(mockPath.isJumping()).thenReturn(true); + when(mockFinalHex.depth()).thenReturn(0); + when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.SWAMP}); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(1); + assertEquals(35.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(2); + assertEquals(3033.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(3); + assertEquals(3033.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + + // Test walking into Swamp, Swamp-turned-Quicksand, and Quicksand. + // Hazard is lower due to better chance to escape getting bogged down initially, but still high. + when(mockPath.isJumping()).thenReturn(false); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(1); + assertEquals(28.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(2); + assertEquals(2941.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(3); + assertEquals(2941.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + + // Test non-hover vehicle hazard + // It takes one fewer round to destroy a 1-height tank _and_ the initial PSR is harder! + 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.TRACKED); + when(mockUnit.getHeight()).thenReturn(1); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(1); + assertEquals(112.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(2); + assertEquals(14519.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(3); + assertEquals(14519.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + + // Confirm hovers are immune + when(mockTank.getMovementMode()).thenReturn(EntityMovementMode.HOVER); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(1); + assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(2); + assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(3); + assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + } + + @Test + public void testMudHazard() { + final BasicPathRanker testRanker = spy(new BasicPathRanker(mockPrincess)); + + final List testCoords = setupCoords("10,7", "10,8", "10,9", "10,10"); + final Coords testCoordsThree = testCoords.get(2); + + final List testHexes = setupHexes(testCoords); + final Hex mockFinalHex = testHexes.get(3); + + final Vector stepVector = setupMoveStepVector(testCoords); + + final MovePath mockPath = setupPath(stepVector); + + final Entity mockUnit = mock(BipedMech.class); + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + when(mockUnit.getHeight()).thenReturn(2); + + final Game mockGame = setupGame(testCoords, testHexes); + + final Crew mockCrew = mock(Crew.class); + when(mockUnit.getCrew()).thenReturn(mockCrew); + when(mockCrew.getPiloting()).thenReturn(5); + + final Building mockBuilding = mock(Building.class); + when(mockGame.getBoard().getBuildingAt(eq(testCoordsThree))).thenReturn(mockBuilding); + when(mockBuilding.getCurrentCF(eq(testCoordsThree))).thenReturn(77); + + // Test walking onto mud; jumping doesn't change danger because Mechs can't bog down here + // Small hazard to Mechs due to PSR malus + when(mockPath.isJumping()).thenReturn(false); + when(mockFinalHex.depth()).thenReturn(0); + when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MUD}); + assertEquals(3.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + + // Test non-hover vehicle hazard + // PSR malus and chance to bog down makes this slightly hazardous for vehicles + 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.TRACKED); + when(mockUnit.getHeight()).thenReturn(1); + when(mockFinalHex.terrainLevel(Terrains.MUD)).thenReturn(1); + assertEquals(31.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + + // Confirm hovers are immune + when(mockTank.getMovementMode()).thenReturn(EntityMovementMode.HOVER); + when(mockFinalHex.terrainLevel(Terrains.MUD)).thenReturn(1); + assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + } } From e1da6f3b72f4b7ca802598e7dbe1db4f9d05a8ff Mon Sep 17 00:00:00 2001 From: Richard J Hancock Date: Mon, 29 Jul 2024 14:23:54 -0500 Subject: [PATCH 25/32] Additional refinements related to INI and script names. Updated libraries. --- megamek/build.gradle | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/megamek/build.gradle b/megamek/build.gradle index 43df398a623..c253e628cbb 100644 --- a/megamek/build.gradle +++ b/megamek/build.gradle @@ -5,7 +5,7 @@ plugins { id 'edu.sc.seis.launch4j' version '3.0.6' id 'jacoco' id 'java' - id "io.sentry.jvm.gradle" version '4.9.0' + id "io.sentry.jvm.gradle" version '4.10.0' id 'com.palantir.git-version' version '3.1.0' } @@ -34,15 +34,15 @@ sourceSets { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-core:2.17.1' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1' - implementation 'com.formdev:flatlaf:3.4.1' - implementation 'com.formdev:flatlaf-extras:3.4.1' + implementation 'com.fasterxml.jackson.core:jackson-core:2.17.2' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.2' + implementation 'com.formdev:flatlaf:3.5' + implementation 'com.formdev:flatlaf-extras:3.5' implementation 'com.sun.mail:jakarta.mail:2.0.1' implementation 'com.thoughtworks.xstream:xstream:1.4.20' implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2' implementation 'org.apache.commons:commons-collections4:4.5.0-M2' - implementation 'org.apache.commons:commons-lang3:3.14.0' + implementation 'org.apache.commons:commons-lang3:3.15.0' implementation 'org.apache.commons:commons-text:1.12.0' implementation 'org.apache.logging.log4j:log4j-api:2.23.1' implementation 'org.apache.logging.log4j:log4j-core:2.23.1' @@ -50,7 +50,7 @@ dependencies { runtimeOnly 'org.glassfish.jaxb:jaxb-runtime:4.0.5' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.3' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.3' testImplementation 'org.mockito:mockito-core:5.12.0' @@ -77,9 +77,7 @@ ext { log = "logs" mmconf = "mmconf" userdata = 'userdata' - distributionDir = "${buildDir}/distributions" fileStagingDir = "${buildDir}/files" - mmRepoDir = "${buildDir}/repo" atlasedImages = "${fileStagingDir}/atlasedImages.txt" scriptsDir = "${projectDir}/scripts" scriptTemplate = "${scriptsDir}/startScriptTemplate.txt" @@ -129,6 +127,7 @@ task equipmentList(type: JavaExec, dependsOn: jar) { task copyFiles(type: Copy) { description = 'Stages files that are to be copied into the distribution.' + group = 'build' dependsOn officialUnitList dependsOn equipmentList @@ -141,7 +140,7 @@ task copyFiles(type: Copy) { include "SubmitBug.html" include "license.txt" include "sentry.properties" - include "MegaMek.l4j.ini" + include "*.ini" exclude "**/*.psd" // No need to copy the files that are going to be zipped exclude { it.file.isDirectory() && (it.file in file(unitFiles).listFiles()) } @@ -149,12 +148,6 @@ task copyFiles(type: Copy) { include "${userdata}/" into fileStagingDir - - inputs.dir "${data}" - inputs.dir "${docs}" - inputs.dir "${mmconf}" - inputs.files 'license.txt', 'SubmitBug.html', 'sentry.properties', 'MegaMek.l4j.ini' - outputs.dir fileStagingDir } task createImageAtlases(type: JavaExec, dependsOn: copyFiles) { @@ -213,7 +206,7 @@ task stageFiles { task createStartScripts (type: CreateStartScripts) { description = 'Create shell script for generic distribution.' - applicationName = 'mm' + applicationName = 'megamek' mainClass = application.mainClass outputDir = startScripts.outputDir classpath = jar.outputs.files + files(project.sourceSets.main.runtimeClasspath.files) @@ -239,13 +232,12 @@ distributions { contents { from ("${buildDir}/launch4j") { include '*.exe' - include '*.ini' } from(jar) { into "${lib}" } from(createStartScripts.outputs.files) { - include "mm*" + include "megamek*" } from(jar) from fileStagingDir From ac6e56a387b1ad12f659f57105996d00bbc65c3a Mon Sep 17 00:00:00 2001 From: sleet01 Date: Mon, 29 Jul 2024 13:45:41 -0700 Subject: [PATCH 26/32] Modified some checks, added unit test --- .../client/bot/princess/BasicPathRanker.java | 78 ++++++++++++++++--- .../bot/princess/BasicPathRankerTest.java | 60 ++++++++++++-- 2 files changed, 121 insertions(+), 17 deletions(-) diff --git a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java index 99a6636d586..fb2a0852d0c 100644 --- a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java @@ -19,6 +19,7 @@ import megamek.client.bot.princess.UnitBehavior.BehaviorType; import megamek.common.*; import megamek.common.options.OptionsConstants; +import megamek.common.planetaryconditions.PlanetaryConditions; import org.apache.logging.log4j.LogManager; import java.text.DecimalFormat; @@ -49,6 +50,8 @@ public class BasicPathRanker extends PathRanker implements IPathRanker { // whether they will target me. private Map bestDamageByEnemies; + protected int blackIce = -1; + public BasicPathRanker(Princess owningPrincess) { super(owningPrincess); @@ -474,6 +477,12 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal Entity movingUnit = path.getEntity(); StringBuilder formula = new StringBuilder("Calculation: {"); + if (blackIce == -1) { + blackIce = ((game.getOptions().booleanOption(OptionsConstants.ADVANCED_BLACK_ICE) + && game.getPlanetaryConditions().getTemperature() <= PlanetaryConditions.BLACK_ICE_TEMP) + || game.getPlanetaryConditions().getWeather().isIceStorm()) ? 1 : 0; + } + // Copy the path to avoid inadvertent changes. MovePath pathCopy = path.clone(); @@ -791,7 +800,13 @@ private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, Mo Terrains.BRIDGE, Terrains.BLACK_ICE, Terrains.SWAMP, - Terrains.MUD)); + Terrains.MUD, + Terrains.TUNDRA)); + + // Black Ice can appear if the conditions are favorable + if (blackIce > 0) { + HAZARDS.add(Terrains.PAVEMENT); + } int[] terrainTypes = hex.getTerrainTypes(); Set hazards = new HashSet<>(); @@ -843,6 +858,13 @@ private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, Mo case Terrains.MUD: hazardValue += calcMudHazard(endHex, movingUnit, logMsg); break; + case Terrains.TUNDRA: + hazardValue += calcTundraHazard(endHex, jumpLanding, movingUnit, logMsg); + break; + case Terrains.PAVEMENT: + // 1 in 3 chance to hit Black Ice on any given Pavement hex + hazardValue += calcIceHazard(movingUnit, hex, step, movePath, jumpLanding, logMsg) / 3.0; + break; } } @@ -921,11 +943,24 @@ private double calcIceHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath return 0; } + // Categorize chance to skid / fall + double hazard = 0.0; + if (!movePath.isCareful()) { + // Most falling and skidding damage is weight-based... + double arbitraryHazard = movingUnit.getWeight(); + hazard += Math.round(arbitraryHazard * + (1 - (Compute.oddsAbove(movingUnit.getCrew().getPiloting()) / 100.0))); + if (movingUnit.isReckless()) { + // Double the hazard for Reckless + hazard *= 2; + } + } + // If there is no water under the ice, don't worry about breaking // through. if (hex.depth() < 1) { logMsg.append("No water under ice (0)."); - return 0; + return hazard; } // Hazard is based on chance to break through to the water underneath. @@ -933,7 +968,7 @@ private double calcIceHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath logMsg.append("\n\t\tChance to break through ice: ") .append(LOG_PERCENT.format(breakthroughMod)); - double hazard = calcWaterHazard(movingUnit, hex, step, movePath, logMsg) * + hazard += calcWaterHazard(movingUnit, hex, step, movePath, logMsg) * breakthroughMod; logMsg.append("\n\t\tHazard value (") .append(LOG_DECIMAL.format(hazard)).append(")."); @@ -1295,11 +1330,11 @@ private double calcSwampHazard(Hex hex, boolean endHex, Entity movingUnit, logMsg.append("\nBase hazard value: ").append(LOG_DECIMAL.format(hazard)); // Mod is to difficulty, not to PSR roll results - // Quicksand makes PSRs an additional +3! - int psrMod = (movingUnit instanceof Mech) ? +1 : +2; - psrMod += (quicksand) ? +3 : 0; + // Quicksand makes PSRs an additional +3! Otherwise +1 for Mechs, +2 for all other types + int psrMod = (quicksand) ? +3 : (movingUnit instanceof Mech) ? +1 : +2; - int pilotSkill = movingUnit.getCrew().getPiloting(); + // Infantry use 4+ check instead of Pilot / Driving skill + int pilotSkill = (movingUnit.isInfantry()) ? 4 : movingUnit.getCrew().getPiloting(); double factor = calcBogDownFactor(type, endHex, jumpLanding, pilotSkill, psrMod, logMsg); logMsg.append("\nFactor applied to hazard value: ").append(LOG_DECIMAL.format(factor)); @@ -1321,8 +1356,10 @@ private double calcMudHazard(boolean endHex, Entity movingUnit, StringBuilder lo return 0; } - int psrMod = +1; - int pilotSkill = movingUnit.getCrew().getPiloting(); + // PSR checks _to bog down_ and _escape bogged down_ are at -1; all others are at +1! + int psrMod = -1; + // Infantry use 4+ check instead of Pilot / Driving skill + int pilotSkill = (movingUnit.isInfantry()) ? 4 : movingUnit.getCrew().getPiloting(); double hazard; if (movingUnit instanceof Mech) { @@ -1340,6 +1377,29 @@ private double calcMudHazard(boolean endHex, Entity movingUnit, StringBuilder lo return Math.round(hazard); } + private double calcTundraHazard(boolean endHex, boolean jumpLanding, Entity movingUnit, StringBuilder logMsg) { + logMsg.append("\n\tCalculating Tundra hazard: "); + + // Hover units are above the surface. + if (EntityMovementMode.HOVER == movingUnit.getMovementMode() + || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { + logMsg.append("Hovering above Tundra (0)."); + return 0; + } + + // PSR checks _to bog down_ and _escape bogged down_ are at -1; all others are at +1! + int psrMod = -1; + // Infantry use 4+ check instead of Pilot / Driving skill + int pilotSkill = (movingUnit.isInfantry()) ? 4 : movingUnit.getCrew().getPiloting(); + double hazard; + + // Base hazard is arbitrarily set to 10 + hazard = 10 * calcBogDownFactor( + "Tundra", endHex, jumpLanding, pilotSkill, psrMod, logMsg); + logMsg.append("\nBase hazard value: ").append(LOG_DECIMAL.format(hazard)); + return Math.round(hazard); + } + /** * Simple data structure that holds a separate firing and physical damage number. * diff --git a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java index 98ab64a53ea..aa1aa078104 100644 --- a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java +++ b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java @@ -29,6 +29,8 @@ import megamek.common.options.GameOptions; import megamek.common.options.OptionsConstants; import megamek.common.options.PilotOptions; +import megamek.common.planetaryconditions.PlanetaryConditions; +import megamek.common.planetaryconditions.Weather; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -381,11 +383,17 @@ public void testRankPath() { final GameOptions mockGameOptions = mock(GameOptions.class); when(mockGameOptions.booleanOption(eq(OptionsConstants.ALLOWED_NO_CLAN_PHYSICAL))).thenReturn(false); + when(mockGameOptions.booleanOption(eq(OptionsConstants.ADVANCED_BLACK_ICE))).thenReturn(false); + + final PlanetaryConditions mockPC = new PlanetaryConditions(); + mockPC.setTemperature(25); + mockPC.setWeather(Weather.CLEAR); final Game mockGame = mock(Game.class); when(mockGame.getBoard()).thenReturn(mockBoard); when(mockGame.getOptions()).thenReturn(mockGameOptions); when(mockGame.getArtilleryAttacks()).thenReturn(Collections.emptyEnumeration()); + when(mockGame.getPlanetaryConditions()).thenReturn(mockPC); when(mockPrincess.getGame()).thenReturn(mockGame); final List testEnemies = new ArrayList<>(); @@ -1516,9 +1524,9 @@ public void testSwampHazard() { when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(1); assertEquals(35.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(2); - assertEquals(3033.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(2094.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(3); - assertEquals(3033.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(2094.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Test walking into Swamp, Swamp-turned-Quicksand, and Quicksand. // Hazard is lower due to better chance to escape getting bogged down initially, but still high. @@ -1526,9 +1534,9 @@ public void testSwampHazard() { when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(1); assertEquals(28.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(2); - assertEquals(2941.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(1955.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(3); - assertEquals(2941.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(1955.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Test non-hover vehicle hazard // It takes one fewer round to destroy a 1-height tank _and_ the initial PSR is harder! @@ -1543,9 +1551,9 @@ public void testSwampHazard() { when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(1); assertEquals(112.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(2); - assertEquals(14519.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + assertEquals(5865.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.SWAMP)).thenReturn(3); - assertEquals(14519.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + assertEquals(5865.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); // Confirm hovers are immune when(mockTank.getMovementMode()).thenReturn(EntityMovementMode.HOVER); @@ -1591,7 +1599,7 @@ public void testMudHazard() { when(mockPath.isJumping()).thenReturn(false); when(mockFinalHex.depth()).thenReturn(0); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MUD}); - assertEquals(3.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(2.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Test non-hover vehicle hazard // PSR malus and chance to bog down makes this slightly hazardous for vehicles @@ -1604,11 +1612,47 @@ public void testMudHazard() { when(mockTank.getMovementMode()).thenReturn(EntityMovementMode.TRACKED); when(mockUnit.getHeight()).thenReturn(1); when(mockFinalHex.terrainLevel(Terrains.MUD)).thenReturn(1); - assertEquals(31.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + assertEquals(20.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); // Confirm hovers are immune when(mockTank.getMovementMode()).thenReturn(EntityMovementMode.HOVER); when(mockFinalHex.terrainLevel(Terrains.MUD)).thenReturn(1); assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); } + + @Test + public void testBlackIceHazard() { + + final BasicPathRanker testRanker = spy(new BasicPathRanker(mockPrincess)); + testRanker.blackIce = 1; + + final List testCoords = setupCoords("10,7", "10,8", "10,9", "10,10"); + + final List testHexes = setupHexes(testCoords); + final Hex mockPenultimateHex = testHexes.get(2); + + final Vector stepVector = setupMoveStepVector(testCoords); + + final MovePath mockPath = setupPath(stepVector); + + final Entity mockUnit = mock(BipedMech.class); + when(mockUnit.getWeight()).thenReturn(70.0); + when(mockUnit.locations()).thenReturn(8); + when(mockUnit.getArmor(anyInt())).thenReturn(10); + when(mockUnit.getHeight()).thenReturn(2); + when(mockPath.isJumping()).thenReturn(false); + + final Game mockGame = setupGame(testCoords, testHexes); + + final Crew mockCrew = mock(Crew.class); + when(mockUnit.getCrew()).thenReturn(mockCrew); + when(mockCrew.getPiloting()).thenReturn(5); + + // Test visible black ice hazard value + when(mockPenultimateHex.getTerrainTypes()).thenReturn(new int[]{Terrains.BLACK_ICE}); + assertEquals(12.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + // Test _possible_ black ice hazard value (1/3 lower) + when(mockPenultimateHex.getTerrainTypes()).thenReturn(new int[]{Terrains.PAVEMENT}); + assertEquals(4.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + } } From 9a267ab8bda8893f76811706f2bc2946801cf9b8 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Mon, 29 Jul 2024 15:58:58 -0700 Subject: [PATCH 27/32] Test refinements, add progressive Magma hazard increase for damaged units --- .../client/bot/princess/BasicPathRanker.java | 9 ++-- .../bot/princess/BasicPathRankerTest.java | 43 +++++++++++++++---- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java index fb2a0852d0c..7935ca4b881 100644 --- a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java @@ -1164,8 +1164,8 @@ private double calcMagmaHazard(Hex hex, boolean endHex, Entity movingUnit, .append(LOG_PERCENT.format(breakThroughMod)); // Factor in the chance to break through. - double lavalHazard = calcLavaHazard(endHex, jumpLanding, movingUnit, step, - logMsg) * breakThroughMod; + double lavalHazard = Math.round(calcLavaHazard(endHex, jumpLanding, movingUnit, step, + logMsg) * breakThroughMod); logMsg.append("\n\t\t\tLava hazard (") .append(LOG_DECIMAL.format(lavalHazard)).append(")."); hazardValue += lavalHazard; @@ -1186,6 +1186,7 @@ private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity moving MoveStep step, StringBuilder logMsg) { logMsg.append("\n\tCalculating laval hazard: "); + int unitDamageLevel = movingUnit.getDamageLevel(); double dmg; // Hovers/VTOLs are unaffected _unless_ they end on the hex and are in danger of losing mobility. @@ -1197,7 +1198,7 @@ private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity moving } 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 = (movingUnit.getDamageLevel()/4.0) * UNIT_DESTRUCTION_FACTOR; + dmg = (unitDamageLevel/4.0) * UNIT_DESTRUCTION_FACTOR; logMsg.append("Ending hover/VTOL movement over lava ("); logMsg.append(LOG_DECIMAL.format(dmg)).append(")."); return dmg; @@ -1251,7 +1252,7 @@ private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity moving logMsg.append("legs ("); } logMsg.append(LOG_DECIMAL.format(dmg)).append(")."); - hazardValue += dmg; + hazardValue += (unitDamageLevel + 1) * dmg; // Multiply total hazard value by the chance of getting stuck for 1 or more additional turns logMsg.append("\nFactor applied to hazard value: ").append(LOG_DECIMAL.format(psrFactor)); diff --git a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java index aa1aa078104..1c473548ee5 100644 --- a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java +++ b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java @@ -1268,7 +1268,7 @@ public void testCheckPathForHazards() { when(mockPath.isJumping()).thenReturn(false); when(mockHexThree.getTerrainTypes()).thenReturn(new int[]{Terrains.MAGMA}); when(mockHexThree.terrainLevel(Terrains.MAGMA)).thenReturn(1); - assertEquals(166.7, testRanker.checkPathForHazards(mockPath, mockProto, mockGame), TOLERANCE); + assertEquals(167.0, testRanker.checkPathForHazards(mockPath, mockProto, mockGame), TOLERANCE); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); when(mockHexThree.terrainLevel(Terrains.MAGMA)).thenReturn(0); @@ -1341,7 +1341,7 @@ public void testCheckPathForHazards() { when(mockHexTwo.terrainLevel(Terrains.MAGMA)).thenReturn(1); when(mockHexThree.terrainLevel(Terrains.MAGMA)).thenReturn(1); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); - assertEquals(17.8351, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(17.500, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockHexTwo.getTerrainTypes()).thenReturn(new int[0]); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[0]); @@ -1379,7 +1379,7 @@ public void testCheckPathForHazards() { when(mockFinalHex.depth()).thenReturn(0); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MAGMA}); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); - assertEquals(32.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(32.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); assertEquals(59.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(0); @@ -1428,9 +1428,10 @@ public void testMagmaHazard() { when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MAGMA}); // Only 50% chance to break through Crust, but must make PSR to avoid getting bogged down. when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); - assertEquals(32.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(32.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // 100% chance to take damage when Magma is Liquid (aka Lava) and PSR chance to get stuck. when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + when(mockFinalHex.depth()).thenReturn(1); assertEquals(59.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Test jumping with worse piloting score (hazard should increase quickly) @@ -1444,16 +1445,41 @@ public void testMagmaHazard() { assertEquals(39.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // 100% chance to take damage when Magma is Liquid (aka Lava) when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + when(mockFinalHex.depth()).thenReturn(1); assertEquals(74.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Only 50% chance to break through Crust when(mockCrew.getPiloting()).thenReturn(7); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); - assertEquals(51.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + when(mockFinalHex.depth()).thenReturn(0); + assertEquals(51.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // 100% chance to take damage when Magma is Liquid (aka Lava) when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + when(mockFinalHex.depth()).thenReturn(1); assertEquals(97.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); - // Check damaged Hover ending on Magma crust + // Test damaged 'mech walking hazard (should increase hazard as damage level increases) + when(mockCrew.getPiloting()).thenReturn(5); + when(mockPath.isJumping()).thenReturn(false); + when(mockUnit.getArmor(eq(Mech.LOC_LLEG))).thenReturn(10); + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); + when(mockFinalHex.depth()).thenReturn(0); + // Moderate damage means moderate hazard + when(mockUnit.getDamageLevel()).thenReturn(Entity.DMG_MODERATE); + assertEquals(13.1665, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + when(mockFinalHex.depth()).thenReturn(1); + assertEquals(52, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + // Crippled should be very high hazard + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); + when(mockFinalHex.depth()).thenReturn(0); + when(mockUnit.getDamageLevel()).thenReturn(Entity.DMG_CRIPPLED); + assertEquals(17.1665, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + when(mockFinalHex.depth()).thenReturn(1); + assertEquals(80, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + + + // Check damaged Hover ending on Liquid Magma // Ramps up quickly with damage state! final Entity mockTank = mock(Tank.class); when(mockTank.locations()).thenReturn(5); @@ -1465,6 +1491,7 @@ public void testMagmaHazard() { when(mockTank.getHeatCapacity()).thenReturn(Entity.DOES_NOT_TRACK_HEAT); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); + when(mockFinalHex.depth()).thenReturn(1); when(mockTank.getDamageLevel()).thenReturn(0); assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); @@ -1480,10 +1507,10 @@ public void testMagmaHazard() { assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); when(mockTank.getDamageLevel()).thenReturn(1); - assertEquals(41.675, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + assertEquals(42.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); when(mockTank.getDamageLevel()).thenReturn(2); - assertEquals(83.350, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); + assertEquals(83.0, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); } From 62161aacb21d501096cddba6023289b1f3e5eeeb Mon Sep 17 00:00:00 2001 From: sleet01 Date: Mon, 29 Jul 2024 18:21:09 -0700 Subject: [PATCH 28/32] Update unit test values for higher PSR mods; remove bog down test walking into Lava --- .../client/bot/princess/BasicPathRanker.java | 11 +-- megamek/src/megamek/common/Terrain.java | 1 + megamek/src/megamek/server/GameManager.java | 77 ++++++++++--------- .../bot/princess/BasicPathRankerTest.java | 18 ++--- 4 files changed, 57 insertions(+), 50 deletions(-) diff --git a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java index 7935ca4b881..8cac7c40293 100644 --- a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java @@ -1222,7 +1222,8 @@ private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity moving // Latter is: N = log(desired failure to escape chance, e.g. 10%)/log(%chance Fail PSR) logMsg.append("Possibly jumping onto lava hex, may get bogged down."); int pilotSkill = movingUnit.getCrew().getPiloting(); - double oddsPSR = Compute.oddsAbove(pilotSkill) / 100; + int psrMod = +4; + double oddsPSR = Compute.oddsAbove(pilotSkill + psrMod) / 100; double oddsBogged = (1.0 - oddsPSR); double expectedTurns = Math.log10(0.10)/Math.log10(oddsBogged); logMsg.append("\n\t\tEffective Piloting Skill: ").append(LOG_INT.format(pilotSkill)); @@ -1357,8 +1358,8 @@ private double calcMudHazard(boolean endHex, Entity movingUnit, StringBuilder lo return 0; } - // PSR checks _to bog down_ and _escape bogged down_ are at -1; all others are at +1! - int psrMod = -1; + // PSR checks _to bog down_ and _escape bogged down_ are at (mod - 1); all others are at +1 mod + int psrMod = 0; // Infantry use 4+ check instead of Pilot / Driving skill int pilotSkill = (movingUnit.isInfantry()) ? 4 : movingUnit.getCrew().getPiloting(); double hazard; @@ -1388,8 +1389,8 @@ private double calcTundraHazard(boolean endHex, boolean jumpLanding, Entity movi return 0; } - // PSR checks _to bog down_ and _escape bogged down_ are at -1; all others are at +1! - int psrMod = -1; + // PSR checks _to bog down_ and _escape bogged down_ are at (mod - 1); all others are at +1 mod + int psrMod = 0; // Infantry use 4+ check instead of Pilot / Driving skill int pilotSkill = (movingUnit.isInfantry()) ? 4 : movingUnit.getCrew().getPiloting(); double hazard; diff --git a/megamek/src/megamek/common/Terrain.java b/megamek/src/megamek/common/Terrain.java index c14cf3cb373..0c806d4b368 100644 --- a/megamek/src/megamek/common/Terrain.java +++ b/megamek/src/megamek/common/Terrain.java @@ -607,6 +607,7 @@ public int getBogDownModifier(EntityMovementMode moveMode, boolean largeVee) { return 0; } case MAGMA: + // Only applies when jumping into a hex. return (level == 2) ? 0 : TargetRoll.AUTOMATIC_SUCCESS; case MUD: if (moveMode.isBiped() || moveMode.isQuad()) { diff --git a/megamek/src/megamek/server/GameManager.java b/megamek/src/megamek/server/GameManager.java index 2eca511c570..be1194b0f41 100644 --- a/megamek/src/megamek/server/GameManager.java +++ b/megamek/src/megamek/server/GameManager.java @@ -6864,45 +6864,50 @@ private void processMovement(Entity entity, MovePath md, Map Date: Mon, 29 Jul 2024 23:04:41 -0700 Subject: [PATCH 29/32] Update Princess debug reporting to omit mention of bogging down when walking in Liquid Magma --- .../src/megamek/client/ui/SharedUtility.java | 95 ++++++++++--------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/megamek/src/megamek/client/ui/SharedUtility.java b/megamek/src/megamek/client/ui/SharedUtility.java index e3f136dbbb1..8eb69d23d9b 100644 --- a/megamek/src/megamek/client/ui/SharedUtility.java +++ b/megamek/src/megamek/client/ui/SharedUtility.java @@ -310,6 +310,9 @@ private static Object doPSRCheck(MovePath md, boolean stringResult) { // check for magma int level = curHex.terrainLevel(Terrains.MAGMA); + boolean jumpedIntoMagma = (curPos.equals(lastPos) + && (curHex.terrainLevel(Terrains.MAGMA) == 2) + && (moveType == EntityMovementType.MOVE_JUMP)); if ((level == 1) && (step.getElevation() == 0) && (entity.getMovementMode() != EntityMovementMode.HOVER) && (moveType != EntityMovementType.MOVE_JUMP) @@ -336,53 +339,55 @@ private static Object doPSRCheck(MovePath md, boolean stringResult) { checkNag(rollTarget, nagReport, psrList); } - // check if we've moved into swamp - rollTarget = entity.checkBogDown(step, overallMoveType, curHex, - lastPos, curPos, lastElevation, isPavementStep); - checkNag(rollTarget, nagReport, psrList); + // check if we've moved into swamp; skip Liquid Magma bog-down check if not jumping into the last hex + if (level != 2 || jumpedIntoMagma) { + rollTarget = entity.checkBogDown(step, overallMoveType, curHex, + lastPos, curPos, lastElevation, isPavementStep); + checkNag(rollTarget, nagReport, psrList); - // Check if used more MPs than Mech/Vehicle would have w/o gravity - if (!i.hasMoreElements() && !firstStep) { - if ((entity instanceof Mech) || (entity instanceof Tank)) { - if ((moveType == EntityMovementType.MOVE_WALK) - || (moveType == EntityMovementType.MOVE_VTOL_WALK) - || (moveType == EntityMovementType.MOVE_RUN) - || (moveType == EntityMovementType.MOVE_VTOL_RUN) - || (moveType == EntityMovementType.MOVE_SPRINT) - || (moveType == EntityMovementType.MOVE_VTOL_SPRINT)) { - int limit = entity.getRunningGravityLimit(); - if (step.isOnlyPavement() && entity.isEligibleForPavementBonus()) { - limit++; - } - if (step.getMpUsed() > limit) { - rollTarget = entity.checkMovedTooFast(step, overallMoveType); - checkNag(rollTarget, nagReport, psrList); - } - } else if (moveType == EntityMovementType.MOVE_JUMP) { - int origWalkMP = entity.getWalkMP(MPCalculationSetting.NO_GRAVITY); - int gravWalkMP = entity.getWalkMP(); - if (step.getMpUsed() > entity.getJumpMP(MPCalculationSetting.NO_GRAVITY)) { - rollTarget = entity.checkMovedTooFast(step, overallMoveType); - checkNag(rollTarget, nagReport, psrList); - } else if ((game.getPlanetaryConditions().getGravity() > 1) - && ((origWalkMP - gravWalkMP) > 0)) { - rollTarget = entity.getBasePilotingRoll(md.getLastStepMovementType()); - entity.addPilotingModifierForTerrain(rollTarget, step); - int gravMod = game.getPlanetaryConditions() - .getGravityPilotPenalty(); - if ((gravMod != 0) && !game.getBoard().inSpace()) { - rollTarget.addModifier(gravMod, game - .getPlanetaryConditions().getGravity() - + "G gravity"); + // Check if used more MPs than Mech/Vehicle would have w/o gravity + if (!i.hasMoreElements() && !firstStep) { + if ((entity instanceof Mech) || (entity instanceof Tank)) { + if ((moveType == EntityMovementType.MOVE_WALK) + || (moveType == EntityMovementType.MOVE_VTOL_WALK) + || (moveType == EntityMovementType.MOVE_RUN) + || (moveType == EntityMovementType.MOVE_VTOL_RUN) + || (moveType == EntityMovementType.MOVE_SPRINT) + || (moveType == EntityMovementType.MOVE_VTOL_SPRINT)) { + int limit = entity.getRunningGravityLimit(); + if (step.isOnlyPavement() && entity.isEligibleForPavementBonus()) { + limit++; + } + if (step.getMpUsed() > limit) { + rollTarget = entity.checkMovedTooFast(step, overallMoveType); + checkNag(rollTarget, nagReport, psrList); + } + } else if (moveType == EntityMovementType.MOVE_JUMP) { + int origWalkMP = entity.getWalkMP(MPCalculationSetting.NO_GRAVITY); + int gravWalkMP = entity.getWalkMP(); + if (step.getMpUsed() > entity.getJumpMP(MPCalculationSetting.NO_GRAVITY)) { + rollTarget = entity.checkMovedTooFast(step, overallMoveType); + checkNag(rollTarget, nagReport, psrList); + } else if ((game.getPlanetaryConditions().getGravity() > 1) + && ((origWalkMP - gravWalkMP) > 0)) { + rollTarget = entity.getBasePilotingRoll(md.getLastStepMovementType()); + entity.addPilotingModifierForTerrain(rollTarget, step); + int gravMod = game.getPlanetaryConditions() + .getGravityPilotPenalty(); + if ((gravMod != 0) && !game.getBoard().inSpace()) { + rollTarget.addModifier(gravMod, game + .getPlanetaryConditions().getGravity() + + "G gravity"); + } + rollTarget.append(new PilotingRollData(entity + .getId(), 0, "jumped in high gravity")); + SharedUtility.checkNag(rollTarget, nagReport, + psrList); + } + if (step.getMpUsed() > entity.getSprintMP(MPCalculationSetting.NO_GRAVITY)) { + rollTarget = entity.checkMovedTooFast(step, overallMoveType); + checkNag(rollTarget, nagReport, psrList); } - rollTarget.append(new PilotingRollData(entity - .getId(), 0, "jumped in high gravity")); - SharedUtility.checkNag(rollTarget, nagReport, - psrList); - } - if (step.getMpUsed() > entity.getSprintMP(MPCalculationSetting.NO_GRAVITY)) { - rollTarget = entity.checkMovedTooFast(step, overallMoveType); - checkNag(rollTarget, nagReport, psrList); } } } From c0de6b78daba5e2a3422e5df9a42144dad97f383 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Tue, 30 Jul 2024 00:30:21 -0700 Subject: [PATCH 30/32] Final tweaks to Magma weighting --- .../client/bot/princess/BasicPathRanker.java | 12 +++++-- .../bot/princess/BasicPathRankerTest.java | 36 ++++++++++--------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java index 8cac7c40293..cb1090e8ea4 100644 --- a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java @@ -779,6 +779,8 @@ int distanceToClosestEdge(Coords position, Game game) { game.getBoard(), logMsg); previousCoords = coords; } + logMsg.append("Compiled Hazard for Path (") + .append(path.toString()).append("): ").append(LOG_DECIMAL.format(totalHazard)); return totalHazard; } finally { @@ -1237,23 +1239,29 @@ private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity moving hazardValue += heat; logMsg.append("\n\t\tHeat gain (").append(LOG_DECIMAL.format(heat)).append(")."); - // Factor in potential damage. + // Factor in potential to suffer fatal damage. + // Dependent on expected average damage / exposed remaining armor * UNIT_DESTRUCTION_FACTOR + int exposedArmor = 0; logMsg.append("\n\t\tDamage to "); if (step.isProne()) { dmg = 7 * movingUnit.locations(); + exposedArmor = movingUnit.getTotalArmor(); logMsg.append("everything [prone] ("); } else if (movingUnit instanceof BipedMech) { dmg = 14; + exposedArmor = List.of(Mech.LOC_LLEG, Mech.LOC_RLEG).stream().mapToInt(a -> movingUnit.getArmor(a)).sum(); logMsg.append("legs ("); } else if (movingUnit instanceof TripodMech) { + exposedArmor = List.of(Mech.LOC_LLEG, Mech.LOC_RLEG, Mech.LOC_CLEG).stream().mapToInt(a -> movingUnit.getArmor(a)).sum(); dmg = 21; logMsg.append("legs ("); } else { + exposedArmor = List.of(Mech.LOC_LLEG, Mech.LOC_RLEG, Mech.LOC_LARM, Mech.LOC_RARM).stream().mapToInt(a -> movingUnit.getArmor(a)).sum(); dmg = 28; logMsg.append("legs ("); } logMsg.append(LOG_DECIMAL.format(dmg)).append(")."); - hazardValue += (unitDamageLevel + 1) * dmg; + hazardValue += (UNIT_DESTRUCTION_FACTOR * (dmg/Math.max(exposedArmor, 1))); // Multiply total hazard value by the chance of getting stuck for 1 or more additional turns logMsg.append("\nFactor applied to hazard value: ").append(LOG_DECIMAL.format(psrFactor)); diff --git a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java index 3ed039f5989..18a74a3111e 100644 --- a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java +++ b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java @@ -1341,7 +1341,7 @@ public void testCheckPathForHazards() { when(mockHexTwo.terrainLevel(Terrains.MAGMA)).thenReturn(1); when(mockHexThree.terrainLevel(Terrains.MAGMA)).thenReturn(1); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); - assertEquals(17.500, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(361.500, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockHexTwo.getTerrainTypes()).thenReturn(new int[0]); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[0]); @@ -1350,11 +1350,12 @@ public void testCheckPathForHazards() { when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(0); // Test the stupidity of going prone in lava. + // Now that hazard is inversely related to remaining armor, this is a _BIG_ number when(mockPath.isJumping()).thenReturn(false); when(mockFinalStep.isProne()).thenReturn(true); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MAGMA}); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); - assertEquals(66.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(56010.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalStep.isProne()).thenReturn(false); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[0]); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(0); @@ -1379,9 +1380,9 @@ public void testCheckPathForHazards() { when(mockFinalHex.depth()).thenReturn(0); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MAGMA}); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); - assertEquals(108.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(3134.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); - assertEquals(212.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(6264.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(0); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.WOODS, Terrains.FIRE}); assertEquals(5.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); @@ -1423,60 +1424,61 @@ public void testMagmaHazard() { // Test jumping onto Magma Crust. when(mockPath.isJumping()).thenReturn(true); - when(mockUnit.getArmor(eq(Mech.LOC_LLEG))).thenReturn(10); + when(mockUnit.getArmor(eq(Mech.LOC_LLEG))).thenReturn(24); + when(mockUnit.getArmor(eq(Mech.LOC_RLEG))).thenReturn(24); when(mockFinalHex.depth()).thenReturn(0); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MAGMA}); // Only 50% chance to break through Crust, but must make PSR to avoid getting bogged down. when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); - assertEquals(108.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(1333.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // 100% chance to take damage when Magma is Liquid (aka Lava) and PSR chance to get stuck. when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); when(mockFinalHex.depth()).thenReturn(1); - assertEquals(212.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(2661.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Test jumping with worse piloting score (hazard should increase quickly) when(mockPath.isJumping()).thenReturn(true); - when(mockUnit.getArmor(eq(Mech.LOC_LLEG))).thenReturn(10); when(mockFinalHex.depth()).thenReturn(0); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[]{Terrains.MAGMA}); // Only 50% chance to break through Crust when(mockCrew.getPiloting()).thenReturn(6); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); - assertEquals(176.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(2192.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // 100% chance to take damage when Magma is Liquid (aka Lava) when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); when(mockFinalHex.depth()).thenReturn(1); - assertEquals(348.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(4380.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Only 50% chance to break through Crust when(mockCrew.getPiloting()).thenReturn(7); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); when(mockFinalHex.depth()).thenReturn(0); - assertEquals(344.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(4300.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // 100% chance to take damage when Magma is Liquid (aka Lava) when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); when(mockFinalHex.depth()).thenReturn(1); - assertEquals(684.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(8595.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Test damaged 'mech walking hazard (should increase hazard as damage level increases) when(mockCrew.getPiloting()).thenReturn(5); when(mockPath.isJumping()).thenReturn(false); - when(mockUnit.getArmor(eq(Mech.LOC_LLEG))).thenReturn(10); + when(mockUnit.getArmor(eq(Mech.LOC_LLEG))).thenReturn(2); + when(mockUnit.getArmor(eq(Mech.LOC_RLEG))).thenReturn(2); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); when(mockFinalHex.depth()).thenReturn(0); // Moderate damage means moderate hazard when(mockUnit.getDamageLevel()).thenReturn(Entity.DMG_MODERATE); - assertEquals(13.1665, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(589.1665, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); when(mockFinalHex.depth()).thenReturn(1); - assertEquals(52, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(3510.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Crippled should be very high hazard when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); when(mockFinalHex.depth()).thenReturn(0); when(mockUnit.getDamageLevel()).thenReturn(Entity.DMG_CRIPPLED); - assertEquals(17.1665, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(589.1665, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); when(mockFinalHex.depth()).thenReturn(1); - assertEquals(80, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(3510.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Check damaged Hover ending on Liquid Magma From 9d4bccf00705aa6711a95df52464d22c2abe9296 Mon Sep 17 00:00:00 2001 From: Sleet01 Date: Tue, 30 Jul 2024 10:45:55 -0700 Subject: [PATCH 31/32] Update history.txt --- megamek/docs/history.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/megamek/docs/history.txt b/megamek/docs/history.txt index 602810603b2..06ccc0a5fa7 100644 --- a/megamek/docs/history.txt +++ b/megamek/docs/history.txt @@ -66,7 +66,7 @@ MEGAMEK VERSION HISTORY: + Fix #5705: Prevent NPE when maneuvering Land-Air Meks as ASFs + Fix #5796: Prevent NPE when units are ineligible for Physical Attack phase due to having loaded onto a transport this round + Fix #5818: correct issue with minimap autodisplay using wrong function - ++ PR #5822: Implement RFE 5361: enhance Princess handling of hazardous terrain 0.49.20 (2024-06-28 2100 UTC) (THIS IS THE LAST VERSION TO SUPPORT JAVA 11) + PR #5281, #5327, #5308, #5336, #5318, #5383, #5369, #5384, #5455, #5505, #5541: Code internals: preparatory work for supporting game types such as SBF, code cleanup for string drawing, superclass change for BoardView, From c573cf6d9305018ad7a32f42b95c7e98539047ea Mon Sep 17 00:00:00 2001 From: NickAragua Date: Tue, 30 Jul 2024 16:06:44 -0400 Subject: [PATCH 32/32] Update history.txt --- megamek/docs/history.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/megamek/docs/history.txt b/megamek/docs/history.txt index 06ccc0a5fa7..46f39886337 100644 --- a/megamek/docs/history.txt +++ b/megamek/docs/history.txt @@ -67,6 +67,7 @@ MEGAMEK VERSION HISTORY: + Fix #5796: Prevent NPE when units are ineligible for Physical Attack phase due to having loaded onto a transport this round + Fix #5818: correct issue with minimap autodisplay using wrong function + PR #5822: Implement RFE 5361: enhance Princess handling of hazardous terrain ++ Issues #2577, #897, #353: Implement placeable/carryable cargo objects (for mechs and protomechs only at the moment) 0.49.20 (2024-06-28 2100 UTC) (THIS IS THE LAST VERSION TO SUPPORT JAVA 11) + PR #5281, #5327, #5308, #5336, #5318, #5383, #5369, #5384, #5455, #5505, #5541: Code internals: preparatory work for supporting game types such as SBF, code cleanup for string drawing, superclass change for BoardView,