diff --git a/.gitignore b/.gitignore index 8b829940bc7..cbd954a6cd0 100644 --- a/.gitignore +++ b/.gitignore @@ -95,7 +95,8 @@ megamek/userdata/** /megamek/apidocs/ #End MegaMek Other Outputs -/megamek/docs/mm-revision.txt /megamek/MegaMek.l4j.ini OfficialUnitList.txt equipment.txt +.github/copilot-instructions.md +*.iml diff --git a/build.gradle b/build.gradle index 755382a668b..1056908c071 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ allprojects { subprojects { group = 'org.megamek' - version = '0.50.03-SNAPSHOT' + version = '0.50.04-SNAPSHOT' } // A properties_local.gradle file can be used to override any of the above options. For instance, diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197d7..a4b76b9530d 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9355b415575..e18bc253b85 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/megamek/build.gradle b/megamek/build.gradle index 6d94571c683..aaff4bfbb0f 100644 --- a/megamek/build.gradle +++ b/megamek/build.gradle @@ -5,7 +5,7 @@ plugins { id 'checkstyle' id 'com.palantir.git-version' version '3.1.0' id 'edu.sc.seis.launch4j' version '3.0.6' - id "io.sentry.jvm.gradle" version '4.11.0' + id "io.sentry.jvm.gradle" version '5.1.0' id 'jacoco' id 'java' id 'org.ec4j.editorconfig' version '0.1.0' @@ -150,8 +150,19 @@ task copyFiles(type: Copy) { include "sentry.properties" 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()) } + + // User Config Files + exclude "${mmconf}/clientsettings.xml" + exclude "${mmconf}/gameoptions.xml" + exclude "${mmconf}/megameklab.properties" + exclude "${mmconf}/megameklab.properties.bak" + exclude "${mmconf}/mhq.preferences" + exclude "${mmconf}/mm.preferences" + exclude "${mmconf}/mml.preferences" + exclude "${rats}/**" include "${userdata}/" diff --git a/megamek/data/images/hexes/minimap/defaultminimap.theme b/megamek/data/images/hexes/minimap/default.theme similarity index 100% rename from megamek/data/images/hexes/minimap/defaultminimap.theme rename to megamek/data/images/hexes/minimap/default.theme diff --git a/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (2801).blk b/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (2801).blk index 62b40a72ffa..85414680de3 100644 --- a/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (2801).blk +++ b/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (2801).blk @@ -123,7 +123,7 @@ Medium Laser -MASH Core Component +MASH Equipment:SIZE:1 diff --git a/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3035)Dove.blk b/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3035)Dove.blk index 968ff9086e1..dacc8a7015b 100644 --- a/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3035)Dove.blk +++ b/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3035)Dove.blk @@ -122,16 +122,7 @@ Medium Laser -MASH Core Component -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater +MASH Equipment:SIZE:10 diff --git a/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3054).blk b/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3054).blk index 30cdc7d8932..2ea1c7781d3 100644 --- a/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3054).blk +++ b/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3054).blk @@ -124,7 +124,7 @@ Medium Laser -MASH Core Component +MASH Equipment:SIZE:1 diff --git a/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3079).blk b/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3079).blk index d454d8bd8f9..646b59d3288 100644 --- a/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3079).blk +++ b/megamek/data/mekfiles/dropships/TRO3057R/IS/Condor (3079).blk @@ -119,7 +119,7 @@ ISERMediumLaser -MASH Core Component +MASH Equipment:SIZE:1 diff --git a/megamek/data/mekfiles/dropships/TRO3057R/IS/Seeker (2815M).blk b/megamek/data/mekfiles/dropships/TRO3057R/IS/Seeker (2815M).blk index d5184d53355..2500e255ffe 100644 --- a/megamek/data/mekfiles/dropships/TRO3057R/IS/Seeker (2815M).blk +++ b/megamek/data/mekfiles/dropships/TRO3057R/IS/Seeker (2815M).blk @@ -115,7 +115,7 @@ Medium Laser -MASH Core Component +MASH Equipment:SIZE:1 diff --git a/megamek/data/mekfiles/dropships/TRO3057R/IS/Seeker (3054).blk b/megamek/data/mekfiles/dropships/TRO3057R/IS/Seeker (3054).blk index 91280b39a16..7864de3d288 100644 --- a/megamek/data/mekfiles/dropships/TRO3057R/IS/Seeker (3054).blk +++ b/megamek/data/mekfiles/dropships/TRO3057R/IS/Seeker (3054).blk @@ -118,7 +118,7 @@ Medium Laser -MASH Core Component +MASH Equipment:SIZE:1 diff --git a/megamek/data/mekfiles/jumpships/3057R/IS/Magellan Jumpship (2960).blk b/megamek/data/mekfiles/jumpships/3057R/IS/Magellan Jumpship (2960).blk index 28712813216..1864233871b 100644 --- a/megamek/data/mekfiles/jumpships/3057R/IS/Magellan Jumpship (2960).blk +++ b/megamek/data/mekfiles/jumpships/3057R/IS/Magellan Jumpship (2960).blk @@ -143,7 +143,7 @@ ISAMS Ammo:24 -MASH Core Component +MASH Equipment:SIZE:1 ISMobileHPG diff --git a/megamek/data/mekfiles/vehicles/3039u/MASH Truck (ICE).blk b/megamek/data/mekfiles/vehicles/3039u/MASH Truck (ICE).blk index 5dffb4fa8be..8a5ce492e31 100644 --- a/megamek/data/mekfiles/vehicles/3039u/MASH Truck (ICE).blk +++ b/megamek/data/mekfiles/vehicles/3039u/MASH Truck (ICE).blk @@ -56,11 +56,7 @@ Wheeled -MASH core component -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater +MASH Equipment:SIZE:5 diff --git a/megamek/data/mekfiles/vehicles/3039u/MASH Truck.blk b/megamek/data/mekfiles/vehicles/3039u/MASH Truck.blk index b2cbd69fa15..77a855a04e6 100644 --- a/megamek/data/mekfiles/vehicles/3039u/MASH Truck.blk +++ b/megamek/data/mekfiles/vehicles/3039u/MASH Truck.blk @@ -57,11 +57,7 @@ Wheeled -MASH core component -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater -MASH Operation Theater +MASH Equipment:SIZE:5 diff --git a/megamek/data/mekfiles/vehicles/3075/Saxon APC (MASH).blk b/megamek/data/mekfiles/vehicles/3075/Saxon APC (MASH).blk index 2eab5a03c0b..89fc29777a4 100644 --- a/megamek/data/mekfiles/vehicles/3075/Saxon APC (MASH).blk +++ b/megamek/data/mekfiles/vehicles/3075/Saxon APC (MASH).blk @@ -61,8 +61,7 @@ Hover -MASH core component -MASH Operation Theater +MASH Equipment:SIZE:2 IS Ammo MG - Full diff --git a/megamek/data/mekfiles/vehicles/XTRs/Primitives III/Carter MERV.blk b/megamek/data/mekfiles/vehicles/XTRs/Primitives III/Carter MERV.blk index 9b4f2be8b25..66d8a7577d1 100644 --- a/megamek/data/mekfiles/vehicles/XTRs/Primitives III/Carter MERV.blk +++ b/megamek/data/mekfiles/vehicles/XTRs/Primitives III/Carter MERV.blk @@ -64,10 +64,9 @@ cargobay:2.0:1:1 -MASH Operation Theater +MASH Equipment:SIZE:2 Paramedic Equipment ISOffRoadChassis -MASH Core Component diff --git a/megamek/data/mekfiles/vehicles/XTRs/RetroTech/White Tip Submarine.blk b/megamek/data/mekfiles/vehicles/XTRs/RetroTech/White Tip Submarine.blk index 0cc9ca674bf..2cf9f16a0e0 100644 --- a/megamek/data/mekfiles/vehicles/XTRs/RetroTech/White Tip Submarine.blk +++ b/megamek/data/mekfiles/vehicles/XTRs/RetroTech/White Tip Submarine.blk @@ -89,7 +89,7 @@ IS Ammo LRTorpedo-10 IS Ammo LRTorpedo-10 Communications Equipment (7 ton) FieldKitchen -MASH Core Component +MASH Equipment:SIZE:1 diff --git a/megamek/docs/history.txt b/megamek/docs/history.txt index 4d938def990..36bf05b8d3e 100644 --- a/megamek/docs/history.txt +++ b/megamek/docs/history.txt @@ -1,6 +1,8 @@ MEGAMEK VERSION HISTORY: ---------------- -0.50.03-SNAPSHOT +0.50.04-SNAPSHOT + +0.50.03 (2025-02-02 2030 UTC) + PR #6335: default the directory filter to Select All in Advanced Board Search + PR #6335: cleanup duplicate boards + PR #6328: Building type enum @@ -24,7 +26,7 @@ MEGAMEK VERSION HISTORY: + PR #6407: Feature: ACAR formations reintroduced + Data #6408: Grabbing the Bull by the horns adding Taurian infantry + Fix #6385: Incorrect TMM bonus for WiGE -+ Fix #6395: ++ Fix #6395: Add knowledge of smoke zero damage to princess + PR #6409: Campaign Options IIC - MegaMek Portion + PR #6415: fixes an error where force consolidation would not set the correct owner of the force + PR #6411: Don't load mml scratch files into unit cache @@ -51,6 +53,30 @@ MEGAMEK VERSION HISTORY: + Fix #6444: fix issue where closing window closes the unit display + PR #6450: Allow the print-unit-list code to run with MegaMekLab.jar + PR #6453: Change keys List from ArrayList to thread-safe Vector ++ Fix #5870: princess engaging targets outside visual range in heavy fog ++ PR #6446: feature: added sensor range on minimap, added facing arrow on minimap ++ Fix #3837: illegal stacking in buildings after skid and failed accidental charge ++ Fix #6421: NullPointerException in LobbyMekCellFormatter.java ++ PR #6446: feature: added sensor range on minimap, added facing arrow on minimap ++ Fix #3837: illegal stacking in buildings after skid and failed accidental charge ++ fix #6421: fix npe issue ++ PR #6459: feature: quick and simple hazard check for hex ++ Fix #6451: Bot command display hotkey text and description missing in key bind menu ++ Fix #6464: defaultminimap renamed to default, fixed error when persisting selected theme ++ PR #6462: Convert from StringBuilder to Logger ++ PR #6468: Fix NPE clicking on board with no unit selected ++ PR #6469: Fix: removes a double call for next-unit ++ PR #6470: Fix: minimap summary npe fix ++ PR #6471: Feat: Mini-Map Improvements (Hex Border options, facing arrows, sensor range) ++ PR #6474: makes Princess move way more than she otherwise would on no-contact ++ PR #6478: fix: a couple of signs in the wrong place on the scoring equation ++ PR #6477: Fix 5590: princess firing on 13s due to intervening trees, etc ++ PR #6479: fix: removes an argument and early exit from hyperaggression ++ PR #6481: Fix 5590 part 2: stop Princess choosing zero damage attacks completely ++ PR #6488: Fix MML #1705 - update drone remote systems unit availability ++ PR #6487: Update MASH equipment to use size values ++ PR #6485: Fix: fixes html tags showing on tooltips ++ PR #6484: Fix: stop dropShadow from being calculated for force display tree 0.50.02 (2024-12-30 2130 UTC) + PR #6183: New GM Commands, princess commands on map menu, graphics for some explosions diff --git a/megamek/i18n/megamek/client/bot/messages.properties b/megamek/i18n/megamek/client/bot/messages.properties index 5b5cccc99bd..91e3b620fe8 100644 --- a/megamek/i18n/megamek/client/bot/messages.properties +++ b/megamek/i18n/megamek/client/bot/messages.properties @@ -1,3 +1,6 @@ BotClient.Hi=Hi, I'm a bot client\! BotClient.HowAbout=How about a nice game of chess? BotClient.Bye=Dave, this conversation can serve no purpose anymore. Goodbye. + +FireControl.LoadAmmo.CauseMiss={} tried to load {} with ammo {} but this would have caused it to miss; skipping. +FireControl.LoadAmmo.FailureToLoad={} tried to load {} with ammo {} but failed somehow. diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 7673a7cda1a..91072d6c4b7 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -1312,6 +1312,9 @@ CommonSettingsDialog.main=Main CommonSettingsDialog.audio=Audio CommonSettingsDialog.miniMap=Mini Map CommonSettingsDialog.mmSymbol=Use StratOps unit symbols in the Minimap +CommonSettingsDialog.drawFacingArrowsOnMiniMap=Draw unit facing arrows on the minimap +CommonSettingsDialog.drawSensorRangeOnMiniMap=Draw unit's active sensor range on the minimap +CommonSettingsDialog.paintBordersOnMiniMap=Draw the hex borders on the minimap CommonSettingsDialog.mouseWheelZoom=Mouse wheel zooms map. CommonSettingsDialog.mouseWheelZoomFlip=Flip mouse wheel zoom direction. CommonSettingsDialog.moveDefaultClimbMode.tooltip=Sets the default climb mode. false = Go Thru, true = Climb Up @@ -2015,6 +2018,8 @@ KeyBinds.cmdNames.pause=Pause the Game KeyBinds.cmdDesc.pause=Pauses the game. Works only when only Princess players with active units remain. KeyBinds.cmdNames.unpause=Unpause the Game KeyBinds.cmdDesc.unpause=Unpauses the game +KeyBinds.cmdNames.toggleBotCommandsDisplay=Show Bot Commands +KeyBinds.cmdDesc.toggleBotCommandsDisplay=Show the Bot Command window to give in-game strategic commands to Princess. #Key Bindings Overlay KeyBindingsDisplay.fixedBinds=Toggle Unit Display and Minimap: Mouse Button 4\nZoom: Mouse Wheel\nTurn / Torso twist: Shift + Left-Click\nLine of Sight tool: Ctrl + Left-Click (two hexes)\nRuler tool: Alt + Left-Click (two hexes) @@ -2446,7 +2451,9 @@ Minimap.menu.ShowHeightTotal=Total T Minimap.menu.ShowSymbols=Show Symbols Minimap.menu.ShowSymbolsNoSymbols=No Symbols N Minimap.menu.ShowSymbolsSymbols=Symbols S - +Minimap.menu.ToggleShowSensorRange=Toggle Sensors +Minimap.menu.ToggleDrawFacingArrows=Toggle Facing Arrows +Minimap.menu.ToggleDrawHexBorder=Toggle Draw Hex Border #Mini round report display MiniReportDisplay.Damage=Damage diff --git a/megamek/mmconf/defaultKeyBinds.xml b/megamek/mmconf/defaultKeyBinds.xml index 7b0ecbd7599..0f501db94f7 100644 --- a/megamek/mmconf/defaultKeyBinds.xml +++ b/megamek/mmconf/defaultKeyBinds.xml @@ -505,4 +505,11 @@ false + + toggleBotCommandsDisplay + 71 + 192 + false + + diff --git a/megamek/src/megamek/SuiteConstants.java b/megamek/src/megamek/SuiteConstants.java index 3553e19ff70..c0f5c0d528c 100644 --- a/megamek/src/megamek/SuiteConstants.java +++ b/megamek/src/megamek/SuiteConstants.java @@ -29,7 +29,7 @@ protected SuiteConstants() { // region General Constants public static final String PROJECT_NAME = "MegaMek Suite"; - public static final Version VERSION = new Version("0.50.03-SNAPSHOT"); + public static final Version VERSION = new Version("0.50.04-SNAPSHOT"); public static final Version LAST_MILESTONE = new Version("0.49.19.1"); public static final int MAXIMUM_D6_VALUE = 6; diff --git a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java index a1d8ef3d94d..fc099df39ff 100644 --- a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java @@ -23,22 +23,25 @@ import megamek.client.bot.princess.BotGeometry.CoordFacingCombo; import megamek.client.bot.princess.BotGeometry.HexLine; import megamek.client.bot.princess.UnitBehavior.BehaviorType; -import megamek.codeUtilities.MathUtility; import megamek.common.*; import megamek.common.options.OptionsConstants; import megamek.common.planetaryconditions.PlanetaryConditions; import megamek.logging.MMLogger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.*; +import java.util.stream.Stream; /** * A very "basic" path ranker */ public class BasicPathRanker extends PathRanker { private final static MMLogger logger = MMLogger.create(BasicPathRanker.class); + private static final Logger log = LogManager.getLogger(BasicPathRanker.class); // this is a value used to indicate how much we value the unit being at its // destination @@ -57,14 +60,14 @@ public class BasicPathRanker extends PathRanker { // the best damage enemies could expect were I not here. Used to determine // whether they will target me. - private Map bestDamageByEnemies; + private final Map bestDamageByEnemies; protected int blackIce = -1; public BasicPathRanker(Princess owningPrincess) { super(owningPrincess); bestDamageByEnemies = new TreeMap<>(); - logger.debug("Using %s behavior.", getOwner().getBehaviorSettings().getDescription()); + logger.debug("Using {} behavior.", getOwner().getBehaviorSettings().getDescription()); } FireControl getFireControl(Entity entity) { @@ -199,24 +202,11 @@ protected List getPSRList(MovePath path) { return super.getPSRList(path); } - @Override - public double getMovePathSuccessProbability(MovePath movePath, - StringBuilder msg) { - return super.getMovePathSuccessProbability(movePath, msg); - } - - private double calculateFallMod(double successProbability, StringBuilder formula) { + private double calculateFallMod(double successProbability) { double pilotingFailure = (1 - successProbability); double fallShame = getOwner().getBehaviorSettings().getFallShameValue(); - double fallMod = pilotingFailure * (pilotingFailure == 1 ? -UNIT_DESTRUCTION_FACTOR : fallShame); - - formula.append("fall mod [") - .append(LOG_DECIMAL.format(fallMod)) - .append(" = ") - .append(LOG_DECIMAL.format(pilotingFailure)) - .append(" * ") - .append(LOG_DECIMAL.format(fallShame)) - .append("]"); + double fallMod = pilotingFailure * (pilotingFailure == 1 ? UNIT_DESTRUCTION_FACTOR : fallShame); + logger.trace("fall mod [{} = {} * {}]", fallMod, pilotingFailure, fallShame); return fallMod; } @@ -365,7 +355,8 @@ EntityEvaluationResponse evaluateMovedEnemy(Entity enemy, MovePath path, Game ga // The further I am from a target, the lower this path ranks (weighted by // Hyper Aggression. - protected double calculateAggressionMod(Entity movingUnit, MovePath path, Game game, StringBuilder formula) { + protected double calculateAggressionMod(Entity movingUnit, MovePath path, Game game) { + double distToEnemy = distanceToClosestEnemy(movingUnit, path.getFinalCoords(), game); if ((distToEnemy == 0) && !(movingUnit instanceof Infantry)) { @@ -374,18 +365,14 @@ protected double calculateAggressionMod(Entity movingUnit, MovePath path, Game g double aggression = getOwner().getBehaviorSettings().getHyperAggressionValue(); double aggressionMod = distToEnemy * aggression; - - formula.append(" - aggressionMod [") - .append(LOG_DECIMAL.format(aggressionMod)).append(" = ") - .append(LOG_DECIMAL.format(distToEnemy)).append(" * ") - .append(LOG_DECIMAL.format(aggression)).append("]"); + logger.trace("aggression mod [ -{} = {} * {}]", aggressionMod, distToEnemy, aggression); return aggressionMod; } // Lower this path ranking if I am moving away from my friends (weighted by Herd Mentality). - protected double calculateHerdingMod(Coords friendsCoords, MovePath path, StringBuilder formula) { + protected double calculateHerdingMod(Coords friendsCoords, MovePath path) { if (friendsCoords == null) { - formula.append(" - herdingMod [0 no friends]"); + logger.trace(" herdingMod [-0 no friends]"); return 0; } @@ -393,18 +380,19 @@ protected double calculateHerdingMod(Coords friendsCoords, MovePath path, String double herding = getOwner().getBehaviorSettings().getHerdMentalityValue(); double herdingMod = finalDistance * herding; - formula.append(" - herdingMod [") - .append(LOG_DECIMAL.format(herdingMod)).append(" = ") - .append(LOG_DECIMAL.format(finalDistance)).append(" * ") - .append(LOG_DECIMAL.format(herding)) - .append("]"); + logger.trace("herding mod [-{} = {} * {}]", herdingMod, finalDistance, herding); return herdingMod; } - // todo account for damaged locations and face those away from enemy. - private double calculateFacingMod(Entity movingUnit, Game game, final MovePath path, - StringBuilder formula) { + private double calculateFacingMod(Entity movingUnit, Game game, final MovePath path) { + int facingDiff = getFacingDiff(movingUnit, game, path); + double facingMod = Math.max(0.0, 50 * (facingDiff - 1)); + logger.trace("facing mod [-{} = max(0, 50 * ({}) - 1)]", facingMod, facingDiff); + return facingMod; + } + // todo account for damaged locations and face those away from enemy. + private int getFacingDiff(Entity movingUnit, Game game, final MovePath path) { Targetable closest = findClosestEnemy(movingUnit, movingUnit.getPosition(), game, false); Coords toFace = closest == null ? game.getBoard().getCenter() : closest.getPosition(); int desiredFacing = (toFace.direction(movingUnit.getPosition()) + 3) % 6; @@ -422,28 +410,20 @@ private double calculateFacingMod(Entity movingUnit, Game game, final MovePath p } else { facingDiff = 3; } - - double facingMod = Math.max(0.0, 50 * (facingDiff - 1)); - formula.append(" - facingMod [").append(LOG_DECIMAL.format(facingMod)) - .append(" = max(") - .append(LOG_INT.format(0)).append(", ") - .append(LOG_INT.format(50)).append(" * {") - .append(LOG_INT.format(facingDiff)).append(" - ") - .append(LOG_INT.format(1)).append("})]"); - return facingMod; + return facingDiff; } /** * If intentionally attempting to reach some board edge, favor paths that take * me closer to it. */ - protected double calculateSelfPreservationMod(Entity movingUnit, MovePath path, Game game, StringBuilder formula) { + protected double calculateSelfPreservationMod(Entity movingUnit, MovePath path, Game game) { BehaviorType behaviorType = getOwner().getUnitBehaviorTracker().getBehaviorType(movingUnit, getOwner()); if (behaviorType == BehaviorType.ForcedWithdrawal || behaviorType == BehaviorType.MoveToDestination) { int newDistanceToHome = distanceToHomeEdge(path.getFinalCoords(), getOwner().getHomeEdge(movingUnit), game); double selfPreservation = getOwner().getBehaviorSettings().getSelfPreservationValue(); - double selfPreservationMod = 0; + double selfPreservationMod; // normally, we favor being closer to the edge we're trying to get to if (newDistanceToHome > 0) { @@ -454,14 +434,10 @@ protected double calculateSelfPreservationMod(Entity movingUnit, MovePath path, selfPreservationMod = -ARRIVED_AT_DESTINATION_FACTOR; } - formula.append(" - selfPreservationMod [") - .append(LOG_DECIMAL.format(selfPreservationMod)) - .append(" = ").append(LOG_DECIMAL.format(newDistanceToHome)) - .append(" * ") - .append(LOG_DECIMAL.format(selfPreservation)).append("]"); + logger.trace("self preservation mod [-{} = {} * {}]", selfPreservationMod, newDistanceToHome, selfPreservation); return selfPreservationMod; } - + logger.trace("self preservation mod [0] - not moving nor forced to withdraw"); return 0.0; } @@ -482,7 +458,6 @@ protected double calculateOffBoardMod(MovePath path) { protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fallTolerance, List enemies, Coords friendsCoords) { Entity movingUnit = path.getEntity(); - StringBuilder formula = new StringBuilder("Calculation: {"); if (blackIce == -1) { blackIce = ((game.getOptions().booleanOption(OptionsConstants.ADVANCED_BLACK_ICE) @@ -494,11 +469,11 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal MovePath pathCopy = path.clone(); // Worry about failed piloting rolls (weighted by Fall Shame). - double successProbability = getMovePathSuccessProbability(pathCopy, formula); - double utility = -calculateFallMod(successProbability, formula); + double successProbability = getMovePathSuccessProbability(pathCopy); + double fallMod = calculateFallMod(successProbability); // Worry about how badly we can damage ourselves on this path! - double expectedDamageTaken = calculateMovePathPSRDamage(movingUnit, pathCopy, formula); + double expectedDamageTaken = calculateMovePathPSRDamage(movingUnit, pathCopy); expectedDamageTaken += checkPathForHazards(pathCopy, movingUnit, game); expectedDamageTaken += MinefieldUtil.checkPathForMinefieldHazards(pathCopy); @@ -566,7 +541,7 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal expectedDamageTaken += friendlyArtilleryDamage; } - calcDamageToStrategicTargets(pathCopy, game, getOwner().getFireControlState(), damageEstimate); + damageEstimate = calcDamageToStrategicTargets(pathCopy, game, getOwner().getFireControlState(), damageEstimate); // If I cannot kick because I am a clan unit and "No physical attacks for the // clans" @@ -579,77 +554,115 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal // I can kick a different target than I shoot, so add physical to // total damage after I've looked at all enemies - utility += getBraveryMod(successProbability, damageEstimate, expectedDamageTaken, formula); + double braveryMod = getBraveryMod(successProbability, damageEstimate, expectedDamageTaken); + var isNotAirborne = !path.getEntity().isAirborneAeroOnGroundMap(); // the only critters not subject to aggression and herding mods are // airborne aeros on ground maps, as they move incredibly fast - if (!path.getEntity().isAirborneAeroOnGroundMap()) { - // The further I am from a target, the lower this path ranks - // (weighted by Aggression slider). - utility -= calculateAggressionMod(movingUnit, pathCopy, game, formula); + // The further I am from a target, the lower this path ranks + // (weighted by Aggression slider). + double aggressionMod = isNotAirborne ? + calculateAggressionMod(movingUnit, pathCopy, game) : 0; + // The further I am from my teammates, the lower this path + // ranks (weighted by Herd Mentality). + double herdingMod = isNotAirborne ? calculateHerdingMod(friendsCoords, pathCopy) : 0; + - // The further I am from my teammates, the lower this path - // ranks (weighted by Herd Mentality). - utility -= calculateHerdingMod(friendsCoords, pathCopy, formula); - } // Movement is good, it gives defense and extends a player power in the game. - utility += calculateMovementMod(pathCopy, game, formula); + double movementMod = calculateMovementMod(pathCopy, game, enemies); // Try to face the enemy. - double facingMod = calculateFacingMod(movingUnit, game, pathCopy, formula); - if (facingMod == -10000) { - return new RankedPath(facingMod, pathCopy, formula.toString()); + double facingMod = calculateFacingMod(movingUnit, game, pathCopy); + var formula = new StringBuilder(256); + if (facingMod <= -10000) { + return new RankedPath(facingMod, pathCopy, "Calculation {facing mod[<= -10000]}"); } - utility -= facingMod; // If I need to flee the board, I want to get closer to my home edge. - utility -= calculateSelfPreservationMod(movingUnit, pathCopy, game, formula); - + double selfPreservationMod= calculateSelfPreservationMod(movingUnit, pathCopy, game); + double offBoardMod = calculateOffBoardMod(pathCopy); // if we're an aircraft, we want to de-value paths that will force us off the // board // on the subsequent turn. - utility -= utility * calculateOffBoardMod(pathCopy); + double utility = -fallMod; + utility += braveryMod; + utility -= aggressionMod; + utility -= herdingMod; + utility += movementMod; + utility -= facingMod; + utility -= selfPreservationMod; + utility -= utility * offBoardMod; + + formula.append("Calculation: {fall mod [").append(LOG_DECIMAL.format(fallMod)).append(" = ") + .append(LOG_DECIMAL.format(1 - successProbability)) + .append(" * ") + .append(LOG_DECIMAL.format(getOwner().getBehaviorSettings().getFallShameValue())) + .append("] + braveryMod [") + .append(LOG_DECIMAL.format(braveryMod)) + .append(" = ") + .append(LOG_PERCENT.format(successProbability)) + .append(" * ((") + .append(LOG_DECIMAL.format(damageEstimate.getMaximumDamageEstimate())) + .append(" * ") + .append(LOG_DECIMAL.format(getOwner().getBehaviorSettings().getBraveryValue())) + .append(") - ") + .append(LOG_DECIMAL.format(expectedDamageTaken)) + .append(")] - aggressionMod [") + .append(LOG_DECIMAL.format(aggressionMod)) + .append(" = ") + .append(LOG_DECIMAL.format(distanceToClosestEnemy(movingUnit, path.getFinalCoords(), game))) + .append(" * ") + .append(LOG_DECIMAL.format(getOwner().getBehaviorSettings().getHyperAggressionValue())) + .append("] - herdingMod ["); + if (friendsCoords != null) { + formula.append(LOG_DECIMAL.format(herdingMod)) + .append(" = ") + .append(LOG_DECIMAL.format(friendsCoords.distance(path.getFinalCoords()))) + .append(" * ") + .append(LOG_DECIMAL.format(getOwner().getBehaviorSettings().getHerdMentalityValue())); + } else { + formula.append("0 no friends"); + } + formula + .append("] + movementMod [") + .append(movementMod) + .append("] - facingMod [") + .append(LOG_DECIMAL.format(facingMod)) + .append(" = max(0, 50 * {") + .append(getFacingDiff(movingUnit, game, pathCopy)) + .append(" - 1})]"); + + logger.trace("utility [{} = - fallMod({}) - offBoard*utility({}) - selfPreservation({}) - facingMod({}) + bravery({}) + movement({}) - aggression({}) - herding({})]", + utility, fallMod, utility * offBoardMod, selfPreservationMod, facingMod, braveryMod, movementMod, aggressionMod, herdingMod); RankedPath rankedPath = new RankedPath(utility, pathCopy, formula.toString()); rankedPath.setExpectedDamage(damageEstimate.getMaximumDamageEstimate()); return rankedPath; } - private double getBraveryMod(double successProbability, FiringPhysicalDamage damageEstimate, double expectedDamageTaken, StringBuilder formula) { + private double getBraveryMod(double successProbability, FiringPhysicalDamage damageEstimate, double expectedDamageTaken) { double maximumDamageDone = damageEstimate.getMaximumDamageEstimate(); // My bravery modifier is based on my chance of getting to the // firing position (successProbability), how much damage I can do // (weighted by bravery), less the damage I might take. double braveryValue = getOwner().getBehaviorSettings().getBraveryValue(); double braveryMod = (successProbability * maximumDamageDone * braveryValue) - expectedDamageTaken; - formula.append(" + braveryMod [") - .append(LOG_DECIMAL.format(braveryMod)).append(" = ") - .append(LOG_PERCENT.format(successProbability)) - .append(" * ((") - .append(LOG_DECIMAL.format(maximumDamageDone)).append(" * ") - .append(LOG_DECIMAL.format(braveryValue)).append(") - ") - .append(LOG_DECIMAL.format(expectedDamageTaken)).append("]"); + logger.trace("bravery mod [{} = {} * (({} * {}) - {})]", braveryMod, successProbability, maximumDamageDone, + braveryValue, expectedDamageTaken); return braveryMod; } - private double calculateMovementMod(MovePath pathCopy, Game game, StringBuilder formula) { - var hexMoved = (double) pathCopy.getHexesMoved(); + // Only forces unit to move if there are no units around + private double calculateMovementMod(MovePath pathCopy, Game game, List enemies) { + if (!enemies.isEmpty() || !getOwner().getEnemyHotSpots().isEmpty()) { + return 0.0; + } var distanceMoved = pathCopy.getDistanceTravelled(); var tmm = Compute.getTargetMovementModifier(distanceMoved, pathCopy.isJumping(), pathCopy.isAirborne(), game); + double selfPreservation = getOwner().getBehaviorSettings().getSelfPreservationValue(); var tmmValue = tmm.getValue(); - if (tmmValue == 0 || ((hexMoved + distanceMoved) == 0)) { - formula.append(" + movementMod [0]"); - return 0; - } - var movementFactor = tmmValue * (hexMoved * distanceMoved) / (hexMoved + distanceMoved); - - formula.append(" + movementMod [") - .append(LOG_DECIMAL.format(movementFactor)) - .append(" = tmm [").append(tmm).append("] * (distance[") - .append(LOG_DECIMAL.format(distanceMoved)).append("] * hexMoved[") - .append(LOG_DECIMAL.format(hexMoved)).append("]) / (distance[") - .append(LOG_DECIMAL.format(distanceMoved)).append("] + hexMoved[") - .append(LOG_DECIMAL.format(hexMoved)).append("])]"); + var movementFactor = tmmValue * selfPreservation; + logger.trace("movement mod [{} = {} * {})]", movementFactor, tmmValue, selfPreservation); return movementFactor; } @@ -695,7 +708,7 @@ public void initUnitTurn(Entity unit, Game game) { } } - protected void calcDamageToStrategicTargets(MovePath path, Game game, + protected FiringPhysicalDamage calcDamageToStrategicTargets(MovePath path, Game game, FireControlState fireControlState, FiringPhysicalDamage damageStructure) { for (int i = 0; i < fireControlState.getAdditionalTargets().size(); i++) { @@ -720,7 +733,7 @@ protected void calcDamageToStrategicTargets(MovePath path, Game game, damageStructure.firingDamage = myDamagePotential; } - if (path.getEntity() instanceof Mek) { + if (path.getEntity().isMek()) { PhysicalInfo myKick = new PhysicalInfo( path.getEntity(), new EntityState(path), target, null, @@ -733,6 +746,7 @@ PhysicalAttackType.RIGHT_KICK, game, getOwner(), } } } + return damageStructure; } /** @@ -772,92 +786,58 @@ int distanceToClosestEdge(Coords position, Game game) { } public double checkPathForHazards(MovePath path, Entity movingUnit, Game game) { - StringBuilder logMsg = new StringBuilder("Checking Path (") - .append(path.toString()).append(") for hazards."); - - try { - // If we're flying or swimming, we don't care about ground hazards. - if (EntityMovementType.MOVE_FLYING.equals(path.getLastStepMovementType()) || - EntityMovementType.MOVE_OVER_THRUST.equals(path.getLastStepMovementType()) || - EntityMovementType.MOVE_SAFE_THRUST.equals(path.getLastStepMovementType()) || - EntityMovementType.MOVE_VTOL_WALK.equals(path.getLastStepMovementType()) || - EntityMovementType.MOVE_VTOL_RUN.equals(path.getLastStepMovementType()) || - EntityMovementType.MOVE_VTOL_SPRINT.equals(path.getLastStepMovementType()) || - EntityMovementType.MOVE_SUBMARINE_WALK.equals(path.getLastStepMovementType()) || - EntityMovementType.MOVE_SUBMARINE_RUN.equals(path.getLastStepMovementType())) { - - logMsg.append("\n\tMove Type (").append(path.getLastStepMovementType().toString()) - .append(") ignores ground hazards."); - return 0; - } - - // If we're jumping, we only care about where we land. - if (path.isJumping()) { - logMsg.append("\n\tJumping"); - Coords endCoords = path.getFinalCoords(); - Hex endHex = game.getBoard().getHex(endCoords); - return checkHexForHazards(endHex, movingUnit, true, - path.getLastStep(), true, - path, game.getBoard(), logMsg); - } - - double totalHazard = 0; - Coords previousCoords = null; - MoveStep lastStep = path.getLastStep(); - for (MoveStep step : path.getStepVector()) { - Coords coords = step.getPosition(); - if ((coords == null) || coords.equals(previousCoords)) { - continue; - } - Hex hex = game.getBoard().getHex(coords); - totalHazard += checkHexForHazards(hex, movingUnit, - lastStep.equals(step), step, - false, path, - game.getBoard(), logMsg); - previousCoords = coords; - } - logMsg.append("\nCompiled Hazard for Path (") - .append(path).append("): ").append(LOG_DECIMAL.format(totalHazard)); - - return totalHazard; - } finally { - logger.debug(logMsg.toString()); + logger.trace("Checking Path ({}) for hazards.", path); + + // If we're flying or swimming, we don't care about ground hazards. + if (EntityMovementType.MOVE_FLYING.equals(path.getLastStepMovementType()) || + EntityMovementType.MOVE_OVER_THRUST.equals(path.getLastStepMovementType()) || + EntityMovementType.MOVE_SAFE_THRUST.equals(path.getLastStepMovementType()) || + EntityMovementType.MOVE_VTOL_WALK.equals(path.getLastStepMovementType()) || + EntityMovementType.MOVE_VTOL_RUN.equals(path.getLastStepMovementType()) || + EntityMovementType.MOVE_VTOL_SPRINT.equals(path.getLastStepMovementType()) || + EntityMovementType.MOVE_SUBMARINE_WALK.equals(path.getLastStepMovementType()) || + EntityMovementType.MOVE_SUBMARINE_RUN.equals(path.getLastStepMovementType())) { + + logger.trace("Move Type ({}) ignores ground hazards.", path.getLastStepMovementType()); + return 0; } - } - - private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, MoveStep step, - boolean jumpLanding, MovePath movePath, Board board, - StringBuilder logMsg) { - logMsg.append("\n\tHex ").append(hex.getCoords().toFriendlyString()); - - final List HAZARDS = new ArrayList<>(Arrays.asList(Terrains.FIRE, - Terrains.MAGMA, - Terrains.ICE, - Terrains.WATER, - Terrains.BUILDING, - Terrains.BRIDGE, - Terrains.BLACK_ICE, - Terrains.SNOW, - Terrains.SWAMP, - Terrains.MUD, - Terrains.TUNDRA)); - // Black Ice can appear if the conditions are favorable - if (blackIce > 0) { - HAZARDS.add(Terrains.PAVEMENT); + // If we're jumping, we only care about where we land. + if (path.isJumping()) { + logger.trace("Jumping, only checking landing hex."); + Coords endCoords = path.getFinalCoords(); + Hex endHex = game.getBoard().getHex(endCoords); + return checkHexForHazards(endHex, movingUnit, true, + path.getLastStep(), true, + path, game.getBoard()); } - int[] terrainTypes = hex.getTerrainTypes(); - Set hazards = new HashSet<>(); - for (int type : terrainTypes) { - if (HAZARDS.contains(type)) { - hazards.add(type); + double totalHazard = 0; + Coords previousCoords = null; + MoveStep lastStep = path.getLastStep(); + for (MoveStep step : path.getStepVector()) { + Coords coords = step.getPosition(); + if ((coords == null) || coords.equals(previousCoords)) { + continue; } - } + Hex hex = game.getBoard().getHex(coords); + totalHazard += checkHexForHazards(hex, movingUnit, + lastStep.equals(step), step, + false, path, + game.getBoard()); + previousCoords = coords; + } + logger.trace("Total Hazard = {}", totalHazard); + return totalHazard; + } + private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, MoveStep step, + boolean jumpLanding, MovePath movePath, Board board) { + logger.trace("Checking Hex ({}) for hazards.", hex.getCoords()); + Set hazards = getHazardTerrainIds(hex); // No hazards were found, so nothing to worry about. if (hazards.isEmpty()) { - logMsg.append(" has no hazards."); + logger.trace("No hazards found."); return 0; } @@ -866,64 +846,88 @@ private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, Mo for (int hazard : hazards) { switch (hazard) { case Terrains.FIRE: - hazardValue += calcFireHazard(movingUnit, endHex, logMsg); + hazardValue += calcFireHazard(movingUnit, endHex); break; case Terrains.MAGMA: - hazardValue += calcMagmaHazard(hex, endHex, movingUnit, jumpLanding, step, logMsg); + hazardValue += calcMagmaHazard(hex, endHex, movingUnit, jumpLanding, step); break; + case Terrains.BLACK_ICE: case Terrains.ICE: - hazardValue += calcIceHazard(movingUnit, hex, step, movePath, jumpLanding, logMsg); + hazardValue += calcIceHazard(movingUnit, hex, step, movePath, jumpLanding); break; case Terrains.WATER: if (!hazards.contains(Terrains.ICE)) { - hazardValue += calcWaterHazard(movingUnit, hex, step, movePath, logMsg); + hazardValue += calcWaterHazard(movingUnit, hex, step, movePath); } break; case Terrains.BUILDING: - hazardValue += calcBuildingHazard(step, movingUnit, jumpLanding, board, logMsg); + hazardValue += calcBuildingHazard(step, movingUnit, jumpLanding, board); break; case Terrains.BRIDGE: - hazardValue += calcBridgeHazard(movingUnit, hex, step, jumpLanding, board, logMsg); - break; - case Terrains.BLACK_ICE: - hazardValue += calcIceHazard(movingUnit, hex, step, movePath, jumpLanding, logMsg); + hazardValue += calcBridgeHazard(movingUnit, hex, step, jumpLanding, board); break; case Terrains.SNOW: - hazardValue += calcSnowHazard(hex, endHex, movingUnit, logMsg); + hazardValue += calcSnowHazard(hex, endHex, movingUnit); break; case Terrains.RUBBLE: - hazardValue += calcRubbleHazard(hex, endHex, movingUnit, jumpLanding, step, logMsg); + hazardValue += calcRubbleHazard(hex, endHex, movingUnit, jumpLanding); break; case Terrains.SWAMP: - hazardValue += calcSwampHazard(hex, endHex, movingUnit, jumpLanding, step, logMsg); + hazardValue += calcSwampHazard(hex, endHex, movingUnit, jumpLanding); break; case Terrains.MUD: - hazardValue += calcMudHazard(endHex, movingUnit, logMsg); + hazardValue += calcMudHazard(endHex, movingUnit); break; case Terrains.TUNDRA: - hazardValue += calcTundraHazard(endHex, jumpLanding, movingUnit, logMsg); + hazardValue += calcTundraHazard(endHex, jumpLanding, movingUnit); 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; + hazardValue += calcIceHazard(movingUnit, hex, step, movePath, jumpLanding) / 3.0; break; } } - logMsg.append("\n\tTotal Hazard = ") - .append(LOG_DECIMAL.format(hazardValue)); + logger.trace("Total Hazard = {}", hazardValue); return hazardValue; } - // Building collapse and basements are handled in PathRanker.validatePaths. - private double calcBuildingHazard(MoveStep step, Entity movingUnit, boolean jumpLanding, - Board board, StringBuilder logMsg) { - logMsg.append("\n\tCalculating building hazard: "); + private static final Set HAZARDS = new HashSet<>(Arrays.asList(Terrains.FIRE, + Terrains.MAGMA, + Terrains.ICE, + Terrains.WATER, + Terrains.BUILDING, + Terrains.BRIDGE, + Terrains.BLACK_ICE, + Terrains.SNOW, + Terrains.SWAMP, + Terrains.MUD, + Terrains.TUNDRA)); + private static final Set HAZARDS_WITH_BLACK_ICE = new HashSet<>(); + static { + HAZARDS_WITH_BLACK_ICE.addAll(HAZARDS); + HAZARDS_WITH_BLACK_ICE.add(Terrains.PAVEMENT); + } + private Set getHazardTerrainIds(Hex hex) { + var hazards = hex.getTerrainTypesSet(); + // Black Ice can appear if the conditions are favorable + if (blackIce > 0) { + hazards.retainAll(HAZARDS_WITH_BLACK_ICE); + } else { + hazards.retainAll(HAZARDS); + } + + return hazards; + } + + // Building collapse and basements are handled in PathRanker.validatePaths. + private double calcBuildingHazard(MoveStep step, Entity movingUnit, boolean jumpLanding, Board board) { + logger.trace("Checking Building ({}) for hazards.", step.getPosition()); // Protos, BA and Infantry move through buildings freely. - if (movingUnit instanceof ProtoMek || movingUnit instanceof Infantry) { - logMsg.append("Safe for infantry and protos."); + if (movingUnit.isProtoMek() || movingUnit.isInfantry() || movingUnit.isConventionalInfantry() || movingUnit.isBattleArmor()) { + logger.trace("Safe for infantry and protos (0)."); return 0; } @@ -935,45 +939,36 @@ private double calcBuildingHazard(MoveStep step, Entity movingUnit, boolean jump // Get the odds of failing the piloting roll while moving through the building. double odds = (1.0 - (Compute.oddsAbove(movingUnit.getCrew() .getPiloting()) / 100)); - logMsg.append("\n\t\tChance to fail piloting roll: ") - .append(LOG_PERCENT.format(odds)); - + logger.trace("Chance to fail piloting roll: {}", odds); // Hazard is based on potential damage taken. double dmg = board.getBuildingAt(step.getPosition()) .getCurrentCF(step.getPosition()) / 10D; - logMsg.append("\n\t\tPotential building damage: ") - .append(LOG_DECIMAL.format(dmg)); - + logger.trace("Potential building damage: {}", dmg); double hazard = dmg * odds; - logMsg.append("\n\t\tHazard value (") - .append(LOG_DECIMAL.format(hazard)).append(")."); + logger.trace("Total Hazard = {}", hazard); return hazard; } - private double calcBridgeHazard(Entity movingUnit, Hex hex, MoveStep step, boolean jumpLanding, - Board board, StringBuilder logMsg) { - logMsg.append("\n\tCalculating bridge hazard: "); - + private double calcBridgeHazard(Entity movingUnit, Hex hex, MoveStep step, boolean jumpLanding, Board board) { + logger.trace("Checking Bridge ({}) for hazards.", hex.getCoords()); // if we are going to BWONGGG into a bridge from below, then it's treated as a // building. // Otherwise, bridge collapse checks have already been handled in validatePaths int bridgeElevation = hex.terrainLevel(Terrains.BRIDGE_ELEV); if ((bridgeElevation > step.getElevation()) && (bridgeElevation <= (step.getElevation() + movingUnit.getHeight()))) { - return calcBuildingHazard(step, movingUnit, jumpLanding, board, logMsg); + return calcBuildingHazard(step, movingUnit, jumpLanding, board); } return 0; } - private double calcIceHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath movePath, boolean jumpLanding, - StringBuilder logMsg) { - logMsg.append("\n\tCalculating ice hazard: "); - + private double calcIceHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath movePath, boolean jumpLanding) { + logger.trace("Checking Ice ({}) for hazards.", hex.getCoords()); // Hover units are above the surface. if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { - logMsg.append("Hovering above ice (0)."); + logger.trace("Hovering above ice (0)."); return 0; } @@ -982,7 +977,7 @@ private double calcIceHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath EntityMovementMode.INF_MOTORIZED == movingUnit.getMovementMode() || EntityMovementMode.INF_JUMP == movingUnit.getMovementMode() || EntityMovementMode.INF_UMU == movingUnit.getMovementMode()) { - logMsg.append("Infantry on Ice (0)."); + logger.trace("Infantry on Ice (0)."); return 0; } @@ -1001,32 +996,27 @@ private double calcIceHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath // 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)."); + if (hex.depth() < 1) {; + logger.trace("No water under ice (0)."); return hazard; } // Hazard is based on chance to break through to the water underneath. double breakthroughMod = jumpLanding ? 0.5 : 0.1667; - logMsg.append("\n\t\tChance to break through ice: ") - .append(LOG_PERCENT.format(breakthroughMod)); - - hazard += calcWaterHazard(movingUnit, hex, step, movePath, logMsg) * + logger.trace("Chance to break through ice: {}", breakthroughMod); + hazard += calcWaterHazard(movingUnit, hex, step, movePath) * breakthroughMod; - logMsg.append("\n\t\tHazard value (") - .append(LOG_DECIMAL.format(hazard)).append(")."); + logger.trace("Total Hazard = {}", hazard); // Changed this to UNIT_DESTRUCTION_FACTOR because she suicided too often. // No reason to be on the ice at all except as an absolute last resort. return UNIT_DESTRUCTION_FACTOR; } - private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath movePath, - StringBuilder logMsg) { - logMsg.append("\n\tCalculating water hazard: "); - + private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath movePath) { + logger.trace("Checking Water ({}) for hazards.", hex.getCoords()); // Puddles don't count. if (hex.depth() == 0) { - logMsg.append("Puddles don't count (0)."); + logger.trace("Puddles don't count (0)."); return 0; } @@ -1034,20 +1024,20 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || EntityMovementMode.WIGE == movingUnit.getMovementMode() || EntityMovementMode.NAVAL == movingUnit.getMovementMode()) { - logMsg.append("Hovering or swimming above water (0)."); + logger.trace("Hovering or swimming above water (0)."); return 0; } // Amphibious units are safe (kind of the point). if (movingUnit.hasWorkingMisc(MiscType.F_FULLY_AMPHIBIOUS) || movingUnit.hasWorkingMisc(MiscType.F_AMPHIBIOUS)) { - logMsg.append("Amphibious unit (0)."); + logger.trace("Amphibious units are safe (0)."); return 0; } // Submarine units should be fine; Orca-riding Infantry goes here. if (EntityMovementMode.SUBMARINE == movingUnit.getMovementMode()) { - logMsg.append("Submarine locomotion unit (0)."); + logger.trace("Submarine units are safe (0)."); return 0; } @@ -1057,7 +1047,7 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa if (hex.containsTerrain(Terrains.BRIDGE_ELEV)) { int bridgeElevation = hex.terrainLevel(Terrains.BRIDGE_ELEV); if (bridgeElevation == step.getElevation()) { - logMsg.append("Unit (0) crossing bridge."); + logger.trace("Bridge elevation matches unit elevation (0)."); return 0; } } @@ -1067,7 +1057,7 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa // but all other hazards (e.g. breaches, crush depth) still apply. if (!(movingUnit instanceof Mek || movingUnit instanceof ProtoMek || movingUnit instanceof BattleArmor || movingUnit.hasUMU())) { - logMsg.append("Ill drown (1000)."); + logger.trace("Drowning (1000)."); return UNIT_DESTRUCTION_FACTOR; } @@ -1080,7 +1070,7 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa && hex.depth() >= 1 && step.equals(lastStep)) { double destructionFactor = hex.depth() >= 2 ? UNIT_DESTRUCTION_FACTOR : UNIT_DESTRUCTION_FACTOR * 0.5d; - logMsg.append(String.format("Industrial Meks drown too (%f).", destructionFactor)); + logger.trace("Industrial Meks drown too ({}).", destructionFactor); return destructionFactor; } @@ -1110,89 +1100,81 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa submergedLocations.add(loc); } } - logMsg.append("\n\t\tSubmerged locations: ") - .append(submergedLocations.size()); + logger.trace("Submerged locations: {}", submergedLocations); int hazardValue = 0; for (int loc : submergedLocations) { - logMsg.append("\n\t\t\tLocation ").append(loc).append(" is "); - // Only locations without armor can breach in movement phase. if (movingUnit.getArmor(loc) > 0) { - logMsg.append(" not breached (0)."); + logger.trace("Location {} is not breached (0).", loc); continue; } // Meks or ProtoMeks having a head or torso breach is deadly. // For other units, any breach is deadly. // noinspection ConstantConditions - if (Mek.LOC_HEAD == loc || - Mek.LOC_CT == loc || - ProtoMek.LOC_HEAD == loc || - ProtoMek.LOC_TORSO == loc || - (!(movingUnit instanceof Mek) && - !(movingUnit instanceof ProtoMek))) { - logMsg.append(" breached and critical (1000)."); + if ((Mek.LOC_HEAD == loc) || + (Mek.LOC_CT == loc) || + (ProtoMek.LOC_HEAD == loc) || + (ProtoMek.LOC_TORSO == loc) || + (!movingUnit.isMek() && !movingUnit.isProtoMek()) + ) { + logger.trace("Location {} breached and critical (1000).", loc); return UNIT_DESTRUCTION_FACTOR; } // Add 50 points per potential breach location. - logMsg.append(" breached (50)."); + logger.trace("Location {} breached (50).", loc); hazardValue += 50; } return hazardValue; } - private double calcFireHazard(Entity movingUnit, boolean endHex, - StringBuilder logMsg) { - logMsg.append("\n\tCalculating fire hazard: "); - + private double calcFireHazard(Entity movingUnit, boolean endHex) { + logger.trace("Calculating fire hazard."); double hazardValue = 0; // Fireproof BA ignores fire. if ((movingUnit instanceof BattleArmor) && ((BattleArmor) movingUnit).isFireResistant()) { - logMsg.append("Ignored by fire resistant armor (0)."); + logger.trace("Fireproof BA ignores fire."); return 0; } // Tanks risk critical hits. if (movingUnit instanceof Tank) { - logMsg.append("Possible crit on tank (25)."); + logger.trace("Tank risks critical hit (25)."); return 25; } // ProtoMeks risk location destruction. if (movingUnit instanceof ProtoMek) { - logMsg.append("Possible location destruction (50)."); + logger.trace("ProtoMek risks location destruction (50)."); return 50; } // Infantry and BA risk total destruction. if (movingUnit instanceof Infantry) { - logMsg.append(("Possible unit destruction (1000).")); + logger.trace("Infantry risks total destruction (1000)."); return UNIT_DESTRUCTION_FACTOR; } // If this unit tracks heat, add the heat gain to the hazard value. if (movingUnit.getHeatCapacity() != Entity.DOES_NOT_TRACK_HEAT) { hazardValue += endHex ? 5 : 2; - logMsg.append("Heat gain (").append(hazardValue).append(")."); + logger.trace("Heat gain ({}).", hazardValue); } return hazardValue; } - private double calcMagmaHazard(Hex hex, boolean endHex, Entity movingUnit, - boolean jumpLanding, MoveStep step, - StringBuilder logMsg) { - logMsg.append("\n\tCalculating magma hazard: "); - + private double calcMagmaHazard(Hex hex, boolean endHex, Entity movingUnit, boolean jumpLanding, MoveStep step) { + logger.trace("Calculating magma hazard."); // Hovers / WiGE are normally unaffected. if ((EntityMovementMode.HOVER == movingUnit.getMovementMode() || EntityMovementMode.WIGE == movingUnit.getMovementMode()) && !endHex) { - logMsg.append("Hovering above magma (0)."); + logger.trace("Hovering above magma (0)."); return 0; } @@ -1201,35 +1183,28 @@ private double calcMagmaHazard(Hex hex, boolean endHex, Entity movingUnit, // Liquid magma. if (magmaLevel == 2) { - return calcLavaHazard(endHex, jumpLanding, movingUnit, step, logMsg); + return calcLavaHazard(endHex, jumpLanding, movingUnit, step); } else { double breakThroughMod = jumpLanding ? 0.5 : 0.1667; - logMsg.append("\n\t\tChance to break through crust = ") - .append(LOG_PERCENT.format(breakThroughMod)); - + logger.trace("Chance to break through crust = {}", breakThroughMod); // Factor in the chance to break through. - double lavalHazard = Math.round(calcLavaHazard(endHex, jumpLanding, movingUnit, step, - logMsg) * breakThroughMod); - logMsg.append("\n\t\t\tLava hazard (") - .append(LOG_DECIMAL.format(lavalHazard)).append(")."); + double lavalHazard = Math.round(calcLavaHazard(endHex, jumpLanding, movingUnit, step) * breakThroughMod); + logger.trace("Lava hazard = {}", lavalHazard); hazardValue += lavalHazard; // Factor in heat. if (movingUnit.getHeatCapacity() != Entity.DOES_NOT_TRACK_HEAT) { double heatMod = (endHex ? 5 : 2) * (1 - breakThroughMod); hazardValue += heatMod; - logMsg.append("\n\t\tHeat gain (") - .append(LOG_DECIMAL.format(heatMod)).append(")."); + logger.trace("Heat gain = {}", heatMod); } } return hazardValue; } - private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity movingUnit, - MoveStep step, StringBuilder logMsg) { - logMsg.append("\n\tCalculating laval hazard: "); - + private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity movingUnit, MoveStep step) { + logger.trace("Calculating lava hazard."); int unitDamageLevel = movingUnit.getDamageLevel(); double dmg; @@ -1238,7 +1213,7 @@ private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity moving if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { if (!endHex) { - logMsg.append("Hovering/VTOL while traversing lava (0)."); + logger.trace("Hovering/VTOL while traversing lava (0)."); return 0; } else { // Estimate chance of being disabled or immobilized over open lava; this is @@ -1246,15 +1221,14 @@ private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity moving // Calc expected damage as ((current damage level [0 ~ 4]) / 4) * // 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(")."); + logger.trace("Ending hover/VTOL movement over lava ({}).", dmg); return dmg; } } // Non-Mek units auto-destroyed. if (!(movingUnit instanceof Mek)) { - logMsg.append("Non-Mek instant destruction (1000)."); + logger.trace("Non-Mek instant destruction (1000)."); return UNIT_DESTRUCTION_FACTOR; } @@ -1269,59 +1243,53 @@ private double calcLavaHazard(boolean endHex, boolean jumpLanding, Entity moving // Former is: %chance _not_ passing 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."); + logger.trace("Jumping onto lava hex, may get bogged down."); int pilotSkill = movingUnit.getCrew().getPiloting(); 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)); - 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)); + logger.trace("Chance to bog down = {}, expected turns = {}", oddsBogged, expectedTurns); psrFactor = 1.0 + oddsBogged + (expectedTurns); } // Factor in heat. double heat = endHex ? 10.0 : 5.0; hazardValue += heat; - logMsg.append("\n\t\tHeat gain (").append(LOG_DECIMAL.format(heat)).append(")."); - + logger.trace("Heat gain = {}", heat); // 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 "); + int exposedArmor; if (step.isProne()) { dmg = 7 * movingUnit.locations(); exposedArmor = movingUnit.getTotalArmor(); - logMsg.append("everything [prone] ("); + logger.trace("Prone Mek damage = {}, exposed armor = {}", dmg, exposedArmor); } else if (movingUnit instanceof BipedMek) { dmg = 14; - exposedArmor = List.of(Mek.LOC_LLEG, Mek.LOC_RLEG).stream().mapToInt(a -> movingUnit.getArmor(a)).sum(); - logMsg.append("legs ("); + exposedArmor = Stream.of(Mek.LOC_LLEG, Mek.LOC_RLEG).mapToInt(movingUnit::getArmor).sum(); + logger.trace("Biped Mek damage = {}, exposed armor = {}", dmg, exposedArmor); } else if (movingUnit instanceof TripodMek) { - exposedArmor = List.of(Mek.LOC_LLEG, Mek.LOC_RLEG, Mek.LOC_CLEG).stream() - .mapToInt(a -> movingUnit.getArmor(a)).sum(); + exposedArmor = Stream.of(Mek.LOC_LLEG, Mek.LOC_RLEG, Mek.LOC_CLEG) + .mapToInt(movingUnit::getArmor).sum(); dmg = 21; - logMsg.append("legs ("); + logger.trace("Tripod Mek damage = {}, exposed armor = {}", dmg, exposedArmor); } else { - exposedArmor = List.of(Mek.LOC_LLEG, Mek.LOC_RLEG, Mek.LOC_LARM, Mek.LOC_RARM).stream() - .mapToInt(a -> movingUnit.getArmor(a)).sum(); + exposedArmor = Stream.of(Mek.LOC_LLEG, Mek.LOC_RLEG, Mek.LOC_LARM, Mek.LOC_RARM) + .mapToInt(movingUnit::getArmor).sum(); dmg = 28; - logMsg.append("legs ("); + logger.trace("Quad Mek damage = {}, exposed armor = {}", dmg, exposedArmor); } - logMsg.append(LOG_DECIMAL.format(dmg)).append(")."); 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)); + logger.trace("Total hazard = {}", hazardValue * 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); + private double calcBogDownFactor(String name, boolean endHex, boolean jumpLanding, int pilotSkill, int modifier) { + return calcBogDownFactor(name, endHex, jumpLanding, pilotSkill, modifier, true); } /** @@ -1338,11 +1306,10 @@ private double calcBogDownFactor(String name, boolean endHex, boolean jumpLandin * @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) { + int modifier, boolean bogPossible) { double factor; int effectiveSkill = pilotSkill + modifier; double oddsPSR = Math.max((Compute.oddsAbove(effectiveSkill) / 100.0), 0.0); @@ -1352,38 +1319,31 @@ private double calcBogDownFactor(String name, boolean endHex, boolean jumpLandin 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; + logger.trace("Jumping onto {} hex, would get bogged down.", name); } 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; + logger.trace("Entering onto {} hex, chance to bog down = {}", name, oddsBogged); } // (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)); + logger.trace("Chance to bog down = {}, expected turns = {}", oddsBogged, expectedTurns); } factor = 1.0 + oddsBogged + (expectedTurns); return factor; } - private double calcSnowHazard(Hex hex, boolean endHex, Entity movingUnit, StringBuilder logMsg) { - logMsg.append("\n\tCalculating Deep Snow hazard: "); - + private double calcSnowHazard(Hex hex, boolean endHex, Entity movingUnit) { + logger.trace("Checking Snow ({}) for hazards.", hex.getCoords()); // Hover units are above the surface. if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { - logMsg.append("Hovering above Snow (0)."); + logger.trace("Hovering above snow (0)."); return 0; } @@ -1397,9 +1357,9 @@ private double calcSnowHazard(Hex hex, boolean endHex, Entity movingUnit, String // Base hazard is arbitrarily set to 10 hazard = 10 * calcBogDownFactor( - "Deep Snow", endHex, false, pilotSkill, psrMod, logMsg); + "Deep Snow", endHex, false, pilotSkill, psrMod); - logMsg.append("\nBase hazard value: ").append(LOG_DECIMAL.format(hazard)); + logger.trace("Deep snow hazard = {}", hazard); return Math.round(hazard); } @@ -1407,15 +1367,12 @@ private double calcSnowHazard(Hex hex, boolean endHex, Entity movingUnit, String return 0; } - private double calcSwampHazard(Hex hex, boolean endHex, Entity movingUnit, - boolean jumpLanding, MoveStep step, - StringBuilder logMsg) { - logMsg.append("\n\tCalculating Swamp hazard: "); - + private double calcSwampHazard(Hex hex, boolean endHex, Entity movingUnit, boolean jumpLanding) { + logger.trace("Checking Swamp ({}) for hazards.", hex.getCoords()); // Hover units are above the surface. if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { - logMsg.append("Hovering above swamp (0)."); + logger.trace("Hovering above swamp (0)."); return 0; } @@ -1427,8 +1384,7 @@ private double calcSwampHazard(Hex hex, boolean endHex, Entity movingUnit, 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)); - + logger.trace("Base hazard value: {}", hazard); // Mod is to difficulty, not to PSR roll results // Quicksand makes PSRs an additional +3! Otherwise +1 for Meks, +2 for all // other types @@ -1437,9 +1393,8 @@ private double calcSwampHazard(Hex hex, boolean endHex, Entity movingUnit, // 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)); - + double factor = calcBogDownFactor(type, endHex, jumpLanding, pilotSkill, psrMod); + logger.trace("Factor applied to hazard value: {}", factor); // The danger is increased if pilot skill is low, as the chance of succumbing or // getting // permanently stuck increases! @@ -1448,13 +1403,12 @@ private double calcSwampHazard(Hex hex, boolean endHex, Entity movingUnit, return Math.round(hazard); } - private double calcMudHazard(boolean endHex, Entity movingUnit, StringBuilder logMsg) { - logMsg.append("\n\tCalculating Mud hazard: "); - + private double calcMudHazard(boolean endHex, Entity movingUnit) { + logger.trace("Checking Mud for hazards."); // Hover units are above the surface. if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { - logMsg.append("Hovering above Mud (0)."); + logger.trace("Hovering above Mud (0)."); return 0; } @@ -1465,28 +1419,25 @@ private double calcMudHazard(boolean endHex, Entity movingUnit, StringBuilder lo int pilotSkill = (movingUnit.isInfantry()) ? 4 : movingUnit.getCrew().getPiloting(); double hazard; - if (movingUnit instanceof Mek) { + if (movingUnit.isMek()) { // The only hazard is the +1 to PSRs, which are difficult to quantify // Even jumping Meks cannot bog down in mud. - hazard = calcBogDownFactor( - "Mud", endHex, false, pilotSkill, psrMod, false, logMsg); + hazard = calcBogDownFactor("Mud", endHex, false, pilotSkill, psrMod, false); } 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); + hazard = 10 * calcBogDownFactor("Mud", endHex, false, pilotSkill, psrMod); } - logMsg.append("\nBase hazard value: ").append(LOG_DECIMAL.format(hazard)); + logger.trace("Mud hazard = {}", hazard); return Math.round(hazard); } - private double calcTundraHazard(boolean endHex, boolean jumpLanding, Entity movingUnit, StringBuilder logMsg) { - logMsg.append("\n\tCalculating Tundra hazard: "); - + private double calcTundraHazard(boolean endHex, boolean jumpLanding, Entity movingUnit) { + logger.trace("Checking Tundra for hazards."); // Hover units are above the surface. if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { - logMsg.append("Hovering above Tundra (0)."); + logger.trace("Hovering above Tundra (0)."); return 0; } @@ -1498,22 +1449,17 @@ private double calcTundraHazard(boolean endHex, boolean jumpLanding, Entity movi 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)); + hazard = 10 * calcBogDownFactor("Tundra", endHex, jumpLanding, pilotSkill, psrMod); + logger.trace("Tundra hazard = {}", hazard); return Math.round(hazard); } - private double calcRubbleHazard(Hex hex, boolean endHex, Entity movingUnit, - boolean jumpLanding, MoveStep step, - StringBuilder logMsg) { - - logMsg.append("\n\tCalculating Rubble hazard: "); - + private double calcRubbleHazard(Hex hex, boolean endHex, Entity movingUnit, boolean jumpLanding) { + logger.trace("Checking Rubble ({}) for hazards.", hex.getCoords()); // Hover units are above the surface. if (EntityMovementMode.HOVER == movingUnit.getMovementMode() || EntityMovementMode.WIGE == movingUnit.getMovementMode()) { - logMsg.append("Hovering above Rubble (0)."); + logger.trace("Hovering above Rubble (0)."); return 0; } @@ -1533,10 +1479,9 @@ private double calcRubbleHazard(Hex hex, boolean endHex, Entity movingUnit, int pilotSkill = movingUnit.getCrew().getPiloting(); // The only hazard is the +1 to PSRs, which are difficult to quantify - hazard = calcBogDownFactor( - "Rubble", endHex, jumpLanding, pilotSkill, psrMod, false, logMsg); + hazard = calcBogDownFactor("Rubble", endHex, jumpLanding, pilotSkill, psrMod, false); } - logMsg.append("\nBase hazard value: ").append(LOG_DECIMAL.format(hazard)); + logger.trace("Total Hazard = {}", hazard); return Math.round(hazard); } diff --git a/megamek/src/megamek/client/bot/princess/FireControl.java b/megamek/src/megamek/client/bot/princess/FireControl.java index 4f27a35e0ac..4ae8c17f3c9 100644 --- a/megamek/src/megamek/client/bot/princess/FireControl.java +++ b/megamek/src/megamek/client/bot/princess/FireControl.java @@ -163,6 +163,7 @@ public class FireControl { static final TargetRollModifier TH_SWARM_STOPPED = new TargetRollModifier(TargetRoll.AUTOMATIC_SUCCESS, "stops swarming"); static final TargetRollModifier TH_OUT_OF_RANGE = new TargetRollModifier(TargetRoll.IMPOSSIBLE, "out of range"); + static final TargetRollModifier TH_OUT_OF_VISUAL = new TargetRollModifier(TargetRoll.IMPOSSIBLE, "out of visual targeting range"); static final TargetRollModifier TH_SHORT_RANGE = new TargetRollModifier(0, "Short Range"); static final TargetRollModifier TH_MEDIUM_RANGE = new TargetRollModifier(2, "Medium Range"); static final TargetRollModifier TH_LONG_RANGE = new TargetRollModifier(4, "Long Range"); @@ -305,6 +306,11 @@ ToHitData guessToHitModifierHelperForAnyAttack(final Entity shooter, return new ToHitData(TH_RNG_TOO_FAR); } + final int maxVisRange = game.getPlanetaryConditions().getVisualRange(shooter, shooter.isUsingSearchlight()); + if (distance > maxVisRange) { + return new ToHitData(TH_OUT_OF_VISUAL); + } + final ToHitData toHitData = new ToHitData(); // If people are moving or lying down, there are consequences @@ -348,8 +354,12 @@ ToHitData guessToHitModifierHelperForAnyAttack(final Entity shooter, } // terrain modifiers, since "compute" won't let me do these remotely + LosEffects los = LosEffects.calculateLOS(game, shooter, target); + + // We want to check the target hex _and_ the intervening hexes for woods, smoke, etc. final Hex targetHex = game.getBoard().getHex(targetState.getPosition()); - int woodsLevel = targetHex.terrainLevel(Terrains.WOODS); + int woodsLevel = targetHex.terrainLevel(Terrains.WOODS) + + ((los.thruWoods()) ? los.getLightWoods() + los.getHeavyWoods() + los.getUltraWoods() : 0); if (targetHex.terrainLevel(Terrains.JUNGLE) > woodsLevel) { woodsLevel = targetHex.terrainLevel(Terrains.JUNGLE); } @@ -357,7 +367,9 @@ ToHitData guessToHitModifierHelperForAnyAttack(final Entity shooter, toHitData.addModifier(woodsLevel, TH_WOODS); } - final int smokeLevel = targetHex.terrainLevel(Terrains.SMOKE); + // final int smokeLevel = targetHex.terrainLevel(Terrains.SMOKE); + final int smokeLevel = targetHex.terrainLevel(Terrains.SMOKE) + + los.getLightSmoke() + los.getHeavySmoke(); if (1 <= smokeLevel) { // Smoke level doesn't necessarily correspond to the to-hit modifier // even levels are light smoke, odd are heavy smoke @@ -2105,7 +2117,8 @@ FiringPlan getFullFiringPlan(final Entity shooter, continue; } } - if (bestShoot.getProbabilityToHit() > toHitThreshold) { + // Attack should have a chance to hit, and expect to do non-zero damage; otherwise skip it + if (bestShoot.getProbabilityToHit() > toHitThreshold && bestShoot.getExpectedDamage() > 0.0) { myPlan.add(bestShoot); continue; } @@ -2831,22 +2844,45 @@ void loadAmmo(final Entity shooter, final AmmoMounted suggestedAmmo = info.getAmmo(); final AmmoMounted mountedAmmo = getPreferredAmmo(shooter, info.getTarget(), currentWeapon, suggestedAmmo); + + // if we didn't find preferred ammo after all, continue + if (mountedAmmo == null) { + continue; + } + + // If the selected ammo would cause the shot to miss, skip loading it. + final WeaponAttackAction cloneWAA = new WeaponAttackAction(info.getAction()); + cloneWAA.setAmmoId(shooter.getEquipmentNum(mountedAmmo)); + cloneWAA.setAmmoMunitionType(((AmmoType) mountedAmmo.getType()).getMunitionType()); + cloneWAA.setAmmoCarrier(mountedAmmo.getEntity().getId()); + if (cloneWAA.toHit(owner.getGame(), owner.getPrecognition().getECMInfo()).getValue() > 12) { + logger.warn( + Messages.getString( + "FireControl.LoadAmmo.CauseMiss", + shooter.getDisplayName(), + currentWeapon.getName(), + mountedAmmo.getDesc() + ) + ); + continue; + } + // if we found preferred ammo but can't apply it to the weapon, log it and // continue. - if ((null != mountedAmmo) && !shooter.loadWeapon(currentWeapon, mountedAmmo)) { - logger.warn(shooter.getDisplayName() + " tried to load " - + currentWeapon.getName() + " with ammo " + - mountedAmmo.getDesc() + " but failed somehow."); - continue; - // if we didn't find preferred ammo after all, continue - } else if (mountedAmmo == null) { + if (!shooter.loadWeapon(currentWeapon, mountedAmmo)) { + logger.warn( + Messages.getString( + "FireControl.LoadAmmo.FailureToLoad", + shooter.getDisplayName(), + currentWeapon.getName(), + mountedAmmo.getDesc() + ) + ); continue; } - final WeaponAttackAction action = info.getAction(); - action.setAmmoId(shooter.getEquipmentNum(mountedAmmo)); - action.setAmmoMunitionType(((AmmoType) mountedAmmo.getType()).getMunitionType()); - action.setAmmoCarrier(mountedAmmo.getEntity().getId()); - info.setAction(action); + + // If everything looks okay, replace the old WAA with the updated copy + info.setAction(cloneWAA); owner.sendAmmoChange(info.getShooter().getId(), shooter.getEquipmentNum(currentWeapon), shooter.getEquipmentNum(mountedAmmo), mountedAmmo.getSwitchedReason()); } diff --git a/megamek/src/megamek/client/bot/princess/InfantryPathRanker.java b/megamek/src/megamek/client/bot/princess/InfantryPathRanker.java index 7f066bc6466..767533aeb8c 100644 --- a/megamek/src/megamek/client/bot/princess/InfantryPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/InfantryPathRanker.java @@ -27,8 +27,10 @@ import megamek.common.MekWarrior; import megamek.common.MovePath; import megamek.common.options.OptionsConstants; +import megamek.logging.MMLogger; public class InfantryPathRanker extends BasicPathRanker { + private final static MMLogger logger = MMLogger.create(InfantryPathRanker.class); public InfantryPathRanker(Princess princess) { super(princess); @@ -40,7 +42,7 @@ public InfantryPathRanker(Princess princess) { protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fallTolerance, List enemies, Coords friendsCoords) { Entity movingUnit = path.getEntity(); - StringBuilder formula = new StringBuilder("Calculation: {"); + StringBuilder formula = new StringBuilder(); // Copy the path to avoid inadvertent changes. MovePath pathCopy = path.clone(); @@ -48,13 +50,10 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal // look at all of my enemies FiringPhysicalDamage damageEstimate = new FiringPhysicalDamage(); - double expectedDamageTaken = checkPathForHazards(pathCopy, - movingUnit, - game); + double expectedDamageTaken = checkPathForHazards(pathCopy, movingUnit, game); boolean extremeRange = game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_RANGE); boolean losRange = game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_LOS_RANGE); for (Entity enemy : enemies) { - // Skip ejected pilots. if (enemy instanceof MekWarrior) { continue; @@ -95,31 +94,49 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal // My bravery modifier is based on my chance of getting to the // firing position (successProbability), how much damage I can do // (weighted by bravery), less the damage I might take. - double braveryValue = - getOwner().getBehaviorSettings().getBraveryValue(); + double braveryValue = getOwner().getBehaviorSettings().getBraveryValue(); double braveryMod = (maximumDamageDone * braveryValue) - expectedDamageTaken; - formula.append(" + braveryMod [") - .append(LOG_DECIMAL.format(braveryMod)).append(" = ") - .append("((") - .append(LOG_DECIMAL.format(maximumDamageDone)).append(" * ") - .append(LOG_DECIMAL.format(braveryValue)).append(") - ") - .append(LOG_DECIMAL.format(expectedDamageTaken)).append("]"); - double utility = braveryMod; // If an infantry unit is not in range to do damage, // then we want it to move closer. Otherwise, let's avoid charging up to unmoved units, // that's not going to end well. - if (maximumDamageDone <= 0) { - utility -= calculateAggressionMod(movingUnit, pathCopy, game, formula); - } + var aggressionMod = calculateAggressionMod(movingUnit, pathCopy, game); // The further I am from my teammates, the lower this path // ranks (weighted by Herd Mentality). - utility -= calculateHerdingMod(friendsCoords, pathCopy, formula); + var herdingMod = calculateHerdingMod(friendsCoords, pathCopy); // If I need to flee the board, I want to get closer to my home edge. - utility -= calculateSelfPreservationMod(movingUnit, pathCopy, game, - formula); + var selfPreservationMod = calculateSelfPreservationMod(movingUnit, pathCopy, game); + + double utility = braveryMod; + utility -= aggressionMod; + utility -= herdingMod; + utility -= selfPreservationMod; + + formula.append("Calculation: {braveryMod [") + .append(LOG_DECIMAL.format(braveryMod)).append(" = ") + .append("((") + .append(LOG_DECIMAL.format(maximumDamageDone)).append(" * ") + .append(LOG_DECIMAL.format(braveryValue)).append(") - ") + .append(LOG_DECIMAL.format(expectedDamageTaken)).append("]") + .append(")] - aggressionMod [").append(aggressionMod).append(" = ") + .append(distanceToClosestEnemy(movingUnit, path.getFinalCoords(), game)).append(" * ") + .append(getOwner().getBehaviorSettings().getHyperAggressionValue()).append("] - herdingMod [") + .append(herdingMod).append(" = ").append(distanceToClosestEnemy(movingUnit, path.getFinalCoords(), game)) + .append(" * ").append(getOwner().getBehaviorSettings().getHerdMentalityValue()).append("] + selfPreservationMod [") + .append(selfPreservationMod).append("]}"); + + logger.trace("Calculation: {braveryMod [{}] = (({} * {}) - {})] - aggressionMod [{}] = {} * {}] - herdingMod [{}] = {} * {}] + selfPreservationMod [{}]}", + LOG_DECIMAL.format(braveryMod), + LOG_DECIMAL.format(maximumDamageDone), + LOG_DECIMAL.format(braveryValue), + LOG_DECIMAL.format(expectedDamageTaken), + aggressionMod, + distanceToClosestEnemy(movingUnit, path.getFinalCoords(), game), getOwner().getBehaviorSettings().getHyperAggressionValue(), + herdingMod, + distanceToClosestEnemy(movingUnit, path.getFinalCoords(), game), getOwner().getBehaviorSettings().getHerdMentalityValue(), + selfPreservationMod); RankedPath rankedPath = new RankedPath(utility, pathCopy, formula.toString()); rankedPath.setExpectedDamage(maximumDamageDone); @@ -127,9 +144,7 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal } @Override - EntityEvaluationResponse evaluateUnmovedEnemy(Entity enemy, MovePath path, - boolean useExtremeRange, boolean useLOSRange) { - + EntityEvaluationResponse evaluateUnmovedEnemy(Entity enemy, MovePath path, boolean useExtremeRange, boolean useLOSRange) { //some preliminary calculations final double damageDiscount = 0.25; EntityEvaluationResponse returnResponse = diff --git a/megamek/src/megamek/client/bot/princess/PathRanker.java b/megamek/src/megamek/client/bot/princess/PathRanker.java index 47681d40401..ffb54407c71 100644 --- a/megamek/src/megamek/client/bot/princess/PathRanker.java +++ b/megamek/src/megamek/client/bot/princess/PathRanker.java @@ -140,8 +140,8 @@ public TreeSet rankPaths(List movePaths, Game game, int ma return rankPaths(getOwner().getMovePathsAndSetNecessaryTargets(mover, true), game, maxRange, fallTolerance, enemies, friends); } - } catch (Exception ignored) { - logger.error(ignored, ignored.getMessage()); + } catch (Exception exception) { + logger.error(exception, exception.getMessage()); return returnPaths; } @@ -175,62 +175,58 @@ private List validatePaths(List startingPathList, Game game, continue; } - StringBuilder msg = new StringBuilder("Validating Path: ").append(path); - - try { - // if we are an aero unit on the ground map, we want to discard paths that keep - // us at altitude 1 with no bombs - if (isAirborneAeroOnGroundMap) { - // if we have no bombs, we want to make sure our altitude is above 1 - // if we do have bombs, we may consider altitude bombing (in the future) - if (path.getEntity().getBombs(BombType.F_GROUND_BOMB).isEmpty() - && (path.getFinalAltitude() < 2)) { - msg.append("\n\tNo bombs but at altitude 1. No way."); - continue; - } - } - - Coords finalCoords = path.getFinalCoords(); - - // Make sure I'm trying to get/stay in range of a target. - // Skip this part if I'm an aero on the ground map, as it's kind of irrelevant - // also skip this part if I'm attempting to retreat, as engagement is not the - // point here - if (!isAirborneAeroOnGroundMap && !getOwner().wantsToFallBack(mover) && !hasNoEnemyAvailable) { - Targetable closestToEnd = findClosestEnemy(mover, finalCoords, game); - String validation = validRange(finalCoords, closestToEnd, startingTargetDistance, maxRange, - inRange); - if (!StringUtility.isNullOrBlank(validation)) { - msg.append("\n\t").append(validation); - continue; - } - } - - // Don't move on/through buildings that will not support our weight. - if (willBuildingCollapse(path, game)) { - msg.append("\n\tINVALID: Building in path will collapse."); + logger.trace("Validating path {}", path); + // if we are an aero unit on the ground map, we want to discard paths that keep + // us at altitude 1 with no bombs + if (isAirborneAeroOnGroundMap) { + // if we have no bombs, we want to make sure our altitude is above 1 + // if we do have bombs, we may consider altitude bombing (in the future) + if (path.getEntity().getBombs(BombType.F_GROUND_BOMB).isEmpty() + && (path.getFinalAltitude() < 2)) { + logger.trace("INVALID: No bombs but at altitude 1. No way."); continue; } + } - // Skip any path where I am too likely to fail my piloting roll. - double chance = getMovePathSuccessProbability(path, msg); - if (chance < fallTolerance) { - msg.append("\n\tINVALID: Too likely to fall on my face."); + Coords finalCoords = path.getFinalCoords(); + + // Make sure I'm trying to get/stay in range of a target. + // Skip this part if I'm an aero on the ground map, as it's kind of irrelevant + // also skip this part if I'm attempting to retreat, as engagement is not the + // point here + if (!isAirborneAeroOnGroundMap && !getOwner().wantsToFallBack(mover) && !hasNoEnemyAvailable) { + Targetable closestToEnd = findClosestEnemy(mover, finalCoords, game); + var validation = validRange(finalCoords, closestToEnd, startingTargetDistance, maxRange, + inRange); + if (!validation) { + logger.trace("Invalid range to target."); continue; } + } - // first crack at logic involving unjamming RACs: just do it - if (needToUnjamRAC && ((path.getMpUsed() > walkMP) || path.isJumping())) { - msg.append("\n\tINADVISABLE: Want to unjam autocannon but path involves running or jumping"); - continue; - } + // Don't move on/through buildings that will not support our weight. + if (willBuildingCollapse(path, game)) { + logger.trace("INVALID: Building in path will collapse."); + continue; + } - // If all the above checks have passed, this is a valid path. - msg.append("\n\tVALID."); - returnPaths.add(path); - } finally { - logger.debug(msg.toString()); + // Skip any path where I am too likely to fail my piloting roll. + double chance = getMovePathSuccessProbability(path); + if (chance < fallTolerance) { + logger.trace("INVALID: Too likely to fall on my face."); + continue; + } + + // first crack at logic involving unjamming RACs: just do it + if (needToUnjamRAC && ((path.getMpUsed() > walkMP) || path.isJumping())) { + logger.trace("INADVISABLE: Want to unjam autocannon but path involves running or jumping"); + continue; } + + // If all the above checks have passed, this is a valid path. + logger.trace("VALID"); + returnPaths.add(path); + } // If we've eliminated all valid paths, let's try to pick out a long range path @@ -319,7 +315,7 @@ public Targetable findClosestEnemy(Entity me, Coords position, Game game, /** * Returns the probability of success of a move path */ - protected double getMovePathSuccessProbability(MovePath movePath, StringBuilder msg) { + protected double getMovePathSuccessProbability(MovePath movePath) { // introduced a caching mechanism, as the success probability was being // calculated at least twice if (getPathRankerState().getPathSuccessProbabilities().containsKey(movePath.getKey())) { @@ -329,7 +325,8 @@ protected double getMovePathSuccessProbability(MovePath movePath, StringBuilder MovePath pathCopy = movePath.clone(); List pilotingRolls = getPSRList(pathCopy); double successProbability = 1.0; - msg.append("\n\tCalculating Move Path Success"); + logger.trace("Calculating Move Path Success for {}", pathCopy); + for (TargetRoll roll : pilotingRolls) { // Skip the getting up check. That's handled when checking for being immobile. if (roll.getDesc().toLowerCase().contains("getting up")) { @@ -339,37 +336,31 @@ protected double getMovePathSuccessProbability(MovePath movePath, StringBuilder } boolean naturalAptPilot = movePath.getEntity().hasAbility(OptionsConstants.PILOT_APTITUDE_PILOTING); if (naturalAptPilot) { - msg.append("\n\t\tPilot has Natural Aptitude Piloting"); + logger.trace("Pilot has Natural Aptitude Piloting"); } - msg.append("\n\t\tRoll ").append(roll.getDesc()).append(' ').append(roll.getValue()); double odds = Compute.oddsAbove(roll.getValue(), naturalAptPilot) / 100d; - msg.append(" (").append(NumberFormat.getPercentInstance().format(odds)).append(')'); + logger.trace("Odds above {} = {}", roll.getValue(), odds); successProbability *= odds; } // Account for MASC if (pathCopy.hasActiveMASC()) { - msg.append("\n\t\tMASC "); int target = pathCopy.getEntity().getMASCTarget(); - msg.append(target); // TODO : Does Natural Aptitude Piloting apply to this? I assume not. double odds = Compute.oddsAbove(target) / 100d; - msg.append(" (").append(NumberFormat.getPercentInstance().format(odds)).append(')'); + logger.trace("MASC target {}, odds = {}", target, odds); successProbability *= odds; } // Account for Supercharger if (pathCopy.hasActiveSupercharger()) { - msg.append("\n\t\tSupercharger "); int target = pathCopy.getEntity().getSuperchargerTarget(); - msg.append(target); // todo Does Natural Aptitude Piloting apply to this? I assume not. double odds = Compute.oddsAbove(target) / 100d; - msg.append(" (").append(NumberFormat.getPercentInstance().format(odds)).append(")"); + logger.trace("Supercharger target {}, odds = {}", target, odds); successProbability *= odds; } - msg.append("\n\t\tTotal = ").append(NumberFormat.getPercentInstance().format(successProbability)); - + logger.trace("Success probability = {}", successProbability); getPathRankerState().getPathSuccessProbabilities().put(movePath.getKey(), successProbability); return successProbability; @@ -383,10 +374,9 @@ protected double getMovePathSuccessProbability(MovePath movePath, StringBuilder * * @param movingEntity * @param path - * @param msg * @return */ - protected double calculateMovePathPSRDamage(Entity movingEntity, MovePath path, StringBuilder msg) { + protected double calculateMovePathPSRDamage(Entity movingEntity, MovePath path) { double damage = 0.0; List pilotingRolls = getPSRList(path); @@ -396,11 +386,11 @@ protected double calculateMovePathPSRDamage(Entity movingEntity, MovePath path, if ( description.contains(Messages.getString("TacOps.leaping.leg_damage")) ) { - damage += predictLeapDamage(movingEntity, roll, msg); + damage += predictLeapDamage(movingEntity, roll); } else if ( description.contains(Messages.getString("TacOps.leaping.fall_damage")) ) { - damage += predictLeapFallDamage(movingEntity, roll, msg); + damage += predictLeapFallDamage(movingEntity, roll); } } @@ -452,10 +442,10 @@ public int distanceToHomeEdge(Coords position, CardinalEdge homeEdge, Game game) return distance; } - private String validRange(Coords finalCoords, Targetable target, int startingTargetDistance, + private boolean validRange(Coords finalCoords, Targetable target, int startingTargetDistance, int maxRange, boolean inRange) { if (target == null) { - return null; + return false; } // If I am not currently in range, discard any path that takes me further away @@ -463,15 +453,17 @@ private String validRange(Coords finalCoords, Targetable target, int startingTar int finalDistanceToTarget = finalCoords.distance(target.getPosition()); if (!inRange) { if (finalDistanceToTarget > startingTargetDistance) { - return "INVALID: Not in range and moving further away."; + logger.trace("INVALID: Not in range and moving further away."); + return false; } } else { // If I am in range, discard any path that takes me out of range. if (finalDistanceToTarget > maxRange) { - return "INVALID: In range and moving out of range."; + logger.trace("INVALID: In range and moving out of range."); + return false; } } - return null; + return true; } /** diff --git a/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java b/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java index 04e5d9c3dd6..0e5d77e2b61 100644 --- a/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java +++ b/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java @@ -429,6 +429,37 @@ private WeaponAttackAction buildBombAttackAction(final HashMap bo ) { return 0D; } + + // Handle woods blocking cluster shots + if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_WOODS_COVER)) { + // SRMs, LB-X, Flak AC, AC-2 derivatives, MGs, smaller LRMs, + // and Silver Bullet Gauss are among the weapons + // that lose all effectiveness if this rule is + // on and the target is in woods/jungle. + if ( + game.getBoard().contains(target.getPosition()) + && game.getBoard().getHex(target.getPosition()).containsAnyTerrainOf( + Terrains.WOODS, Terrains.JUNGLE + ) + ) { + int woodsLevel = 2 * Math.max( + game.getBoard().getHex(target.getPosition()).terrainLevel(Terrains.WOODS), + game.getBoard().getHex(target.getPosition()).terrainLevel(Terrains.JUNGLE) + ); + boolean blockedByWoods = ( + weapon.getType().getDamage() == WeaponType.DAMAGE_BY_CLUSTERTABLE + ); + blockedByWoods |= weapon.getType().getRackSize() <= woodsLevel + || weapon.getType().getDamage() <= woodsLevel; + blockedByWoods |= preferredAmmo.getType().getMunitionType().contains( + AmmoType.Munitions.M_CLUSTER + ); + + if (blockedByWoods) { + return 0D; + } + } + } } // bay weapons require special consideration, by looping through all weapons and diff --git a/megamek/src/megamek/client/ui/SharedUtility.java b/megamek/src/megamek/client/ui/SharedUtility.java index a1aa6ad3b32..72e209a63d8 100644 --- a/megamek/src/megamek/client/ui/SharedUtility.java +++ b/megamek/src/megamek/client/ui/SharedUtility.java @@ -19,16 +19,17 @@ import java.util.List; import megamek.client.Client; +import megamek.client.bot.princess.PathRanker; import megamek.common.*; import megamek.common.MovePath.MoveStepType; import megamek.common.annotations.Nullable; import megamek.common.options.OptionsConstants; +import megamek.logging.MMLogger; import megamek.server.totalwarfare.TWGameManager; -import static java.util.List.of; public class SharedUtility { - + private final static MMLogger logger = MMLogger.create(SharedUtility.class); private static int CRIT_VALUE = 100; public static String doPSRCheck(MovePath md) { @@ -935,8 +936,7 @@ public static String[] getDisplayArray(List entities) { return retVal; } - public static @Nullable Targetable getTargetPicked(@Nullable List targets, - @Nullable String chosenDisplayName) { + public static @Nullable Targetable getTargetPicked(@Nullable List targets, @Nullable String chosenDisplayName) { if ((chosenDisplayName == null) || (targets == null)) { return null; } else { @@ -944,16 +944,13 @@ public static String[] getDisplayArray(List entities) { } } - public static double predictLeapFallDamage(Entity movingEntity, TargetRoll data, StringBuilder msg) { + public static double predictLeapFallDamage(Entity movingEntity, TargetRoll data) { // Rough guess based on normal pilots double odds = Compute.oddsAbove(data.getValue(), false) / 100d; int fallHeight = data.getModifiers().get(data.getModifiers().size()-1).getValue(); double fallDamage = Math.round(movingEntity.getWeight() / 10.0) * (fallHeight + 1); - msg.append("\nPredicting expected Leap fall damage:") - .append(String.format("\n\tFall height: %d", fallHeight)) - .append(String.format("\n\tChance of taking damage: %.2f", ((1-odds)*100d))).append('%') - .append(String.format("\n\tExpected total damage from fall: %.2f", fallDamage * (1 - odds) )); + logger.trace("Predicting Leap fall damage for {} at {}% odds, {} fall height", movingEntity.getDisplayName(), odds, fallHeight); return fallDamage * (1 - odds); } @@ -964,30 +961,24 @@ public static double predictLeapFallDamage(Entity movingEntity, TargetRoll data, * 2. risk of falling; mod is distance leaped. * @param movingEntity * @param data - * @param msg * @return */ - public static double predictLeapDamage(Entity movingEntity, TargetRoll data, StringBuilder msg) { + public static double predictLeapDamage(Entity movingEntity, TargetRoll data) { int legMultiplier = (movingEntity.isQuadMek()) ? 4 : 2; double odds = Compute.oddsAbove(data.getValue(), false) / 100d; int fallHeight = data.getModifiers().get(data.getModifiers().size()-1).getValue() / 2; double legDamage = fallHeight * (legMultiplier); - msg.append("\nPredicting expected Leap damage:") - .append(String.format("\n\tFall height: %d", fallHeight)) - .append(String.format("\n\tChance of taking damage: %.2f", ((1-odds)*100d))).append('%'); - + logger.trace("Predicting Leap damage for {} at {}% odds, {} fall height", movingEntity.getDisplayName(), odds, fallHeight); int[] legLocations = {BipedMek.LOC_LLEG, BipedMek.LOC_RLEG, QuadMek.LOC_LARM, QuadMek.LOC_RARM}; // Add required crits; say the effective leg "damage" from a crit is 20 for now. legDamage += legMultiplier * CRIT_VALUE; - msg.append( - String.format("\n\tAdding %d leg critical chances as %d additional damage", legMultiplier, legMultiplier * CRIT_VALUE) - ); + logger.trace("Adding {} leg critical chances as {} additional damage", legMultiplier, legMultiplier * CRIT_VALUE); // Add additional crits for each leg that would take internal damage for (int i=0;i void moveElement(DefaultListModel srcModel, int srcIndex, int trg private final JCheckBox aOHexShadows = new JCheckBox(Messages.getString("CommonSettingsDialog.aOHexSHadows")); private final JCheckBox floatingIso = new JCheckBox(Messages.getString("CommonSettingsDialog.floatingIso")); private final JCheckBox mmSymbol = new JCheckBox(Messages.getString("CommonSettingsDialog.mmSymbol")); + private final JCheckBox drawFacingArrowsOnMiniMap = new JCheckBox(Messages.getString("CommonSettingsDialog.drawFacingArrowsOnMiniMap")); + private final JCheckBox drawSensorRangeOnMiniMap = new JCheckBox(Messages.getString("CommonSettingsDialog.drawSensorRangeOnMiniMap")); + private final JCheckBox paintBordersOnMiniMap = new JCheckBox(Messages.getString("CommonSettingsDialog.paintBordersOnMiniMap")); private final JCheckBox entityOwnerColor = new JCheckBox( Messages.getString("CommonSettingsDialog.entityOwnerColor")); private final JCheckBox teamColoring = new JCheckBox(Messages.getString("CommonSettingsDialog.teamColoring")); @@ -502,6 +505,9 @@ private void moveElement(DefaultListModel srcModel, int srcIndex, int trg private boolean savedLevelhighlight; private boolean savedFloatingIso; private boolean savedMmSymbol; + private boolean savedDrawFacingArrowsOnMiniMap; + private boolean savedDrawSensorRangeOnMiniMap; + private boolean savedPaintBorders; private boolean savedTeamColoring; private boolean savedDockOnLeft; private boolean savedDockMultipleOnYAxis; @@ -1659,6 +1665,9 @@ private JPanel getMiniMapPanel() { comps.add(checkboxEntry(gameSummaryMM, Messages.getString("CommonSettingsDialog.gameSummaryMM.tooltip", Configuration.gameSummaryImagesMMDir()))); + comps.add(checkboxEntry(drawFacingArrowsOnMiniMap, null)); + comps.add(checkboxEntry(drawSensorRangeOnMiniMap, null)); + comps.add(checkboxEntry(paintBordersOnMiniMap, null)); return createSettingsPanel(comps); } @@ -2049,6 +2058,9 @@ public void setVisible(boolean visible) { aOHexShadows.setSelected(GUIP.getAOHexShadows()); floatingIso.setSelected(GUIP.getFloatingIso()); mmSymbol.setSelected(GUIP.getMmSymbol()); + drawFacingArrowsOnMiniMap.setSelected(GUIP.getDrawFacingArrowsOnMiniMap()); + drawSensorRangeOnMiniMap.setSelected(GUIP.getDrawSensorRangeOnMiniMap()); + paintBordersOnMiniMap.setSelected(GUIP.paintBorders()); levelhighlight.setSelected(GUIP.getLevelHighlight()); shadowMap.setSelected(GUIP.getShadowMap()); hexInclines.setSelected(GUIP.getHexInclines()); @@ -2070,7 +2082,7 @@ public void setVisible(boolean visible) { } } - minimapTheme.setSelectedItem(CLIENT_PREFERENCES.getMinimapTheme()); + minimapTheme.setSelectedItem(CLIENT_PREFERENCES.getMinimapTheme().getName()); gameSummaryBV.setSelected(GUIP.getGameSummaryBoardView()); gameSummaryMM.setSelected(GUIP.getGameSummaryMinimap()); @@ -2139,6 +2151,9 @@ public void setVisible(boolean visible) { savedLevelhighlight = GUIP.getLevelHighlight(); savedFloatingIso = GUIP.getFloatingIso(); savedMmSymbol = GUIP.getMmSymbol(); + savedDrawFacingArrowsOnMiniMap = GUIP.getDrawFacingArrowsOnMiniMap(); + savedDrawSensorRangeOnMiniMap = GUIP.getDrawSensorRangeOnMiniMap(); + savedPaintBorders = GUIP.paintBorders(); savedTeamColoring = GUIP.getTeamColoring(); savedDockOnLeft = GUIP.getDockOnLeft(); savedDockMultipleOnYAxis = GUIP.getDockMultipleOnYAxis(); @@ -2179,6 +2194,9 @@ protected void cancelAction() { GUIP.setLevelHighlight(savedLevelhighlight); GUIP.setFloatingIso(savedFloatingIso); GUIP.setMmSymbol(savedMmSymbol); + GUIP.setDrawSensorRangeOnMiniMap(savedDrawSensorRangeOnMiniMap); + GUIP.setDrawFacingArrowsOnMiniMap(savedDrawFacingArrowsOnMiniMap); + GUIP.setPaintBorders(savedPaintBorders); GUIP.setTeamColoring(savedTeamColoring); GUIP.setDockOnLeft(savedDockOnLeft); GUIP.setDockMultipleOnYAxis(savedDockMultipleOnYAxis); @@ -2429,7 +2447,9 @@ protected void okAction() { GUIP.setAttackArrowTransparency((Integer) attackArrowTransparency.getValue()); GUIP.setECMTransparency((Integer) ecmTransparency.getValue()); - + GUIP.setDrawFacingArrowsOnMiniMap(drawFacingArrowsOnMiniMap.isSelected()); + GUIP.setDrawSensorRangeOnMiniMap(drawSensorRangeOnMiniMap.isSelected()); + GUIP.setPaintBorders(paintBordersOnMiniMap.isSelected()); try { GUIP.setButtonsPerRow(Integer.parseInt(buttonsPerRow.getText())); } catch (Exception ex) { @@ -2909,6 +2929,12 @@ public void itemStateChanged(ItemEvent event) { GUIP.setShowDamageLevel(showDamageLevel.isSelected()); } else if (source.equals(chkHighQualityGraphics)) { GUIP.setHighQualityGraphics(chkHighQualityGraphics.isSelected()); + } else if (source.equals(drawFacingArrowsOnMiniMap)) { + GUIP.setDrawFacingArrowsOnMiniMap(drawFacingArrowsOnMiniMap.isSelected()); + } else if (source.equals(drawSensorRangeOnMiniMap)) { + GUIP.setDrawFacingArrowsOnMiniMap(drawSensorRangeOnMiniMap.isSelected()); + } else if (source.equals(paintBordersOnMiniMap)) { + GUIP.setPaintBorders(paintBordersOnMiniMap.isSelected()); } } diff --git a/megamek/src/megamek/client/ui/swing/GUIPreferences.java b/megamek/src/megamek/client/ui/swing/GUIPreferences.java index d01bf4476e0..1f917ecd883 100644 --- a/megamek/src/megamek/client/ui/swing/GUIPreferences.java +++ b/megamek/src/megamek/client/ui/swing/GUIPreferences.java @@ -288,6 +288,9 @@ public class GUIPreferences extends PreferenceStoreProxy { public static final String MINI_MAP_SYMBOLS_DISPLAY_MODE = "MinimapSymbolsDisplayMode"; public static final String MINI_MAP_AUTO_DISPLAY_REPORT_PHASE = "MinimapAutoDisplayReportPhase"; public static final String MINI_MAP_AUTO_DISPLAY_NONREPORT_PHASE = "MinimapAutoDisplayNonReportPhase"; + public static final String MINI_MAP_SHOW_SENSOR_RANGE = "MinimapShowSensorRange"; + public static final String MINI_MAP_SHOW_FACING_ARROW = "MinimapShowFacingArrow"; + public static final String MINI_MAP_PAINT_BORDERS = "MinimapPaintBorders"; public static final String FIRE_DISPLAY_TAB_DURING_PHASES = "FireDisplayTabDuringPhases"; public static final String MOVE_DISPLAY_TAB_DURING_PHASES = "MoveDisplayTabDuringPhases"; public static final String MINIMUM_SIZE_HEIGHT = "MinimumSizeHeight"; @@ -681,6 +684,10 @@ protected GUIPreferences() { store.setDefault(MINI_MAP_ENABLED, true); store.setDefault(MINI_MAP_AUTO_DISPLAY_REPORT_PHASE, 0); store.setDefault(MINI_MAP_AUTO_DISPLAY_NONREPORT_PHASE, 1); + store.setDefault(MINI_MAP_SHOW_SENSOR_RANGE, true); + store.setDefault(MINI_MAP_SHOW_FACING_ARROW, true); + store.setDefault(MINI_MAP_PAINT_BORDERS, true); + store.setDefault(MOVE_DISPLAY_TAB_DURING_PHASES, true); store.setDefault(FIRE_DISPLAY_TAB_DURING_PHASES, true); @@ -3435,4 +3442,28 @@ public File[] getMinimapThemes() { // List all .theme files inside the minimap themes folder return Configuration.minimapThemesDir().listFiles((dir, name) -> name.endsWith(".theme")); } + + public boolean getDrawFacingArrowsOnMiniMap() { + return getBoolean(MINI_MAP_SHOW_SENSOR_RANGE); + } + + public boolean getDrawSensorRangeOnMiniMap() { + return getBoolean(MINI_MAP_SHOW_FACING_ARROW); + } + + public void setDrawFacingArrowsOnMiniMap(boolean state) { + store.setValue(MINI_MAP_SHOW_SENSOR_RANGE, state); + } + + public void setDrawSensorRangeOnMiniMap(boolean state) { + store.setValue(MINI_MAP_SHOW_FACING_ARROW, state); + } + + public boolean paintBorders() { + return getBoolean(MINI_MAP_PAINT_BORDERS); + } + + public void setPaintBorders(boolean state) { + store.setValue(MINI_MAP_PAINT_BORDERS, state); + } } diff --git a/megamek/src/megamek/client/ui/swing/MovementDisplay.java b/megamek/src/megamek/client/ui/swing/MovementDisplay.java index 81505af4232..138c59a6011 100644 --- a/megamek/src/megamek/client/ui/swing/MovementDisplay.java +++ b/megamek/src/megamek/client/ui/swing/MovementDisplay.java @@ -975,6 +975,9 @@ private void updateMove(boolean redrawMovement) { } private void updateFleeButton() { + if (ce() == null) { + return; + } boolean hasLastStep = (cmd != null) && (cmd.getLastStep() != null); boolean fleeStart = !hasLastStep && ce().canFlee(ce().getPosition()); @@ -4665,6 +4668,10 @@ private void computeCFWarningHexes(Entity ce) { @Override public synchronized void actionPerformed(ActionEvent ev) { final Entity ce = ce(); + final String actionCmd = ev.getActionCommand(); + if (actionCmd.equals(MoveCommand.MOVE_NEXT.getCmd())) { + selectEntity(clientgui.getClient().getNextEntityNum(currentEntity)); + } if (ce == null) { return; @@ -4678,11 +4685,8 @@ public synchronized void actionPerformed(ActionEvent ev) { // odd... return; } - final String actionCmd = ev.getActionCommand(); final IGameOptions opts = clientgui.getClient().getGame().getOptions(); - if (actionCmd.equals(MoveCommand.MOVE_NEXT.getCmd())) { - selectEntity(clientgui.getClient().getNextEntityNum(currentEntity)); - } else if (actionCmd.equals( + if (actionCmd.equals( MoveCommand.MOVE_FORWARD_INI.getCmd())) { selectNextPlayer(); } else if (actionCmd.equals(MoveCommand.MOVE_CANCEL.getCmd())) { diff --git a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java index 266d86e285d..91e1f9fe18c 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/BoardView.java +++ b/megamek/src/megamek/client/ui/swing/boardview/BoardView.java @@ -1900,7 +1900,7 @@ private void drawHex(Coords c, Graphics boardGraph, boolean saveBoardImage) { largestLevelDiff = levelDiff; } } - imgHeight += HEX_ELEV * scale * largestLevelDiff; + imgHeight += (int) (HEX_ELEV * scale * largestLevelDiff); } // If the base image isn't ready, we should signal a repaint and stop if ((imgWidth < 0) || (imgHeight < 0)) { @@ -5201,7 +5201,7 @@ Entity getSelectedEntity() { return clientgui != null ? clientgui.getDisplayedUnit() : null; } - FovHighlightingAndDarkening getFovHighlighting() { + public FovHighlightingAndDarkening getFovHighlighting() { return fovHighlightingAndDarkening; } diff --git a/megamek/src/megamek/client/ui/swing/boardview/FovHighlightingAndDarkening.java b/megamek/src/megamek/client/ui/swing/boardview/FovHighlightingAndDarkening.java index 87b551fa24f..2f1be4513d0 100644 --- a/megamek/src/megamek/client/ui/swing/boardview/FovHighlightingAndDarkening.java +++ b/megamek/src/megamek/client/ui/swing/boardview/FovHighlightingAndDarkening.java @@ -50,7 +50,7 @@ /** * A helper class for highlighting and darkening hexes. */ -class FovHighlightingAndDarkening { +public class FovHighlightingAndDarkening { private static final MMLogger logger = MMLogger.create(FovHighlightingAndDarkening.class); private final BoardView boardView1; @@ -266,6 +266,14 @@ private void clearCache() { GameListener cacheGameListner; + /** + * Returns the cached all ECM info. + * @return the cached all ECM info, nullable + */ + public @Nullable List getCachedECMInfo() { + return cachedAllECMInfo; + } + /** * Checks for los effects, preferably from cache, if not getLosEffects * is invoked and it's return value is cached. diff --git a/megamek/src/megamek/client/ui/swing/forceDisplay/ForceDisplayMekTreeRenderer.java b/megamek/src/megamek/client/ui/swing/forceDisplay/ForceDisplayMekTreeRenderer.java index 22ac552a6df..04d1ecfcd1c 100644 --- a/megamek/src/megamek/client/ui/swing/forceDisplay/ForceDisplayMekTreeRenderer.java +++ b/megamek/src/megamek/client/ui/swing/forceDisplay/ForceDisplayMekTreeRenderer.java @@ -89,7 +89,7 @@ public Component getTreeCellRendererComponent(JTree tree, Object value, boolean setIcon(getToolkit().getImage(UNKNOWN_UNIT), size - 5); } else { Camouflage camo = entity.getCamouflageOrElseOwners(); - Image image = clientGUI.getBoardView().getTilesetManager().loadPreviewImage(entity, camo); + Image image = clientGUI.getBoardView().getTilesetManager().loadPreviewImage(entity, camo, false); setIconTextGap(UIUtil.scaleForGUI(10)); setIcon(image, size); } diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekCellFormatter.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekCellFormatter.java index e99114f09a4..28c55331ab7 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekCellFormatter.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekCellFormatter.java @@ -86,6 +86,7 @@ static String formatUnitFull(Entity entity, ChatLounge lobby, boolean forceView) Client client = lobby.getClientgui().getClient(); Game game = client.getGame(); + GameOptions options = game.getOptions(); Player localPlayer = client.getLocalPlayer(); Player owner = entity.getOwner(); @@ -128,7 +129,7 @@ static String formatUnitFull(Entity entity, ChatLounge lobby, boolean forceView) } // Critical (Red) Warnings - if ((entity.getGame().getPlanetaryConditions().whyDoomed(entity, entity.getGame()) != null) + if ((game.getPlanetaryConditions().whyDoomed(entity, entity.getGame()) != null) || (entity.doomedInAtmosphere() && mapType == MapSettings.MEDIUM_ATMOSPHERE) || (entity.doomedOnGround() && mapType == MapSettings.MEDIUM_GROUND) || (entity.doomedInSpace() && mapType == MapSettings.MEDIUM_SPACE) @@ -563,7 +564,7 @@ static String formatUnitCompact(Entity entity, ChatLounge lobby, boolean forceVi } // Critical (Red) Warnings - if ((entity.getGame().getPlanetaryConditions().whyDoomed(entity, entity.getGame()) != null) + if ((game.getPlanetaryConditions().whyDoomed(entity, entity.getGame()) != null) || (entity.doomedInAtmosphere() && mapType == MapSettings.MEDIUM_ATMOSPHERE) || (entity.doomedOnGround() && mapType == MapSettings.MEDIUM_GROUND) || (entity.doomedInSpace() && mapType == MapSettings.MEDIUM_SPACE) diff --git a/megamek/src/megamek/client/ui/swing/minimap/BoardviewlessMinimap.java b/megamek/src/megamek/client/ui/swing/minimap/BoardviewlessMinimap.java index 6531e790405..ee4cc64f17c 100644 --- a/megamek/src/megamek/client/ui/swing/minimap/BoardviewlessMinimap.java +++ b/megamek/src/megamek/client/ui/swing/minimap/BoardviewlessMinimap.java @@ -25,6 +25,7 @@ import megamek.common.actions.AttackAction; import megamek.common.actions.EntityAction; import megamek.common.event.*; +import megamek.common.options.OptionsConstants; import megamek.common.preference.ClientPreferences; import megamek.common.preference.PreferenceManager; @@ -57,7 +58,7 @@ public class BoardviewlessMinimap extends JPanel implements OverlayPainter { private final List lines; private final List attackActions; private final List overlays; - + private final static int unitSize = 10; private record Blip(int x, int y, String code, IFF iff , Color color, int round) {}; private record Line(int x1, int y1, int x2, int y2, Color color, int round) {}; @@ -110,13 +111,6 @@ public void gameBoardChanged(GameBoardChangeEvent e) { @Override public void gamePhaseChange(GamePhaseChangeEvent e) { update(); -// if (e.getNewPhase() == GamePhase.MOVEMENT_REPORT) { -// update(); -// } else if (e.getNewPhase() == GamePhase.FIRING_REPORT) { -// update(); -// } else if (e.getNewPhase() == GamePhase.END) { -// update(); -// } } @Override @@ -311,6 +305,9 @@ protected void paintComponent(Graphics g) { if (entity.getPosition() != null) { paintUnit(g, entity); } + if (entity.getPosition() != null) { + paintSensor(g, entity); + } } } @@ -385,7 +382,7 @@ private static Color fadeColor(Color color, double alphaDivisor) { private void drawAutoHit(Graphics g, Coords hex) { int baseX = (hex.getX() * (HEX_SIDE[zoom] + HEX_SIDE_BY_SIN30[zoom])) + leftMargin + HEX_SIDE[zoom] + xOffset; int baseY = (((2 * hex.getY()) + 1 + (hex.getX() % 2)) * HEX_SIDE_BY_COS30[zoom]) + topMargin + yOffset; - int unitSize = 10; + g.setColor(Color.RED); g.drawOval(baseX - (unitSize - 1), baseY - (unitSize - 1), (2 * unitSize) - 2, (2 * unitSize) - 2); g.drawLine(baseX - unitSize - 1, baseY, (baseX - unitSize) + 3, baseY); @@ -398,6 +395,7 @@ private void drawAutoHit(Graphics g, Coords hex) { private void paintUnit(Graphics g, Entity entity) { int x = entity.getPosition().getX(); int y = entity.getPosition().getY(); + int facing = entity.getFacing(); int baseX = x * (HEX_SIDE[zoom] + HEX_SIDE_BY_SIN30[zoom]) + leftMargin + HEX_SIDE[zoom] + xOffset; int baseY = (2 * y + 1 + (x % 2)) * HEX_SIDE_BY_COS30[zoom] + topMargin + yOffset; @@ -419,14 +417,11 @@ private void paintUnit(Graphics g, Entity entity) { Graphics2D g2 = (Graphics2D) g; Stroke saveStroke = g2.getStroke(); AffineTransform saveTransform = g2.getTransform(); - boolean stratOpsSymbols = GUIP.getMmSymbol(); - // Choose player or team color depending on preferences Color iconColor = entity.getOwner().getColour().getColour(false); if (GUIP.getTeamColoring()) { boolean isLocalTeam = entity.getOwner().getTeam() == client.getLocalPlayer().getTeam(); boolean isLocalPlayer = entity.getOwner().equals(client.getLocalPlayer()); -// iconColor = IFF.getPlayerIff(client.getLocalPlayer(), entity.getOwner()).getColor(); if (isLocalPlayer) { iconColor = GUIP.getMyUnitColor(); } else if (isLocalTeam) { @@ -435,6 +430,7 @@ private void paintUnit(Graphics g, Entity entity) { iconColor = GUIP.getEnemyUnitColor(); } } + // Transform for placement and scaling var placement = AffineTransform.getTranslateInstance(baseX, baseY); @@ -456,80 +452,154 @@ private void paintUnit(Graphics g, Entity entity) { float innerBorderWidth = 10f; float formStrokeWidth = 20f; - if (stratOpsSymbols) { - // White border to set off the icon from the background - g2.setStroke(new BasicStroke(outerBorderWidth, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL)); - g2.setColor(Color.BLACK); - g2.draw(STRAT_BASERECT); + // White border to set off the icon from the background + g2.setStroke(new BasicStroke(outerBorderWidth, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL)); + g2.setColor(Color.BLACK); + g2.draw(STRAT_BASERECT); - // Black background to fill forms like the DropShip - g2.setColor(fontColor); - g2.fill(STRAT_BASERECT); + // Black background to fill forms like the DropShip + g2.setColor(fontColor); + g2.fill(STRAT_BASERECT); - // Set a thin brush for filled areas (leave a thick brush for line symbols - if ((entity instanceof Mek) || (entity instanceof ProtoMek) - || (entity instanceof VTOL) || (entity.isAero())) { - g2.setStroke(new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); - } else { - g2.setStroke(new BasicStroke(formStrokeWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + // Set a thin brush for filled areas (leave a thick brush for line symbols + if ((entity instanceof Mek) || (entity instanceof ProtoMek) + || (entity instanceof VTOL) || (entity.isAero())) { + g2.setStroke(new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); + } else { + g2.setStroke(new BasicStroke(formStrokeWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + } + + // Fill the form in player color / team color + g.setColor(iconColor); + g2.fill(form); + + // Add the weight class or other lettering for certain units + g.setColor(fontColor); + if ((entity instanceof ProtoMek) || (entity instanceof Mek) || (entity instanceof Aero)) { + String s = ""; + if (entity instanceof ProtoMek) { + s = "P"; + } else if ((entity instanceof Mek) && ((Mek) entity).isIndustrial()) { + s = "I"; + } else if (entity.getWeightClass() < 6) { + s = STRAT_WEIGHTS[entity.getWeightClass()]; + } + if (!s.isBlank()) { + var fontContext = new FontRenderContext(null, true, true); + var font = new Font(MMConstants.FONT_SANS_SERIF, Font.BOLD, 100); + FontMetrics currentMetrics = getFontMetrics(font); + int stringWidth = currentMetrics.stringWidth(s); + GlyphVector gv = font.createGlyphVector(fontContext, s); + g2.fill(gv.getOutline((int) STRAT_CX - (float) stringWidth / 2, + (float) STRAT_SYMBOLSIZE.getHeight() / 3.0f)); } + } else if (entity instanceof MekWarrior) { + g2.setColor(fontColor); + g2.fillOval(-25, -25, 50, 50); + } + // Draw the unit icon in black + g2.draw(form); + g2.setStroke(new BasicStroke(innerBorderWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); - // Fill the form in player color / team color + // Rectangle border for all units + g2.setColor(borderColor); + g2.draw(STRAT_BASERECT); + + // Draw Facing Arrow + if (facing > -1) { + g2.setColor(Color.BLACK); + g2.rotate(Math.toRadians(facing * 60)); + g2.draw(FACING_ARROW); g.setColor(iconColor); - g2.fill(form); - - // Add the weight class or other lettering for certain units - g.setColor(fontColor); - if ((entity instanceof ProtoMek) || (entity instanceof Mek) || (entity instanceof Aero)) { - String s = ""; - if (entity instanceof ProtoMek) { - s = "P"; - } else if ((entity instanceof Mek) && ((Mek) entity).isIndustrial()) { - s = "I"; - } else if (entity.getWeightClass() < 6) { - s = STRAT_WEIGHTS[entity.getWeightClass()]; - } - if (!s.isBlank()) { - var fontContext = new FontRenderContext(null, true, true); - var font = new Font(MMConstants.FONT_SANS_SERIF, Font.BOLD, 100); - FontMetrics currentMetrics = getFontMetrics(font); - int stringWidth = currentMetrics.stringWidth(s); - GlyphVector gv = font.createGlyphVector(fontContext, s); - g2.fill(gv.getOutline((int) STRAT_CX - (float) stringWidth / 2, - (float) STRAT_SYMBOLSIZE.getHeight() / 3.0f)); - } - } else if (entity instanceof MekWarrior) { - g2.setColor(fontColor); - g2.fillOval(-25, -25, 50, 50); + g2.fill(FACING_ARROW); + } + + g2.setTransform(saveTransform); + g2.setStroke(saveStroke); + } + + + /** Draws the symbol for a single entity. Checks visibility in double blind. */ + private void paintSensor(Graphics g, Entity entity) { + + if (EntityVisibilityUtils.onlyDetectedBySensors(client.getLocalPlayer(), entity)) { + // This unit is visible only as a sensor Return + return; + } else if (game instanceof Game twGame && !EntityVisibilityUtils.detectedOrHasVisual(client.getLocalPlayer(), twGame, entity)) { + // This unit is not visible, don't draw it + return; + } + + Graphics2D g2 = (Graphics2D) g; + Stroke saveStroke = g2.getStroke(); + Color iconColor = entity.getOwner().getColour().getColour(false); + if (GUIP.getTeamColoring()) { + boolean isLocalTeam = entity.getOwner().getTeam() == client.getLocalPlayer().getTeam(); + boolean isLocalPlayer = entity.getOwner().equals(client.getLocalPlayer()); + if (isLocalPlayer) { + iconColor = GUIP.getMyUnitColor(); + } else if (isLocalTeam) { + iconColor = GUIP.getAllyUnitColor(); + } else { + iconColor = GUIP.getEnemyUnitColor(); } - // Draw the unit icon in black - g2.draw(form); + } + Color iconColorSemiTransparent = new Color(iconColor.getRed(), iconColor.getGreen(), iconColor.getBlue(), 200); + // White border to set off the icon from the background + g2.setStroke(new BasicStroke(2, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL)); + g2.setColor(iconColorSemiTransparent); - // Rectangle border for all units - g2.setColor(borderColor); - g2.setStroke(new BasicStroke(innerBorderWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); - g2.draw(STRAT_BASERECT); - } else { - // Standard symbols - // White border to set off the icon from the background - g2.setStroke(new BasicStroke(outerBorderWidth, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND)); - g2.setColor(Color.BLACK); - g2.draw(form); + int maxSensorRange = 0; + int minSensorRange = 0; - // Fill the form in player color / team color - g.setColor(iconColor); - g2.fill(form); + if (game.getOptions().booleanOption(OptionsConstants.ADVANCED_TACOPS_SENSORS)) { + int bracket = Compute.getSensorRangeBracket(entity, null, null); + // noinspection ConstantConditions + int range = Compute.getSensorRangeByBracket((Game) game, entity, null, null); - // Black border - g2.setColor(borderColor); - g2.setStroke(new BasicStroke(innerBorderWidth / 2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); - g2.draw(form); + maxSensorRange = bracket * range; + minSensorRange = Math.max((bracket - 1) * range, 0); + if (game.getOptions().booleanOption(OptionsConstants.ADVANCED_INCLUSIVE_SENSOR_RANGE)) { + minSensorRange = 0; + } + } + Coords origin = entity.getPosition(); + + for (var sensorRange : List.of(minSensorRange, maxSensorRange)) { + if (sensorRange <= 0) { + continue; + } + + int xo; + int yo; + var sensor = new Path2D.Double(); + + var internalOrExternal = (sensorRange == minSensorRange) && (maxSensorRange != 0) ? -1 : 1; + for (int i = 0; i < 6; i++) { + var movingCoord = origin.translated(i, sensorRange + internalOrExternal); + xo = coordsXToPixel(movingCoord.getX()); + yo = coordsYtoPixel(movingCoord.getY(), movingCoord.getX()); + if (i == 0) { + sensor.moveTo(xo+xOffset, yo+yOffset); + } else { + sensor.lineTo(xo+xOffset, yo+yOffset); + } + } + sensor.closePath(); + g2.draw(sensor); } - g2.setTransform(saveTransform); g2.setStroke(saveStroke); } + private int coordsYtoPixel(int y, int x) { + return (2 * y + 1 + (x % 2)) * HEX_SIDE_BY_COS30[zoom] + topMargin; + } + + private int coordsXToPixel(int x) { + return x * (HEX_SIDE[zoom] + HEX_SIDE_BY_SIN30[zoom]) + leftMargin + HEX_SIDE[zoom]; + } + public void forceBoardRedraw() { this.boardNeedsRedraw = true; repaint(); diff --git a/megamek/src/megamek/client/ui/swing/minimap/Minimap.java b/megamek/src/megamek/client/ui/swing/minimap/Minimap.java index 53a4b582be9..359ddec0f1b 100644 --- a/megamek/src/megamek/client/ui/swing/minimap/Minimap.java +++ b/megamek/src/megamek/client/ui/swing/minimap/Minimap.java @@ -20,9 +20,8 @@ */ package megamek.client.ui.swing.minimap; -import static megamek.client.ui.swing.minimap.MinimapUnitSymbols.STRAT_BASERECT; -import static megamek.client.ui.swing.minimap.MinimapUnitSymbols.STRAT_CX; -import static megamek.client.ui.swing.minimap.MinimapUnitSymbols.STRAT_SYMBOLSIZE; +import static megamek.client.ui.swing.minimap.MinimapUnitSymbols.*; +import static megamek.client.ui.swing.minimap.MinimapUnitSymbols.FACING_ARROW; import static megamek.common.Terrains.*; import java.awt.*; @@ -53,6 +52,7 @@ import megamek.MMConstants; import megamek.client.Client; +import megamek.client.IClient; import megamek.client.event.BoardViewEvent; import megamek.client.event.BoardViewListener; import megamek.client.event.BoardViewListenerAdapter; @@ -69,12 +69,12 @@ import megamek.common.actions.WeaponAttackAction; import megamek.common.annotations.Nullable; import megamek.common.event.*; +import megamek.common.options.OptionsConstants; import megamek.common.preference.ClientPreferences; import megamek.common.preference.IPreferenceChangeListener; import megamek.common.preference.PreferenceChangeEvent; import megamek.common.preference.PreferenceManager; import megamek.common.util.ImageUtil; -import megamek.common.util.fileUtils.MegaMekFile; import megamek.logging.MMLogger; /** @@ -134,6 +134,8 @@ public final class Minimap extends JPanel implements IPreferenceChangeListener { private static final String ACTION_HEIGHT_TOTAL = "HEIGHT_TOTAL"; private static final String ACTION_SYMBOLS_NO = "SYMBOLS_NO"; private static final String ACTION_SYMBOLS_SHOW = "SYMBOLS_SHOW"; + private static final String ACTION_FACING_ARROWS_SHOW = "FACING_ARROWS_SHOW"; + private static final String ACTION_SENSORS_SHOW = "SENSORS_SHOW"; private static final GUIPreferences GUIP = GUIPreferences.getInstance(); private static final ClientPreferences CLIENT_PREFERENCES = PreferenceManager.getClientPreferences(); @@ -164,7 +166,9 @@ public final class Minimap extends JPanel implements IPreferenceChangeListener { private int zoom = GUIP.getMinimapZoom(); private int heightDisplayMode = GUIP.getMinimapHeightDisplayMode(); private int symbolsDisplayMode = GUIP.getMinimapSymbolsDisplayMode(); - + private boolean drawSensorRangeOnMiniMap = GUIP.getDrawSensorRangeOnMiniMap(); + private boolean drawFacingArrowsOnMiniMap = GUIP.getDrawFacingArrowsOnMiniMap(); + private boolean paintBorders = GUIP.paintBorders(); private Coords firstLOS; private Coords secondLOS; @@ -234,12 +238,20 @@ public static BufferedImage getMinimapImage(Board board, int zoom, @Nullable Fil * game and boardview object will be used to display additional information. */ public static BufferedImage getMinimapImage(Game game, BoardView bv, int zoom, @Nullable File minimapTheme) { + return getMinimapImage(game, bv, zoom, null, minimapTheme); + } + + /** + * Returns a minimap image of the given board at the given zoom index. The + * game and boardview object will be used to display additional information. + */ + public static BufferedImage getMinimapImage(Game game, BoardView bv, int zoom, IClientGUI clientGui, @Nullable File minimapTheme) { try { // Send the fail image when the zoom index is wrong to make this noticeable if ((zoom < MIM_ZOOM) || (zoom > MAX_ZOOM)) { throw new Exception("The given zoom index is out of bounds."); } - Minimap tempMM = new Minimap(null, game, bv, null, minimapTheme); + Minimap tempMM = new Minimap(null, game, bv, clientGui, minimapTheme); tempMM.zoom = zoom; tempMM.initializeMap(); tempMM.drawMap(true); @@ -266,6 +278,7 @@ private Minimap(@Nullable JDialog dlg, Game g, @Nullable BoardView bview, @Nulla bv = bview; dialog = dlg; clientGui = cg; + if (clientGui != null && clientGui.getClient() instanceof Client castClient) { client = castClient; } @@ -283,7 +296,59 @@ private Minimap(@Nullable JDialog dlg, Game g, @Nullable BoardView bview, @Nulla * are not null). */ private void initializeListeners() { - game.addGameListener(gameListener); + game.addGameListener(new GameListenerAdapter() { + @Override + public void gamePhaseChange(GamePhaseChangeEvent e) { + if (GUIP.getGameSummaryMinimap() + && (e.getOldPhase().isDeployment() || e.getOldPhase().isMovement() + || e.getOldPhase().isTargeting() || e.getOldPhase().isPremovement() + || e.getOldPhase().isPrefiring() || e.getOldPhase().isFiring() + || e.getOldPhase().isPhysical())) { + + File dir = new File(Configuration.gameSummaryImagesMMDir(), game.getUUIDString()); + if (!dir.exists()) { + dir.mkdirs(); + } + File imgFile = new File(dir, "round_" + game.getRoundCount() + "_" + e.getOldPhase().ordinal() + "_" + + e.getOldPhase() + ".png"); + try { + ImageIO.write(getMinimapImage(game, bv, GAME_SUMMARY_ZOOM, clientGui, null), "png", imgFile); + } catch (Exception ex) { + logger.error(ex, ""); + } + } + refreshMap(); + } + + @Override + public void gameTurnChange(GameTurnChangeEvent e) { + refreshMap(); + } + + @Override + public void gameBoardNew(GameBoardNewEvent e) { + Board b = e.getOldBoard(); + if (b != null) { + b.removeBoardListener(boardListener); + } + b = e.getNewBoard(); + if (b != null) { + b.addBoardListener(boardListener); + } + board = b; + initializeMap(); + } + + @Override + public void gameBoardChanged(GameBoardChangeEvent e) { + refreshMap(); + } + + @Override + public void gameNewAction(GameNewActionEvent e) { + refreshMap(); + } + }); board.addBoardListener(boardListener); if (bv != null) { bv.addBoardViewListener(boardViewListener); @@ -305,6 +370,16 @@ private void initializeDialog() { } } + private @Nullable Player getLocalPlayer() { + if (client != null && client.getLocalPlayer() != null) { + return client.getLocalPlayer(); + } + if (clientGui != null && clientGui.getClient() != null && clientGui.getClient().getLocalPlayer() != null) { + return clientGui.getClient().getLocalPlayer(); + } + return null; + } + @Override protected void paintComponent(Graphics g) { if (mapImage != null) { @@ -559,7 +634,7 @@ private void drawMap(boolean forceDraw) { } else if (h.containsTerrain(SKY)) { paintLowAtmoSkyCoord(gg, j, k); } else { - paintCoord(gg, j, k, zoom > 1); + paintCoord(gg, j, k, paintBorders && zoom > 1); } } addRoadElements(h, j, k); @@ -592,7 +667,7 @@ private void drawMap(boolean forceDraw) { } drawDeploymentZone(g); - + // In case the flag SHOW SYMBOLS is set, it will draw the units and other stuff if (symbolsDisplayMode == SHOW_SYMBOLS) { if (null != game) { // draw declared fire @@ -608,6 +683,14 @@ private void drawMap(boolean forceDraw) { paintUnit(g, e); } } + + if (drawSensorRangeOnMiniMap) { + for (Entity e : game.getEntitiesVector()) { + if (e.getPosition() != null) { + paintSensor(g, e); + } + } + } } if ((client != null) && (client.getArtilleryAutoHit() != null)) { @@ -629,7 +712,10 @@ private void drawDeploymentZone(Graphics g) { if ((null != client) && (null != game) && game.getPhase().isDeployment() && (bv != null) && (bv.getDeployingEntity() != null) && (dialog != null)) { GameTurn turn = game.getTurn(); - if ((turn != null) && (turn.playerId() == client.getLocalPlayer().getId())) { + if (getLocalPlayer() == null) { + return; + } + if ((turn != null) && (turn.playerId() == getLocalPlayer().getId())) { Entity deployingUnit = bv.getDeployingEntity(); for (int j = 0; j < board.getWidth(); j++) { @@ -924,8 +1010,8 @@ private void paintAttack(Graphics g, AttackAction attack) { } if (attack instanceof WeaponAttackAction) { WeaponAttackAction waa = (WeaponAttackAction) attack; - if ((attack.getTargetType() == Targetable.TYPE_HEX_ARTILLERY) - && (waa.getEntity(game).getOwner().getId() != client.getLocalPlayer().getId())) { + if ((getLocalPlayer() == null) ||( (attack.getTargetType() == Targetable.TYPE_HEX_ARTILLERY) + && (waa.getEntity(game).getOwner().getId() != getLocalPlayer().getId()))) { return; } } @@ -998,10 +1084,10 @@ private void paintAttack(Graphics g, AttackAction attack) { private void paintUnit(Graphics g, Entity entity) { int x = entity.getPosition().getX(); int y = entity.getPosition().getY(); - int baseX = x * (HEX_SIDE[zoom] + HEX_SIDE_BY_SIN30[zoom]) + leftMargin + HEX_SIDE[zoom]; - int baseY = (2 * y + 1 + (x % 2)) * HEX_SIDE_BY_COS30[zoom] + topMargin; + int baseX = coordsXToPixel(x); + int baseY = coordsYtoPixel(y, x); - if (EntityVisibilityUtils.onlyDetectedBySensors(client.getLocalPlayer(), entity)) { + if (EntityVisibilityUtils.onlyDetectedBySensors(getLocalPlayer(), entity)) { // This unit is visible only as a sensor Return String sensorReturn = "?"; Font font = new Font(MMConstants.FONT_SANS_SERIF, Font.BOLD, FONT_SIZE[zoom]); @@ -1011,7 +1097,7 @@ private void paintUnit(Graphics g, Entity entity) { g.setColor(Color.RED); g.drawString(sensorReturn, baseX - width, baseY + height); return; - } else if (!EntityVisibilityUtils.detectedOrHasVisual(client.getLocalPlayer(), game, entity)) { + } else if (!EntityVisibilityUtils.detectedOrHasVisual(getLocalPlayer(), game, entity)) { // This unit is not visible, don't draw it return; } @@ -1024,8 +1110,8 @@ private void paintUnit(Graphics g, Entity entity) { // Choose player or team color depending on preferences Color iconColor = entity.getOwner().getColour().getColour(false); if (GUIP.getTeamColoring() && (client != null)) { - boolean isLocalTeam = entity.getOwner().getTeam() == client.getLocalPlayer().getTeam(); - boolean isLocalPlayer = entity.getOwner().equals(client.getLocalPlayer()); + boolean isLocalTeam = (getLocalPlayer() != null) && (entity.getOwner().getTeam() == getLocalPlayer().getTeam()); + boolean isLocalPlayer = entity.getOwner().equals(getLocalPlayer()); if (isLocalPlayer) { iconColor = GUIP.getMyUnitColor(); } else if (isLocalTeam) { @@ -1094,7 +1180,7 @@ private void paintUnit(Graphics g, Entity entity) { FontMetrics currentMetrics = getFontMetrics(font); int stringWidth = currentMetrics.stringWidth(s); GlyphVector gv = font.createGlyphVector(fontContext, s); - g2.fill(gv.getOutline((int) STRAT_CX - stringWidth / 2, + g2.fill(gv.getOutline((int) STRAT_CX - (float) stringWidth / 2, (float) STRAT_SYMBOLSIZE.getHeight() / 3.0f)); } } else if (entity instanceof MekWarrior) { @@ -1125,6 +1211,19 @@ private void paintUnit(Graphics g, Entity entity) { g2.setStroke(new BasicStroke(innerBorderWidth / 2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); g2.draw(form); } + g2.setStroke(new BasicStroke(innerBorderWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + + if (drawFacingArrowsOnMiniMap) { + // draw facing arrow + var facing = entity.getFacing(); + if (facing > -1) { + g2.setColor(Color.BLACK); + g2.rotate(Math.toRadians(facing * 60)); + g2.draw(FACING_ARROW); + g.setColor(iconColor); + g2.fill(FACING_ARROW); + } + } g2.setTransform(saveTransform); @@ -1135,13 +1234,94 @@ private void paintUnit(Graphics g, Entity entity) { int rad = stratOpsSymbols ? 2 * unitSize - 1 : unitSize + unitSize / 2; Color color = GUIP.getUnitSelectedColor(); g2.setColor(color.darker()); - g2.setStroke(new BasicStroke(unitSize / 5 + 1)); + g2.setStroke(new BasicStroke((float) unitSize / 5 + 1)); g2.drawOval(baseX - rad, baseY - rad, rad * 2, rad * 2); } g2.setStroke(saveStroke); } + + /** Draws the symbol for a single entity. Checks visibility in double blind. */ + private void paintSensor(Graphics g, Entity entity) { + if (EntityVisibilityUtils.onlyDetectedBySensors(getLocalPlayer(), entity)) { + // This unit is visible only as a sensor Return, we dont know the range of its sensor yet + return; + } else if (!EntityVisibilityUtils.detectedOrHasVisual(getLocalPlayer(), game, entity)) { + // This unit is not visible, don't draw it + return; + } + + int maxSensorRange = 0; + int minSensorRange = 0; + + if (game.getOptions().booleanOption(OptionsConstants.ADVANCED_TACOPS_SENSORS)) { + int bracket = Compute.getSensorRangeBracket(entity, null, null); + int range = Compute.getSensorRangeByBracket(game, entity, null, null); + + maxSensorRange = bracket * range; + minSensorRange = Math.max((bracket - 1) * range, 0); + if (game.getOptions().booleanOption(OptionsConstants.ADVANCED_INCLUSIVE_SENSOR_RANGE)) { + minSensorRange = 0; + } + } + + + // Choose player or team color depending on preferences + Color iconColor = entity.getOwner().getColour().getColour(false); + if (GUIP.getTeamColoring() && (client != null)) { + boolean isLocalTeam = (getLocalPlayer() != null) && (entity.getOwner().getTeam() == getLocalPlayer().getTeam()); + boolean isLocalPlayer = entity.getOwner().equals(getLocalPlayer()); + if (isLocalPlayer) { + iconColor = GUIP.getMyUnitColor(); + } else if (isLocalTeam) { + iconColor = GUIP.getAllyUnitColor(); + } else { + iconColor = GUIP.getEnemyUnitColor(); + } + } + Graphics2D g2 = (Graphics2D) g; + Stroke saveStroke = g2.getStroke(); + + g2.setStroke(new BasicStroke(2)); + Color iconColorSemiTransparent = new Color(iconColor.getRed(), iconColor.getGreen(), iconColor.getBlue(), 200); + g2.setColor(iconColorSemiTransparent); + + var origin = entity.getPosition(); + for (var sensorRange : List.of(minSensorRange, maxSensorRange)) { + if (sensorRange <= 0) { + continue; + } + + int xo; + int yo; + var sensor = new Path2D.Double(); + + var internalOrExternal = (sensorRange == minSensorRange) && (maxSensorRange != 0) ? -1 : 1; + for (int i = 0; i < 6; i++) { + var movingCoord = origin.translated(i, sensorRange + internalOrExternal); + xo = coordsXToPixel(movingCoord.getX()); + yo = coordsYtoPixel(movingCoord.getY(), movingCoord.getX()); + if (i == 0) { + sensor.moveTo(xo, yo); + } else { + sensor.lineTo(xo, yo); + } + } + sensor.closePath(); + g2.draw(sensor); + } + g2.setStroke(saveStroke); + } + + private int coordsYtoPixel(int y, int x) { + return (2 * y + 1 + (x % 2)) * HEX_SIDE_BY_COS30[zoom] + topMargin; + } + + private int coordsXToPixel(int x) { + return x * (HEX_SIDE[zoom] + HEX_SIDE_BY_SIN30[zoom]) + leftMargin + HEX_SIDE[zoom]; + } + /** Draws the road elements previously assembles into the roadHexes list. */ private void paintRoads(Graphics g) { int exits; @@ -1428,6 +1608,24 @@ private void processMouseRelease(int x, int y, int modifiers) { } } + private void setSensorRangeDisplay(boolean state) { + drawSensorRangeOnMiniMap = state; + GUIP.setDrawSensorRangeOnMiniMap(state); + initializeMap(); + } + + private void setFacingArrowsDisplay(boolean state) { + drawFacingArrowsOnMiniMap = state; + GUIP.setDrawFacingArrowsOnMiniMap(state); + initializeMap(); + } + + private void setPaintBordersDisplay(boolean state) { + paintBorders = state; + GUIP.setPaintBorders(state); + initializeMap(); + } + private void setSymbolsDisplay(int i) { symbolsDisplayMode = i; GUIP.setMiniMapSymbolsDisplayMode(i); @@ -1472,60 +1670,6 @@ public void boardChangedHex(BoardEvent b) { } }; - private final GameListener gameListener = new GameListenerAdapter() { - @Override - public void gamePhaseChange(GamePhaseChangeEvent e) { - if (GUIP.getGameSummaryMinimap() - && (e.getOldPhase().isDeployment() || e.getOldPhase().isMovement() - || e.getOldPhase().isTargeting() || e.getOldPhase().isPremovement() - || e.getOldPhase().isPrefiring() || e.getOldPhase().isFiring() - || e.getOldPhase().isPhysical())) { - - File dir = new File(Configuration.gameSummaryImagesMMDir(), game.getUUIDString()); - if (!dir.exists()) { - dir.mkdirs(); - } - File imgFile = new File(dir, "round_" + game.getRoundCount() + "_" + e.getOldPhase().ordinal() + "_" - + e.getOldPhase() + ".png"); - try { - ImageIO.write(getMinimapImage(game, bv, GAME_SUMMARY_ZOOM, null), "png", imgFile); - } catch (Exception ex) { - logger.error(ex, ""); - } - } - refreshMap(); - } - - @Override - public void gameTurnChange(GameTurnChangeEvent e) { - refreshMap(); - } - - @Override - public void gameBoardNew(GameBoardNewEvent e) { - Board b = e.getOldBoard(); - if (b != null) { - b.removeBoardListener(boardListener); - } - b = e.getNewBoard(); - if (b != null) { - b.addBoardListener(boardListener); - } - board = b; - initializeMap(); - } - - @Override - public void gameBoardChanged(GameBoardChangeEvent e) { - refreshMap(); - } - - @Override - public void gameNewAction(GameNewActionEvent e) { - refreshMap(); - } - }; - BoardViewListener boardViewListener = new BoardViewListenerAdapter() { @Override public void hexCursor(BoardViewEvent b) { @@ -1590,6 +1734,8 @@ public void actionPerformed(ActionEvent e) { } }; + + MouseListener mouseListener = new MouseAdapter() { @Override public void mouseClicked(MouseEvent me) { @@ -1624,31 +1770,56 @@ private void showPopup(MouseEvent me) { String msg_zoomout = Messages.getString("Minimap.menu.ZoomOut"); zoomMenu.add(menuItem(msg_zoomout, ACTION_ZOOM_OUT, zoom != MIM_ZOOM, listener, false)); popup.add(zoomMenu); + String msg_showheight = Messages.getString("Minimap.menu.ShowHeight"); JMenu heightMenu = new JMenu(msg_showheight); + String msg_showheightnone = Messages.getString("Minimap.menu.ShowHeightNone"); - heightMenu.add(menuItem(msg_showheightnone, ACTION_HEIGHT_NONE, zoom >= MIM_ZOOM_FOR_HEIGHT, listener, - heightDisplayMode == SHOW_NO_HEIGHT)); + heightMenu.add(menuItem(msg_showheightnone, ACTION_HEIGHT_NONE, zoom >= MIM_ZOOM_FOR_HEIGHT, listener, heightDisplayMode == SHOW_NO_HEIGHT)); + String msg_showheightground = Messages.getString("Minimap.menu.ShowHeightGround"); - heightMenu.add(menuItem(msg_showheightground, ACTION_HEIGHT_GROUND, zoom >= MIM_ZOOM_FOR_HEIGHT, listener, - heightDisplayMode == SHOW_GROUND_HEIGHT)); + heightMenu.add(menuItem(msg_showheightground, ACTION_HEIGHT_GROUND, zoom >= MIM_ZOOM_FOR_HEIGHT, listener, heightDisplayMode == SHOW_GROUND_HEIGHT)); + String msg_showheightbuilding = Messages.getString("Minimap.menu.ShowHeightBuilding"); - heightMenu.add(menuItem(msg_showheightbuilding, ACTION_HEIGHT_BUILDING, zoom >= MIM_ZOOM_FOR_HEIGHT, - listener, heightDisplayMode == SHOW_BUILDING_HEIGHT)); + heightMenu.add(menuItem(msg_showheightbuilding, ACTION_HEIGHT_BUILDING, zoom >= MIM_ZOOM_FOR_HEIGHT, listener, heightDisplayMode == SHOW_BUILDING_HEIGHT)); + String msg_showheighttotal = Messages.getString("Minimap.menu.ShowHeightTotal"); - heightMenu.add(menuItem(msg_showheighttotal, ACTION_HEIGHT_TOTAL, zoom >= MIM_ZOOM_FOR_HEIGHT, listener, - heightDisplayMode == SHOW_TOTAL_HEIGHT)); + heightMenu.add(menuItem(msg_showheighttotal, ACTION_HEIGHT_TOTAL, zoom >= MIM_ZOOM_FOR_HEIGHT, listener, heightDisplayMode == SHOW_TOTAL_HEIGHT)); + popup.add(heightMenu); - String msg_showsymbols = Messages.getString("Minimap.menu.ShowSymbols"); - JMenu symbolsMenu = new JMenu(msg_showsymbols); - String msg_showsymbolsnosymbols = Messages.getString("Minimap.menu.ShowSymbolsNoSymbols"); - symbolsMenu.add(menuItem(msg_showsymbolsnosymbols, ACTION_SYMBOLS_NO, true, listener, - symbolsDisplayMode == SHOW_NO_SYMBOLS)); - String msg_showsymbolssymbols = Messages.getString("Minimap.menu.ShowSymbolsSymbols"); - symbolsMenu.add(menuItem(msg_showsymbolssymbols, ACTION_SYMBOLS_SHOW, true, listener, - symbolsDisplayMode == SHOW_SYMBOLS)); - popup.add(symbolsMenu); + String lblShowSymbols = Messages.getString("Minimap.menu.ShowSymbols"); + JMenu symbolsMenu = new JMenu(lblShowSymbols); + + String lblShowSymbolsNoSymbols = Messages.getString("Minimap.menu.ShowSymbolsNoSymbols"); + symbolsMenu.add(menuItem(lblShowSymbolsNoSymbols, ACTION_SYMBOLS_NO, true, listener, symbolsDisplayMode == SHOW_NO_SYMBOLS)); + + String lblShowSymbolsSymbols = Messages.getString("Minimap.menu.ShowSymbolsSymbols"); + symbolsMenu.add(menuItem(lblShowSymbolsSymbols, ACTION_SYMBOLS_SHOW, true, listener, symbolsDisplayMode == SHOW_SYMBOLS));; + + JCheckBoxMenuItem toggleDrawSensor = new JCheckBoxMenuItem(Messages.getString("Minimap.menu.ToggleShowSensorRange")); + toggleDrawSensor.addActionListener(l -> { + setSensorRangeDisplay(!drawSensorRangeOnMiniMap); + }); + toggleDrawSensor.setSelected(drawSensorRangeOnMiniMap); + symbolsMenu.add(toggleDrawSensor); + + + JCheckBoxMenuItem toggleDrawFacing = new JCheckBoxMenuItem(Messages.getString("Minimap.menu.ToggleDrawFacingArrows")); + toggleDrawFacing.addActionListener(l -> { + setFacingArrowsDisplay(!drawFacingArrowsOnMiniMap); + }); + toggleDrawFacing.setSelected(drawFacingArrowsOnMiniMap); + symbolsMenu.add(toggleDrawFacing); + + JCheckBoxMenuItem togglePaintBorders = new JCheckBoxMenuItem(Messages.getString("Minimap.menu.ToggleDrawHexBorder")); + togglePaintBorders.addActionListener(l -> { + setPaintBordersDisplay(!paintBorders); + }); + togglePaintBorders.setSelected(drawFacingArrowsOnMiniMap); + symbolsMenu.add(togglePaintBorders); + + popup.add(symbolsMenu); popup.show(me.getComponent(), me.getX(), me.getY()); } diff --git a/megamek/src/megamek/client/ui/swing/minimap/MinimapUnitSymbols.java b/megamek/src/megamek/client/ui/swing/minimap/MinimapUnitSymbols.java index 9d92670d62b..92a1a280d91 100644 --- a/megamek/src/megamek/client/ui/swing/minimap/MinimapUnitSymbols.java +++ b/megamek/src/megamek/client/ui/swing/minimap/MinimapUnitSymbols.java @@ -49,6 +49,7 @@ public class MinimapUnitSymbols { public static final Path2D STRAT_HOVER; public static final Path2D STRAT_WHEELED; public static final Path2D STRAT_NAVAL; + public static final Path2D FACING_ARROW; public static final Path2D STD_MEK; public static final Path2D STD_TANK; public static final Path2D STD_VTOL; @@ -64,6 +65,13 @@ public class MinimapUnitSymbols { private static final double PIHALF = PI / 2; static { + FACING_ARROW = new Path2D.Double(); + FACING_ARROW.moveTo(0, -130); + FACING_ARROW.lineTo(25, -130); + FACING_ARROW.lineTo(0, -180); + FACING_ARROW.lineTo(-25, -130); + FACING_ARROW.closePath(); + STD_MEK = new Path2D.Double(); STD_MEK.moveTo(-25, 45); STD_MEK.lineTo(25, 45); diff --git a/megamek/src/megamek/client/ui/swing/tileset/EntityImage.java b/megamek/src/megamek/client/ui/swing/tileset/EntityImage.java index b15efcda65f..75ae1e06180 100644 --- a/megamek/src/megamek/client/ui/swing/tileset/EntityImage.java +++ b/megamek/src/megamek/client/ui/swing/tileset/EntityImage.java @@ -162,48 +162,54 @@ public class EntityImage { private final boolean isTank; private final int unitHeight; private final int unitElevation; + private final boolean withShadows; public static EntityImage createIcon(Image base, Camouflage camouflage, Entity entity) { - return createIcon(base, null, camouflage, entity, -1, true); + return createIcon(base, null, camouflage, entity, -1, true, true); + } + + public static EntityImage createIcon(Image base, Camouflage camouflage, Entity entity, boolean withShadows) { + return createIcon(base, null, camouflage, entity, -1, true, withShadows); } public static EntityImage createLobbyIcon(Image base, Camouflage camouflage, Entity entity) { - return createIcon(base, null, camouflage, entity, -1, true); + return createIcon(base, null, camouflage, entity, -1, true, true); } public static EntityImage createIcon(Image base, Image wreck, Camouflage camouflage, Entity entity, int secondaryPos) { - return createIcon(base, wreck, camouflage, entity, secondaryPos, false); + return createIcon(base, wreck, camouflage, entity, secondaryPos, false, true); } public static EntityImage createIcon(Image base, Image wreck, Camouflage camouflage, - Entity entity, int secondaryPos, boolean preview) { + Entity entity, int secondaryPos, boolean preview, boolean withShadows) { if (entity instanceof FighterSquadron) { - return new FighterSquadronIcon(base, wreck, camouflage, entity, secondaryPos, preview); + return new FighterSquadronIcon(base, wreck, camouflage, entity, secondaryPos, preview, withShadows); } else { - return new EntityImage(base, wreck, camouflage, entity, secondaryPos, preview); + return new EntityImage(base, wreck, camouflage, entity, secondaryPos, preview, withShadows); } } public EntityImage(Image base, Camouflage camouflage, Component comp, Entity entity) { - this(base, null, camouflage, entity, -1, true); + this(base, null, camouflage, entity, -1, true, true); } public EntityImage(Image base, Image wreck, Camouflage camouflage, Component comp, Entity entity, int secondaryPos) { - this(base, wreck, camouflage, entity, secondaryPos, false); + this(base, wreck, camouflage, entity, secondaryPos, false, true); } public EntityImage(Image base, Image wreck, Camouflage camouflage, - Entity entity, int secondaryPos, boolean preview) { - this(base, wreck, camouflage, null, entity, secondaryPos, preview); + Entity entity, int secondaryPos, boolean preview, boolean withShadows) { + this(base, wreck, camouflage, null, entity, secondaryPos, preview, withShadows); } public EntityImage(Image base, Image wreck, Camouflage camouflage, Component comp, - Entity entity, int secondaryPos, boolean preview) { + Entity entity, int secondaryPos, boolean preview, boolean withShadows) { this.base = base; setCamouflage(camouflage); this.wreck = wreck; + this.withShadows = withShadows; this.dmgLevel = calculateDamageLevel(entity); // hack: gun emplacements are pretty beefy but have weight 0 this.weight = entity instanceof GunEmplacement ? SMOKE_THREE + 1 : entity.getWeight(); @@ -286,7 +292,7 @@ public void loadFacings() { // Generate rotated images for the unit and for a wreck fImage = rotateImage(fImage, i); - if (GUIP.getShadowMap() && isSingleHex) { + if (GUIP.getShadowMap() && isSingleHex && withShadows) { facings[i] = applyDropShadow(fImage); } else { facings[i] = fImage; diff --git a/megamek/src/megamek/client/ui/swing/tileset/FighterSquadronIcon.java b/megamek/src/megamek/client/ui/swing/tileset/FighterSquadronIcon.java index 218fac33372..d5d131604d0 100644 --- a/megamek/src/megamek/client/ui/swing/tileset/FighterSquadronIcon.java +++ b/megamek/src/megamek/client/ui/swing/tileset/FighterSquadronIcon.java @@ -107,8 +107,8 @@ public class FighterSquadronIcon extends EntityImage { */ private final int positionHash; - public FighterSquadronIcon(Image base, Image wreck, Camouflage camouflage, Entity entity, int secondaryPos, boolean preview) { - super(base, wreck, camouflage, entity, secondaryPos, preview); + public FighterSquadronIcon(Image base, Image wreck, Camouflage camouflage, Entity entity, int secondaryPos, boolean preview, boolean withShadows) { + super(base, wreck, camouflage, entity, secondaryPos, preview, withShadows); if (entity instanceof FighterSquadron) { squadron = (FighterSquadron) entity; } else { diff --git a/megamek/src/megamek/client/ui/swing/tileset/TilesetManager.java b/megamek/src/megamek/client/ui/swing/tileset/TilesetManager.java index 81fd4885726..9b7ec8466f9 100644 --- a/megamek/src/megamek/client/ui/swing/tileset/TilesetManager.java +++ b/megamek/src/megamek/client/ui/swing/tileset/TilesetManager.java @@ -540,8 +540,15 @@ public void clearHex(Hex hex) { * Loads a preview image of the unit into the BufferedPanel. */ public Image loadPreviewImage(Entity entity, Camouflage camouflage) { + return loadPreviewImage(entity, camouflage, true); + } + + /** + * Loads a preview image of the unit into the BufferedPanel. + */ + public Image loadPreviewImage(Entity entity, Camouflage camouflage, boolean withShadows) { Image base = MMStaticDirectoryManager.getMekTileset().imageFor(entity); - EntityImage entityImage = EntityImage.createIcon(base, camouflage, entity); + EntityImage entityImage = EntityImage.createIcon(base, camouflage, entity, withShadows); entityImage.loadFacings(); return entityImage.getFacing(entity.getFacing()); } diff --git a/megamek/src/megamek/client/ui/swing/unitDisplay/ArmorPanel.java b/megamek/src/megamek/client/ui/swing/unitDisplay/ArmorPanel.java index 3ba03b6021a..af455145c08 100644 --- a/megamek/src/megamek/client/ui/swing/unitDisplay/ArmorPanel.java +++ b/megamek/src/megamek/client/ui/swing/unitDisplay/ArmorPanel.java @@ -242,7 +242,7 @@ public void displayMek(Entity en) { } if (ams == null) { - logger.error("The armor panel is null"); + logger.info("The armor panel is null"); return; } ams.setEntity(en); diff --git a/megamek/src/megamek/client/ui/swing/util/UIUtil.java b/megamek/src/megamek/client/ui/swing/util/UIUtil.java index 250e48758c6..a129e4fe55f 100644 --- a/megamek/src/megamek/client/ui/swing/util/UIUtil.java +++ b/megamek/src/megamek/client/ui/swing/util/UIUtil.java @@ -1113,7 +1113,7 @@ public void setToolTipText(String text) { * its width and adding HTML tags. */ public static String formatSideTooltip(String text) { - return "

" + text; + return "

" + text + ""; } /** diff --git a/megamek/src/megamek/common/EntityVisibilityUtils.java b/megamek/src/megamek/common/EntityVisibilityUtils.java index dfc6edc896a..f709a8af778 100644 --- a/megamek/src/megamek/common/EntityVisibilityUtils.java +++ b/megamek/src/megamek/common/EntityVisibilityUtils.java @@ -18,6 +18,7 @@ */ package megamek.common; +import megamek.common.annotations.Nullable; import megamek.common.options.OptionsConstants; /** @@ -62,21 +63,32 @@ public static boolean detectedOrHasVisual(Player localPlayer, Game game, Entity * * @return */ - public static boolean onlyDetectedBySensors(Player localPlayer, Entity entity) { - boolean sensors = (entity.getGame().getOptions().booleanOption( - OptionsConstants.ADVANCED_TACOPS_SENSORS) - || entity.getGame().getOptions() - .booleanOption(OptionsConstants.ADVAERORULES_STRATOPS_ADVANCED_SENSORS)); - boolean sensorsDetectAll = entity.getGame().getOptions().booleanOption( - OptionsConstants.ADVANCED_SENSORS_DETECT_ALL); - boolean doubleBlind = entity.getGame().getOptions().booleanOption( - OptionsConstants.ADVANCED_DOUBLE_BLIND); + public static boolean onlyDetectedBySensors(@Nullable Player localPlayer, Entity entity) { + boolean usesAdvancedTacOpsSensors = entity.getGame().getOptions().booleanOption(OptionsConstants.ADVANCED_TACOPS_SENSORS); + boolean usesAdvancedStratOpsSensors = entity.getGame().getOptions().booleanOption(OptionsConstants.ADVAERORULES_STRATOPS_ADVANCED_SENSORS); + + boolean usesSensors = usesAdvancedTacOpsSensors || usesAdvancedStratOpsSensors; + + boolean sensorsDetectAll = entity.getGame().getOptions().booleanOption(OptionsConstants.ADVANCED_SENSORS_DETECT_ALL); + boolean doubleBlind = entity.getGame().getOptions().booleanOption(OptionsConstants.ADVANCED_DOUBLE_BLIND); + + if (!doubleBlind) { + return false; + } + + if (localPlayer == null) { + return true; + } + boolean hasVisual = entity.hasSeenEntity(localPlayer); boolean hasDetected = entity.hasDetectedEntity(localPlayer); - - if (sensors && doubleBlind && !sensorsDetectAll - && !EntityVisibilityUtils.trackThisEntitiesVisibilityInfo(localPlayer, entity) - && hasDetected && !hasVisual) { + boolean doesNotTrackThisEntitiesVisibilityInfo = !EntityVisibilityUtils.trackThisEntitiesVisibilityInfo(localPlayer, entity); + if (usesSensors + && !sensorsDetectAll + && doesNotTrackThisEntitiesVisibilityInfo + && hasDetected + && !hasVisual) + { return true; } else { return false; diff --git a/megamek/src/megamek/common/Hex.java b/megamek/src/megamek/common/Hex.java index 455968f4871..368c03c2c55 100644 --- a/megamek/src/megamek/common/Hex.java +++ b/megamek/src/megamek/common/Hex.java @@ -121,6 +121,13 @@ public int[] getTerrainTypes() { return terrains.keySet().stream().mapToInt(Integer::intValue).toArray(); } + /** + * @return A HashSet that contains an id for each terrain present in this hex. + */ + public Set getTerrainTypesSet() { + return new HashSet<>(terrains.keySet()); + } + /** * Resets the theme to what was specified in the board file. */ diff --git a/megamek/src/megamek/common/MiscType.java b/megamek/src/megamek/common/MiscType.java index 338940e017a..bf867b1ad93 100644 --- a/megamek/src/megamek/common/MiscType.java +++ b/megamek/src/megamek/common/MiscType.java @@ -4317,10 +4317,9 @@ public static MiscType createISDroneCarrierControlSystem() { misc.criticals = 1; misc.tankslots = 1; misc.flags = misc.flags.or(F_DRONE_CARRIER_CONTROL).or(F_VARIABLE_SIZE) - .or(F_TANK_EQUIPMENT).or(F_FIGHTER_EQUIPMENT) - .or(F_FIGHTER_EQUIPMENT).or(F_DS_EQUIPMENT).or(F_JS_EQUIPMENT).or(F_SS_EQUIPMENT) - .or(F_SUPPORT_TANK_EQUIPMENT); - misc.rulesRefs = "305, TO"; + .or(F_SC_EQUIPMENT).or(F_DS_EQUIPMENT).or(F_JS_EQUIPMENT).or(F_SS_EQUIPMENT).or(F_WS_EQUIPMENT) + .or(F_SUPPORT_TANK_EQUIPMENT).or(F_TANK_EQUIPMENT); + misc.rulesRefs = "117, TO:AUE"; misc.techAdvancement.setTechBase(TECH_BASE_ALL).setIntroLevel(false).setUnofficial(false) .setTechRating(RATING_C).setAvailability(RATING_E, RATING_F, RATING_F, RATING_E) .setISAdvancement(DATE_ES, DATE_ES, DATE_NONE, DATE_NONE, DATE_NONE) @@ -4345,9 +4344,8 @@ public static MiscType createISDroneOperatingSystem() { misc.tankslots = 1; misc.omniFixedOnly = true; misc.flags = misc.flags.or(F_DRONE_OPERATING_SYSTEM).or(F_MEK_EQUIPMENT).or(F_TANK_EQUIPMENT) - .or(F_FIGHTER_EQUIPMENT).or(F_FIGHTER_EQUIPMENT).or(F_DS_EQUIPMENT) - .or(F_JS_EQUIPMENT).or(F_SS_EQUIPMENT).or(F_SUPPORT_TANK_EQUIPMENT); - misc.rulesRefs = "306, TO"; + .or(F_FIGHTER_EQUIPMENT).or(F_SUPPORT_TANK_EQUIPMENT); + misc.rulesRefs = "118, TO:AUE"; misc.techAdvancement.setTechBase(TECH_BASE_ALL).setIntroLevel(false).setUnofficial(false) .setTechRating(RATING_C).setAvailability(RATING_E, RATING_F, RATING_F, RATING_E) .setISAdvancement(DATE_ES, DATE_ES, DATE_NONE, DATE_NONE, DATE_NONE) diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 7154f0644a6..1cbbbed78be 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -13,12 +13,7 @@ */ package megamek.common.actions; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Vector; +import java.util.*; import megamek.MMConstants; import megamek.client.Client; @@ -144,6 +139,33 @@ public WeaponAttackAction(int entityId, int targetType, int targetId, int weapon this.bombPayloads.put("external", new int[BombType.B_NUM]); } + // Copy constructor, hopefully with enough info to generate same to-hit data. + public WeaponAttackAction(final WeaponAttackAction other) { + this(other.getEntityId(), other.getTargetType(), other.getTargetId(), other.getWeaponId()); + + aimedLocation = other.aimedLocation; + aimMode = other.aimMode; + ammoCarrier = other.ammoCarrier; + ammoId = other.ammoId; + ammoMunitionType = other.ammoMunitionType; + isHomingShot = other.isHomingShot; + isPointblankShot = other.isPointblankShot; + isStrafing = other.isStrafing; + isStrafingFirstShot = other.isStrafingFirstShot; + launchVelocity = other.launchVelocity; + nemesisConfused = other.nemesisConfused; + oldTargetId = other.oldTargetId; + oldTargetType = other.oldTargetType; + originalTargetId = other.originalTargetId; + originalTargetType = other.originalTargetType; + otherAttackInfo = other.otherAttackInfo; + swarmingMissiles = other.swarmingMissiles; + swarmMissiles = other.swarmMissiles; + weaponId = other.weaponId; + this.bombPayloads.put("internal", Arrays.copyOf(other.bombPayloads.get("internal"), BombType.B_NUM)); + this.bombPayloads.put("external", Arrays.copyOf(other.bombPayloads.get("external"), BombType.B_NUM)); + } + public int getWeaponId() { return weaponId; } @@ -1380,7 +1402,9 @@ private static String toHitIsImpossible(Game game, Entity ae, int attackerId, Ta // c3 units can fire if any other unit in their network is in // visual or sensor range for (Entity en : game.getEntitiesVector()) { - if (!en.isEnemyOf(ae) && en.onSameC3NetworkAs(ae) && Compute.canSee(game, en, target)) { + // We got here because ae can't see the target, so we need a C3 buddy that _can_ + // or there's no shot. + if (!en.isEnemyOf(ae) && en.onSameC3NetworkAs(ae) && Compute.canSee(game, en, target, false, null, null)) { networkSee = true; break; } diff --git a/megamek/src/megamek/common/preference/ClientPreferences.java b/megamek/src/megamek/common/preference/ClientPreferences.java index 044e78b2842..c45f7480f9c 100644 --- a/megamek/src/megamek/common/preference/ClientPreferences.java +++ b/megamek/src/megamek/common/preference/ClientPreferences.java @@ -361,6 +361,9 @@ public File getStrategicViewTheme() { } public void setMinimapTheme(String theme) { + if (theme == null) { + return; + } store.setValue(MINIMAP_THEME, theme); } diff --git a/megamek/src/megamek/server/totalwarfare/TWGameManager.java b/megamek/src/megamek/server/totalwarfare/TWGameManager.java index 9f50524a7d5..91564433635 100644 --- a/megamek/src/megamek/server/totalwarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalwarfare/TWGameManager.java @@ -3938,6 +3938,50 @@ private void receiveMovement(Packet packet, int connId) { endCurrentTurn(entity); } + /** + * + * @param skidder the unit which should skid + * @param curPos The Coords where displacement may take place due to stacking violations + * @param direction the direction of the skid + * @return + */ + private Vector processSkidDisplacement(Entity skidder, Coords curPos, int direction) { + Coords nextPos; + Report r; + Vector skidDisplacementReports = new Vector<>(); + + // If the skidding entity violates stacking, + // displace targets until it doesn't. + // Set climb mode off for skid calcs A) in buildings, B) by non-flying vehicles? + Entity target = Compute.stackingViolation(game, skidder.getId(), curPos, false); + while (target != null) { + nextPos = Compute.getValidDisplacement(game, target.getId(), target.getPosition(), direction); + // ASSUMPTION + // There should always be *somewhere* that + // the target can go... last skid hex if + // nothing else is available. + if (null == nextPos) { + // But I don't trust the assumption fully. + // Report the error and try to continue. + logger.error("The skid of " + skidder.getShortName() + + " should displace " + target.getShortName() + + " in hex " + curPos.getBoardNum() + + " but there is nowhere to go."); + break; + } + // indent displacement + r = new Report(1210, Report.PUBLIC); + r.indent(); + r.newlines = 0; + skidDisplacementReports.add(r); + skidDisplacementReports.addAll(doEntityDisplacement(target, curPos, nextPos, null)); + skidDisplacementReports.addAll(doEntityDisplacementMinefieldCheck(skidder, curPos, nextPos, skidder.getElevation())); + target = Compute.stackingViolation(game, skidder.getId(), curPos, skidder.climbMode()); + } + + return skidDisplacementReports; + } + /** * makes a unit skid or sideslip on the board * @@ -3950,8 +3994,8 @@ private void receiveMovement(Packet packet, int connId) { * @return true if the entity was removed from play */ boolean processSkid(Entity entity, Coords start, int elevation, - int direction, int distance, MoveStep step, - EntityMovementType moveType) { + int direction, int distance, MoveStep step, + EntityMovementType moveType) { return processSkid(entity, start, elevation, direction, distance, step, moveType, false); } @@ -4557,6 +4601,10 @@ else if ((target instanceof Infantry) && (bldg != null)) { // and add it to the list of affected buildings. if (bldg.getCurrentCF(nextPos) > 0) { stopTheSkid = true; + + // Before doing the basement check, displace any other entities in the building at the same level. + addReport(processSkidDisplacement(entity, entity.getPosition(), direction)); + if (bldg.rollBasement(nextPos, game.getBoard(), mainPhaseReport)) { sendChangedHex(nextPos); Vector buildings = new Vector<>(); @@ -4687,34 +4735,8 @@ else if ((target instanceof Infantry) && (bldg != null)) { } // Handle the next skid hex. - // If the skidding entity violates stacking, - // displace targets until it doesn't. - curPos = entity.getPosition(); - Entity target = Compute.stackingViolation(game, entity.getId(), curPos, entity.climbMode()); - while (target != null) { - nextPos = Compute.getValidDisplacement(game, target.getId(), target.getPosition(), direction); - // ASSUMPTION - // There should always be *somewhere* that - // the target can go... last skid hex if - // nothing else is available. - if (null == nextPos) { - // But I don't trust the assumption fully. - // Report the error and try to continue. - logger.error("The skid of " + entity.getShortName() - + " should displace " + target.getShortName() - + " in hex " + curPos.getBoardNum() - + " but there is nowhere to go."); - break; - } - // indent displacement - r = new Report(1210, Report.PUBLIC); - r.indent(); - r.newlines = 0; - addReport(r); - addReport(doEntityDisplacement(target, curPos, nextPos, null)); - addReport(doEntityDisplacementMinefieldCheck(entity, curPos, nextPos, entity.getElevation())); - target = Compute.stackingViolation(game, entity.getId(), curPos, entity.climbMode()); - } + // Handle additional displacements + addReport(processSkidDisplacement(entity, entity.getPosition(), direction)); // Meks suffer damage for every hex skidded. // For QuadVees in vehicle mode, apply diff --git a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java index 452ee0ab539..ce1aaea07d9 100644 --- a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java +++ b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java @@ -36,13 +36,7 @@ import java.text.DecimalFormat; import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.Vector; +import java.util.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -157,7 +151,7 @@ void testGetMovePathSuccessProbability() { final BasicPathRanker testRanker = spy(new BasicPathRanker(mockPrincess)); doReturn(testRollList).when(testRanker).getPSRList(eq(mockPath)); - double actual = testRanker.getMovePathSuccessProbability(mockPath, new StringBuilder()); + double actual = testRanker.getMovePathSuccessProbability(mockPath); assertEquals(0.346, actual, TOLERANCE); } @@ -176,7 +170,7 @@ void testGetMovePathSuccessProbabilityWithMASC() { final BasicPathRanker testRanker = spy(new BasicPathRanker(mockPrincess)); doReturn(testRollList).when(testRanker).getPSRList(eq(mockPath)); - double actual = testRanker.getMovePathSuccessProbability(mockPath, new StringBuilder()); + double actual = testRanker.getMovePathSuccessProbability(mockPath); assertEquals(0.346, actual, TOLERANCE); } @@ -408,7 +402,7 @@ void testRankPath() { final BasicPathRanker testRanker = spy(new BasicPathRanker(mockPrincess)); doReturn(1.0) .when(testRanker) - .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); + .getMovePathSuccessProbability(any(MovePath.class)); doReturn(5) .when(testRanker) .distanceToClosestEdge(any(Coords.class), any(Game.class)); @@ -521,11 +515,11 @@ void testRankPath() { + " * " + 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(40) + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " - + LOG_DECIMAL.format(1) + "] + movementMod [0] - facingMod [" + + LOG_DECIMAL.format(1) + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); @@ -535,17 +529,17 @@ void testRankPath() { // Change the move path success probability. doReturn(0.5) .when(testRanker) - .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); + .getMovePathSuccessProbability(any(MovePath.class)); expected = new RankedPath(-318.125, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(250) + " = " + LOG_DECIMAL.format(0.5) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-23.12) + " = " + LOG_PERCENT.format(0.5) + " * ((" + LOG_DECIMAL.format(22.5) + " * " + LOG_DECIMAL.format(1.5) - + ") - " + LOG_DECIMAL.format(40) + "] - aggressionMod [" + + ") - " + 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) + " * " - + LOG_DECIMAL.format(1) + "] + movementMod [0] - facingMod [" + + LOG_DECIMAL.format(1) + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); @@ -556,17 +550,17 @@ void testRankPath() { } doReturn(0.75) .when(testRanker) - .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); + .getMovePathSuccessProbability(any(MovePath.class)); expected = new RankedPath(-184.6875, mockPath, "Calculation: {fall mod [" + LOG_DECIMAL.format(125) + " = " + LOG_DECIMAL.format(0.25) + " * " + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-14.69) + " = " + LOG_PERCENT.format(0.75) + " * ((" + LOG_DECIMAL.format(22.5) + " * " + LOG_DECIMAL.format(1.5) - + ") - " + LOG_DECIMAL.format(40) + "] - aggressionMod [" + + ") - " + 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) + " * " - + LOG_DECIMAL.format(1) + "] + movementMod [0] - facingMod [" + + LOG_DECIMAL.format(1) + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); @@ -577,7 +571,7 @@ void testRankPath() { } doReturn(1.0) .when(testRanker) - .getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class)); + .getMovePathSuccessProbability(any(MovePath.class)); // Change the damage to enemy mek 1. evalForMockEnemyMek = new EntityEvaluationResponse(); @@ -592,11 +586,11 @@ void testRankPath() { + 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(40) + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " - + LOG_DECIMAL.format(1) + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + + LOG_DECIMAL.format(1) + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -616,10 +610,10 @@ void testRankPath() { + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-16) + " = " + LOG_PERCENT.format(1) + " * ((" + LOG_DECIMAL.format(16) + " * " + LOG_DECIMAL.format(1.5) + ") - " + LOG_DECIMAL.format(40) - + "] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " - + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) + "] + movementMod [0] - facingMod [" + + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); @@ -649,11 +643,11 @@ void testRankPath() { + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(-16.25) + " = " + LOG_PERCENT.format(1) + " * ((" + LOG_DECIMAL.format(22.5) + " * " + LOG_DECIMAL.format(1.5) + ") - " + LOG_DECIMAL.format(50) - + "] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -673,11 +667,11 @@ void testRankPath() { + LOG_DECIMAL.format(500) + "] + braveryMod [" + LOG_DECIMAL.format(3.75) + " = " + LOG_PERCENT.format(1) + " * ((" + LOG_DECIMAL.format(22.5) + " * " + LOG_DECIMAL.format(1.5) + ") - " + LOG_DECIMAL.format(30) - + "] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -702,11 +696,11 @@ void testRankPath() { + 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) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -722,11 +716,11 @@ void testRankPath() { + 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(55) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(55) + " = " + LOG_DECIMAL.format(22) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -745,11 +739,11 @@ void testRankPath() { + 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(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(10) + " = " + LOG_DECIMAL.format(10) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -763,11 +757,11 @@ void testRankPath() { + 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(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(20) + " = " + LOG_DECIMAL.format(20) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -780,9 +774,9 @@ void testRankPath() { + 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(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) - + "] - herdingMod [0 no friends] + movementMod [0] - facingMod [" + + "] - herdingMod [0 no friends] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); @@ -798,11 +792,11 @@ void testRankPath() { + 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(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -815,11 +809,11 @@ void testRankPath() { + 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(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -835,10 +829,10 @@ void testRankPath() { + 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(30) + " = " + LOG_DECIMAL.format(12) + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " - + LOG_DECIMAL.format(1) + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + + LOG_DECIMAL.format(1) + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -859,14 +853,16 @@ void testRankPath() { + 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(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(1) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); + System.out.println(expected); + System.out.println(actual); assertRankedPathEquals(expected, actual); if (baseRank != actual.getRank()) { fail("Being 1 hex off facing should make no difference in rank."); @@ -877,11 +873,11 @@ void testRankPath() { + 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(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(50) + " = max(" + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(50) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(2) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -895,11 +891,11 @@ void testRankPath() { + 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(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(100) + " = max(" + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(100) + " = max(" + LOG_INT.format(0) + ", " + "" + LOG_INT.format(50) + " * {" + LOG_INT.format(3) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -918,11 +914,11 @@ void testRankPath() { + 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(30) + " = " + + ")] - aggressionMod [" + LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * " + LOG_DECIMAL.format(2.5) + "] - herdingMod [" + LOG_DECIMAL.format(15) + " = " + LOG_DECIMAL.format(15) + " * " + LOG_DECIMAL.format(1) - + "] + movementMod [0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + + "] + movementMod [0.0] - facingMod [" + LOG_DECIMAL.format(0) + " = max(" + LOG_INT.format(0) + ", " + LOG_INT.format(50) + " * {" + LOG_INT.format(0) + " - " + LOG_INT.format(1) + "})]"); actual = testRanker.rankPath(mockPath, mockGame, 18, 0.5, testEnemies, friendsCoords); @@ -1329,6 +1325,7 @@ void testCheckPathForHazards() { when(mockBA.getCrew()).thenReturn(mockCrew); when(mockBA.getHeatCapacity()).thenReturn(999); when(mockBA.isFireResistant()).thenReturn(true); + when(mockBA.isBattleArmor()).thenReturn(true); when(mockHexThree.getTerrainTypes()).thenReturn(new int[] { Terrains.BUILDING, Terrains.FIRE }); assertEquals(0, testRanker.checkPathForHazards(mockPath, mockBA, mockGame), TOLERANCE); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); @@ -1338,9 +1335,10 @@ void testCheckPathForHazards() { when(mockProto.locations()).thenReturn(6); when(mockProto.getArmor(anyInt())).thenReturn(5); when(mockProto.getCrew()).thenReturn(mockCrew); + when(mockProto.isProtoMek()).thenReturn(true); when(mockProto.getHeatCapacity()).thenReturn(999); when(mockPath.isJumping()).thenReturn(false); - when(mockHexThree.getTerrainTypes()).thenReturn(new int[] { Terrains.MAGMA }); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.MAGMA))); when(mockHexThree.terrainLevel(Terrains.MAGMA)).thenReturn(1); assertEquals(167.0, testRanker.checkPathForHazards(mockPath, mockProto, mockGame), TOLERANCE); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); @@ -1348,7 +1346,7 @@ void testCheckPathForHazards() { // Test waking a ProtoMek through a fire. when(mockPath.isJumping()).thenReturn(false); - when(mockHexThree.getTerrainTypes()).thenReturn(new int[] { Terrains.FIRE, Terrains.WOODS }); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.WOODS, Terrains.FIRE))); assertEquals(50.0, testRanker.checkPathForHazards(mockPath, mockProto, mockGame), TOLERANCE); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); @@ -1357,8 +1355,10 @@ void testCheckPathForHazards() { when(mockInfantry.locations()).thenReturn(2); when(mockInfantry.getArmor(anyInt())).thenReturn(0); when(mockInfantry.getCrew()).thenReturn(mockCrew); + when(mockInfantry.isConventionalInfantry()).thenReturn(true); + when(mockInfantry.isInfantry()).thenReturn(true); when(mockPath.isJumping()).thenReturn(false); - when(mockHexThree.getTerrainTypes()).thenReturn(new int[] { Terrains.ICE, Terrains.WATER }); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.ICE, Terrains.WATER))); when(mockHexThree.depth()).thenReturn(1); assertEquals(1000, testRanker.checkPathForHazards(mockPath, mockInfantry, mockGame), TOLERANCE); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); @@ -1370,13 +1370,13 @@ void testCheckPathForHazards() { when(mockTank.getArmor(anyInt())).thenReturn(10); when(mockTank.getCrew()).thenReturn(mockCrew); when(mockPath.isJumping()).thenReturn(false); - when(mockHexThree.getTerrainTypes()).thenReturn(new int[] { Terrains.BUILDING, Terrains.FIRE }); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.BUILDING, Terrains.FIRE))); assertEquals(26.2859, testRanker.checkPathForHazards(mockPath, mockTank, mockGame), TOLERANCE); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); // Test walking through a building. when(mockPath.isJumping()).thenReturn(false); - when(mockHexThree.getTerrainTypes()).thenReturn(new int[] { Terrains.BUILDING }); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.BUILDING))); assertEquals(1.285, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); @@ -1385,6 +1385,10 @@ void testCheckPathForHazards() { when(mockHexTwo.getTerrainTypes()).thenReturn(new int[] { Terrains.ICE, Terrains.WATER }); when(mockHexThree.getTerrainTypes()).thenReturn(new int[] { Terrains.ICE, Terrains.WATER }); when(mockFinalHex.getTerrainTypes()).thenReturn(new int[] { Terrains.ICE, Terrains.WATER }); + when(mockHexTwo.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.ICE, Terrains.WATER))); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.ICE, Terrains.WATER))); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.ICE, Terrains.WATER))); + when(mockHexTwo.terrainLevel(Terrains.WATER)).thenReturn(0); when(mockHexThree.terrainLevel(Terrains.WATER)).thenReturn(1); when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(2); @@ -1397,9 +1401,9 @@ void testCheckPathForHazards() { when(mockUnit.getArmor(Mek.LOC_RARM)).thenReturn(0); assertEquals(2000, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockUnit.getArmor(Mek.LOC_RARM)).thenReturn(10); - when(mockHexTwo.getTerrainTypes()).thenReturn(new int[0]); - when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[0]); + when(mockHexTwo.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(0))); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(0))); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(0))); when(mockHexTwo.terrainLevel(Terrains.WATER)).thenReturn(0); when(mockHexThree.terrainLevel(Terrains.WATER)).thenReturn(0); when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(0); @@ -1409,16 +1413,16 @@ void testCheckPathForHazards() { // Test walking over 3 hexes of magma crust. when(mockPath.isJumping()).thenReturn(false); - when(mockHexTwo.getTerrainTypes()).thenReturn(new int[] { Terrains.MAGMA }); - when(mockHexThree.getTerrainTypes()).thenReturn(new int[] { Terrains.MAGMA }); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[] { Terrains.MAGMA }); + when(mockHexTwo.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.MAGMA))); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.MAGMA))); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.MAGMA))); when(mockHexTwo.terrainLevel(Terrains.MAGMA)).thenReturn(1); when(mockHexThree.terrainLevel(Terrains.MAGMA)).thenReturn(1); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); 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]); + when(mockHexTwo.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(0))); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(0))); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(0))); when(mockHexTwo.terrainLevel(Terrains.MAGMA)).thenReturn(0); when(mockHexThree.terrainLevel(Terrains.MAGMA)).thenReturn(0); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(0); @@ -1428,24 +1432,25 @@ void testCheckPathForHazards() { // number when(mockPath.isJumping()).thenReturn(false); when(mockFinalStep.isProne()).thenReturn(true); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[] { Terrains.MAGMA }); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.MAGMA))); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); assertEquals(56010.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalStep.isProne()).thenReturn(false); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[0]); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(0))); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(0); // Test walking through 2 hexes of fire. when(mockPath.isJumping()).thenReturn(false); - when(mockHexTwo.getTerrainTypes()).thenReturn(new int[] { Terrains.WOODS, Terrains.FIRE }); - when(mockHexThree.getTerrainTypes()).thenReturn(new int[] { Terrains.WOODS, Terrains.FIRE }); + when(mockHexTwo.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.WOODS, Terrains.FIRE))); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.WOODS, Terrains.FIRE))); assertEquals(4.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); - when(mockHexTwo.getTerrainTypes()).thenReturn(new int[0]); - when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); + when(mockHexTwo.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(0))); + when(mockHexThree.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(0))); // Test jumping. when(mockPath.isJumping()).thenReturn(true); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[] { Terrains.ICE, Terrains.WATER }); + + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.ICE, Terrains.WATER))); when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(2); when(mockFinalHex.depth()).thenReturn(2); when(mockUnit.getArmor(eq(Mek.LOC_LLEG))).thenReturn(0); @@ -1453,15 +1458,17 @@ void testCheckPathForHazards() { when(mockUnit.getArmor(eq(Mek.LOC_LLEG))).thenReturn(10); when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(0); when(mockFinalHex.depth()).thenReturn(0); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[] { Terrains.MAGMA }); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.MAGMA))); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(1); assertEquals(3134.5, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockFinalHex.terrainLevel(Terrains.MAGMA)).thenReturn(2); 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 }); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.WOODS, Terrains.FIRE))); assertEquals(5.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[] { Terrains.WOODS }); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.WOODS))); + + assertEquals(0.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Test a movement type that doesn't worry about ground terrain. @@ -1502,7 +1509,7 @@ void testMagmaHazard() { when(mockUnit.getArmor(eq(Mek.LOC_LLEG))).thenReturn(24); when(mockUnit.getArmor(eq(Mek.LOC_RLEG))).thenReturn(24); when(mockFinalHex.depth()).thenReturn(0); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[] { Terrains.MAGMA }); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(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); @@ -1626,7 +1633,7 @@ void testSwampHazard() { // to total destruction. when(mockPath.isJumping()).thenReturn(true); when(mockFinalHex.depth()).thenReturn(0); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[] { Terrains.SWAMP }); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(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); @@ -1691,6 +1698,7 @@ void testMudHazard() { when(mockUnit.locations()).thenReturn(8); when(mockUnit.getArmor(anyInt())).thenReturn(10); when(mockUnit.getHeight()).thenReturn(2); + when(mockUnit.isMek()).thenReturn(true); final Game mockGame = setupGame(testCoords, testHexes); @@ -1706,7 +1714,7 @@ void testMudHazard() { // down here. Small hazard to Meks due to PSR malus when(mockPath.isJumping()).thenReturn(false); when(mockFinalHex.depth()).thenReturn(0); - when(mockFinalHex.getTerrainTypes()).thenReturn(new int[] { Terrains.MUD }); + when(mockFinalHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.MUD))); assertEquals(2.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); // Test non-hover vehicle hazard @@ -1756,7 +1764,7 @@ void testBlackIceHazard() { when(mockCrew.getPiloting()).thenReturn(5); // Test visible black ice hazard value - when(mockPenultimateHex.getTerrainTypes()).thenReturn(new int[] { Terrains.BLACK_ICE }); + when(mockPenultimateHex.getTerrainTypesSet()).thenReturn(new HashSet<>(Set.of(Terrains.BLACK_ICE))); assertEquals(12.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); } diff --git a/megamek/unittests/megamek/client/bot/princess/FireControlTest.java b/megamek/unittests/megamek/client/bot/princess/FireControlTest.java index a5e78ac4021..122ee25cf5b 100644 --- a/megamek/unittests/megamek/client/bot/princess/FireControlTest.java +++ b/megamek/unittests/megamek/client/bot/princess/FireControlTest.java @@ -51,6 +51,8 @@ import java.util.Set; import java.util.Vector; +import megamek.common.planetaryconditions.Atmosphere; +import megamek.common.planetaryconditions.PlanetaryConditions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -200,6 +202,10 @@ void beforeEach() { when(mockShooter.getHeat()).thenReturn(0); mockShooterState = mock(EntityState.class); mockShooterCoords = new Coords(0, 0); + when(mockShooter.getPosition()).thenReturn(mockShooterCoords); + // internal height values are 0-indexed, so meks are 1, not 2, here + when(mockShooter.getHeight()).thenReturn(1); + when(mockShooter.relHeight()).thenReturn(1); when(mockShooterState.getPosition()).thenReturn(mockShooterCoords); mockShooterMoveMod = new ToHitData(); @@ -234,7 +240,16 @@ void beforeEach() { when(mockGame.getOptions()).thenReturn(mockGameOptions); when(mockGame.getBoard()).thenReturn(mockBoard); + // Base planetary conditions + PlanetaryConditions planetaryConditions = new PlanetaryConditions(); + planetaryConditions.setAtmosphere(Atmosphere.STANDARD); + when(mockGame.getPlanetaryConditions()).thenReturn(planetaryConditions); + mockTarget = mock(BipedMek.class); + when(mockTarget.getPosition()).thenReturn(mockTargetCoords); + // internal height values are 0-indexed, so meks are 1, not 2, here + when(mockTarget.getHeight()).thenReturn(1); + when(mockTarget.relHeight()).thenReturn(1); when(mockTarget.getDisplayName()).thenReturn("mock target"); when(mockTarget.getId()).thenReturn(MOCK_TARGET_ID); when(mockTarget.isMilitary()).thenReturn(true); @@ -521,6 +536,7 @@ void beforeEach() { shooterWeapons.add(mockPPC); mockPPCFireInfo = mock(WeaponFireInfo.class); when(mockPPCFireInfo.getProbabilityToHit()).thenReturn(0.5); + when(mockPPCFireInfo.getExpectedDamage()).thenReturn(5.0); doReturn(mockPPCFireInfo).when(testFireControl).buildWeaponFireInfo(any(Entity.class), any(EntityState.class), any(Targetable.class), any(EntityState.class), eq(mockPPC), isNull(), @@ -556,6 +572,7 @@ void beforeEach() { shooterWeapons.add(mockLRM5); mockLRMFireInfo = mock(WeaponFireInfo.class); when(mockLRMFireInfo.getProbabilityToHit()).thenReturn(0.6); + when(mockLRMFireInfo.getExpectedDamage()).thenReturn(2.0); doReturn(mockLRMFireInfo).when(testFireControl).buildWeaponFireInfo(any(Entity.class), any(EntityState.class), any(Targetable.class), any(EntityState.class), eq(mockLRM5), any(AmmoMounted.class), @@ -1064,6 +1081,7 @@ void testGuessToHitModifierHelperForAnyAttack() { .thenReturn(false); when(mockHex.terrainLevel(Terrains.WOODS)).thenReturn(Terrain.LEVEL_NONE); when(mockHex.terrainLevel(Terrains.JUNGLE)).thenReturn(Terrain.LEVEL_NONE); + when(mockHex.terrainLevel(Terrains.SMOKE)).thenReturn(Terrain.LEVEL_NONE); when(mockPrincess.getMaxWeaponRange(any(Entity.class), anyBoolean())).thenReturn(21); ToHitData expected = new ToHitData(); assertToHitDataEquals(expected, testFireControl.guessToHitModifierHelperForAnyAttack( @@ -1228,17 +1246,47 @@ void testGuessToHitModifierHelperForAnyAttack() { when(mockTargetState.getMovementType()).thenReturn(EntityMovementType.MOVE_NONE); // Stand the target in light woods. + // 1. change coords to adjacent + mockShooterCoords = new Coords(0, 0); + when(mockShooter.getPosition()).thenReturn(mockShooterCoords); + mockTargetCoords = new Coords(1, 0); + when(mockTarget.getPosition()).thenReturn(mockTargetCoords); + + // 2. change terrain info when(mockHex.terrainLevel(Terrains.WOODS)).thenReturn(1); + when(mockHex.terrainLevel(Terrains.FOLIAGE_ELEV)).thenReturn(2); + expected = new ToHitData(); expected.addModifier(1, FireControl.TH_WOODS); assertToHitDataEquals(expected, testFireControl.guessToHitModifierHelperForAnyAttack(mockShooter, mockShooterState, mockTarget, mockTargetState, - 10, + 1, mockGame)); + + // Stand the target farther away in light woods + mockShooterCoords = new Coords(0, 0); + when(mockShooter.getPosition()).thenReturn(mockShooterCoords); + mockTargetCoords = new Coords(0, 2); + when(mockTarget.getPosition()).thenReturn(mockTargetCoords); + + expected = new ToHitData(); + expected.addModifier(2, FireControl.TH_WOODS); + assertToHitDataEquals(expected, testFireControl.guessToHitModifierHelperForAnyAttack(mockShooter, + mockShooterState, + mockTarget, + mockTargetState, + 2, + mockGame)); when(mockHex.terrainLevel(Terrains.WOODS)).thenReturn(Terrain.LEVEL_NONE); + // Revert positions and foliage + mockShooterCoords = new Coords(0, 0); + when(mockShooter.getPosition()).thenReturn(mockShooterCoords); + mockTargetCoords = new Coords(1, 0); + when(mockTarget.getPosition()).thenReturn(mockTargetCoords); + // Stand the target in heavy woods. when(mockHex.terrainLevel(Terrains.WOODS)).thenReturn(2); expected = new ToHitData(); @@ -1247,33 +1295,45 @@ void testGuessToHitModifierHelperForAnyAttack() { mockShooterState, mockTarget, mockTargetState, - 10, + 1, mockGame)); when(mockHex.terrainLevel(Terrains.WOODS)).thenReturn(Terrain.LEVEL_NONE); // Stand the target in super heavy woods. when(mockHex.terrainLevel(Terrains.WOODS)).thenReturn(3); + when(mockHex.terrainLevel(Terrains.FOLIAGE_ELEV)).thenReturn(3); expected = new ToHitData(); expected.addModifier(3, FireControl.TH_WOODS); assertToHitDataEquals(expected, testFireControl.guessToHitModifierHelperForAnyAttack(mockShooter, mockShooterState, mockTarget, mockTargetState, - 10, + 1, mockGame)); when(mockHex.terrainLevel(Terrains.WOODS)).thenReturn(Terrain.LEVEL_NONE); // Stand the target in jungle. when(mockHex.terrainLevel(Terrains.JUNGLE)).thenReturn(2); + when(mockHex.terrainLevel(Terrains.FOLIAGE_ELEV)).thenReturn(2); expected = new ToHitData(); expected.addModifier(2, FireControl.TH_WOODS); assertToHitDataEquals(expected, testFireControl.guessToHitModifierHelperForAnyAttack(mockShooter, mockShooterState, mockTarget, mockTargetState, - 10, + 1, mockGame)); + + // 3. reset coords and terrain + mockShooterCoords = new Coords(0, 0); + when(mockShooter.getPosition()).thenReturn(mockShooterCoords); + when(mockShooterState.getPosition()).thenReturn(mockShooterCoords); + mockTargetCoords = new Coords(10, 0); + when(mockTarget.getPosition()).thenReturn(mockTargetCoords); + when(mockTargetState.getPosition()).thenReturn(mockTargetCoords); + when(mockHex.terrainLevel(Terrains.JUNGLE)).thenReturn(Terrain.LEVEL_NONE); + when(mockHex.terrainLevel(Terrains.FOLIAGE_ELEV)).thenReturn(Terrain.LEVEL_NONE); // Give the shooter the anti-air quirk but fire on a ground target. when(mockShooter.hasQuirk(eq(OptionsConstants.QUIRK_POS_ANTI_AIR))).thenReturn(true); diff --git a/megamek/unittests/megamek/client/ui/SharedUtilityTest.java b/megamek/unittests/megamek/client/ui/SharedUtilityTest.java index cb6984e2d48..2e4cc09a7a1 100644 --- a/megamek/unittests/megamek/client/ui/SharedUtilityTest.java +++ b/megamek/unittests/megamek/client/ui/SharedUtilityTest.java @@ -78,7 +78,6 @@ TargetRoll generateLeapFallRoll(Entity entity, int leapDistance) { @Test void testPredictLeapDamageBipedLeap3AvgPilot() { TargetRoll data = generateLeapRoll(bipedMek, 3); - StringBuilder msg = new StringBuilder(); // Rough math: // Chance of Pilot Skill 5 pilot to successfully Leap down 3 levels: @@ -93,7 +92,7 @@ void testPredictLeapDamageBipedLeap3AvgPilot() { // * (1 - 0.083) // Approximately 188 damage expected. double expectedDamage = 188.0; - double predictedDamage = predictLeapDamage(bipedMek, data, msg); + double predictedDamage = predictLeapDamage(bipedMek, data); assertEquals(expectedDamage, predictedDamage, 1.0); } @@ -101,7 +100,6 @@ void testPredictLeapDamageBipedLeap3AvgPilot() { @Test void testPredictLeapDamageQuadLeap3AvgPilot() { TargetRoll data = generateLeapRoll(quadMek, 3); - StringBuilder msg = new StringBuilder(); // Rough math: // Chance of Pilot Skill 5 pilot to successfully Leap down 3 levels: @@ -116,7 +114,7 @@ void testPredictLeapDamageQuadLeap3AvgPilot() { // * (1 - 0.277) // Approximately 298 damage expected. double expectedDamage = 298.0; - double predictedDamage = predictLeapDamage(quadMek, data, msg); + double predictedDamage = predictLeapDamage(quadMek, data); assertEquals(expectedDamage, predictedDamage, 1.0); } @@ -124,10 +122,9 @@ void testPredictLeapDamageQuadLeap3AvgPilot() { @Test void testPredictLeapFallDamageBipedLeap3AvgPilot() { TargetRoll data = generateLeapFallRoll(bipedMek, 3); - StringBuilder msg = new StringBuilder(); double expectedDamage = 12.0; - double predictedDamage = predictLeapFallDamage(bipedMek, data, msg); + double predictedDamage = predictLeapFallDamage(bipedMek, data); assertEquals(expectedDamage, predictedDamage, 1.0); } @@ -135,10 +132,9 @@ void testPredictLeapFallDamageBipedLeap3AvgPilot() { @Test void testPredictLeapFallDamageQuadLeap3AvgPilot() { TargetRoll data = generateLeapFallRoll(quadMek, 3); - StringBuilder msg = new StringBuilder(); double expectedDamage = 10.0; - double predictedDamage = predictLeapFallDamage(quadMek, data, msg); + double predictedDamage = predictLeapFallDamage(quadMek, data); assertEquals(expectedDamage, predictedDamage, 1.0); }