From f2ca760fc946350d31ef483f6073087dd5e2e33c Mon Sep 17 00:00:00 2001 From: exeea <48771891+exeea@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:15:42 +0200 Subject: [PATCH 01/10] manual/automatic roles --- .../alphaStrike/AlphaStrikeElement.java | 11 +- .../alphaStrike/conversion/ASConverter.java | 4 +- .../src/megamek/common/loaders/BLKFile.java | 4 +- megamek/src/megamek/common/units/Entity.java | 13 +- megamek/src/megamek/common/units/Mek.java | 4 +- .../src/megamek/common/units/UnitRole.java | 201 ++++++++++++++++-- 6 files changed, 206 insertions(+), 31 deletions(-) diff --git a/megamek/src/megamek/common/alphaStrike/AlphaStrikeElement.java b/megamek/src/megamek/common/alphaStrike/AlphaStrikeElement.java index 1fa874843be..73184e9b1d4 100644 --- a/megamek/src/megamek/common/alphaStrike/AlphaStrikeElement.java +++ b/megamek/src/megamek/common/alphaStrike/AlphaStrikeElement.java @@ -166,9 +166,18 @@ public String getModel() { @Override public UnitRole getRole() { + UnitRole roleOverride = getRoleOverride(); + return roleOverride.hasRole() ? roleOverride : UnitRole.bestRoleFor(this); + } + + public UnitRole getRoleOverride() { return (role == null) ? UnitRole.UNDETERMINED : role; } + public boolean hasRoleOverride() { + return getRoleOverride() != UnitRole.UNDETERMINED; + } + /** * @return The AS element's display name, including duplicate markers such as "#2". */ @@ -376,7 +385,7 @@ public void setModel(String newModel) { * Sets the AS element's battlefield role (ROLE). */ public void setRole(UnitRole newRole) { - role = newRole; + role = (newRole == null) ? UnitRole.UNDETERMINED : newRole; } /** diff --git a/megamek/src/megamek/common/alphaStrike/conversion/ASConverter.java b/megamek/src/megamek/common/alphaStrike/conversion/ASConverter.java index 0d233efdcdc..dae283ea853 100644 --- a/megamek/src/megamek/common/alphaStrike/conversion/ASConverter.java +++ b/megamek/src/megamek/common/alphaStrike/conversion/ASConverter.java @@ -113,7 +113,7 @@ private static AlphaStrikeElement performConversion(Entity entity, boolean inclu element.setModel(entity.getModel()); element.setChassis(entity.getFullChassis()); element.setMulId(entity.getMulId()); - element.setRole(entity.getRole()); + element.setRole(entity.getRoleOverride()); element.setFluff(entity.getFluff()); element.setId(entity.getId()); @@ -129,7 +129,7 @@ private static AlphaStrikeElement performConversion(Entity entity, boolean inclu conversionReport.addLine("Chassis:", element.getChassis(), ""); conversionReport.addLine("Model:", element.getModel(), ""); conversionReport.addLine("MUL ID:", Integer.toString(element.getMulId()), ""); - conversionReport.addLine("Unit Role:", element.getRole().toString(), ""); + conversionReport.addLine("Unit Role:", element.getRoleOverride().toString(), ""); // Type element.setType(ASUnitType.getUnitType(entity)); diff --git a/megamek/src/megamek/common/loaders/BLKFile.java b/megamek/src/megamek/common/loaders/BLKFile.java index d745bb0cd12..8e66a0e5d0e 100644 --- a/megamek/src/megamek/common/loaders/BLKFile.java +++ b/megamek/src/megamek/common/loaders/BLKFile.java @@ -765,8 +765,8 @@ public static BuildingBlock getBlock(Entity t) throws EntitySavingException { String type = getType(t); blk.writeBlockData("type", type); - if (t.hasRole()) { - blk.writeBlockData("role", t.getRole().toString()); + if (t.hasRoleOverride()) { + blk.writeBlockData("role", t.getRoleOverride().toString()); } List quirkList = t.getQuirks() diff --git a/megamek/src/megamek/common/units/Entity.java b/megamek/src/megamek/common/units/Entity.java index f765c07df3d..2aec363f5de 100644 --- a/megamek/src/megamek/common/units/Entity.java +++ b/megamek/src/megamek/common/units/Entity.java @@ -17451,12 +17451,21 @@ protected final IGameOptions gameOptions() { } public void setUnitRole(UnitRole role) { - this.role = role; + this.role = (role == null) ? UnitRole.UNDETERMINED : role; + } + + public UnitRole getRoleOverride() { + return (role == null) ? UnitRole.UNDETERMINED : role; + } + + public boolean hasRoleOverride() { + return getRoleOverride() != UnitRole.UNDETERMINED; } @Override public UnitRole getRole() { - return (role == null) ? UnitRole.UNDETERMINED : role; + UnitRole roleOverride = getRoleOverride(); + return roleOverride.hasRole() ? roleOverride : UnitRole.bestRoleFor(this); } /** diff --git a/megamek/src/megamek/common/units/Mek.java b/megamek/src/megamek/common/units/Mek.java index 88eaa6394d4..8ed8d09d57d 100644 --- a/megamek/src/megamek/common/units/Mek.java +++ b/megamek/src/megamek/common/units/Mek.java @@ -5083,8 +5083,8 @@ public String getMtf() { sb.append(MtfFile.RULES_LEVEL).append( TechConstants.T_SIMPLE_LEVEL[techLevel]); sb.append(newLine); - if (hasRole()) { - sb.append(MtfFile.ROLE).append(getRole().toString()); + if (hasRoleOverride()) { + sb.append(MtfFile.ROLE).append(getRoleOverride().toString()); sb.append(newLine); } if (techFaction != null && techFaction != Faction.NONE) { diff --git a/megamek/src/megamek/common/units/UnitRole.java b/megamek/src/megamek/common/units/UnitRole.java index 3079312a0fd..e840ee70b5d 100644 --- a/megamek/src/megamek/common/units/UnitRole.java +++ b/megamek/src/megamek/common/units/UnitRole.java @@ -77,6 +77,17 @@ public enum UnitRole { INTERCEPTOR(AERO), TRANSPORT(AERO); + private static final double NO_MATCH = -1_000_000; + private static final MMLogger LOGGER = MMLogger.create(UnitRole.class); + + private static final UnitRole[] GROUND_ROLES = { + MISSILE_BOAT, SNIPER, JUGGERNAUT, SCOUT, SKIRMISHER, STRIKER, AMBUSHER, BRAWLER + }; + + private static final UnitRole[] AERO_ROLES = { + ATTACK_FIGHTER, DOGFIGHTER, FAST_DOGFIGHTER, FIRE_SUPPORT, INTERCEPTOR + }; + enum Availability { GROUND(BTObject::isGround), AERO(BTObject::isAerospace), @@ -143,12 +154,56 @@ public static UnitRole parseRole(String role) { case "transport" -> TRANSPORT; case "none" -> NONE; default -> { - MMLogger.create(UnitRole.class).warn("Could not parse role: {}", role); + LOGGER.warn("Could not parse role: {}", role); yield UNDETERMINED; } }; } + /** + * Determines the best battlefield role for a unit using its converted Alpha Strike statistics. This does not read + * or replace the unit's persisted role. + * + * @param entity The unit to classify + * + * @return The best matching role, {@link #NONE} for support/non-combat units, or {@link #UNDETERMINED} when the + * unit cannot be converted + */ + public static UnitRole bestRoleFor(Entity entity) { + if (!ASConverter.canConvert(entity)) { + return UNDETERMINED; + } + AlphaStrikeElement element = ASConverter.convert(entity); + return (element == null) ? UNDETERMINED : bestRoleFor(element); + } + + /** + * Determines the best battlefield role for an Alpha Strike element. Ground units use the eight ground roles; + * units that return true for {@link BTObject#isAero()} use the aerospace role set. + * + * @param unit The Alpha Strike element to classify + * + * @return The best matching role, or {@link #NONE} when the unit is not classified by the role system + */ + public static UnitRole bestRoleFor(AlphaStrikeElement unit) { + if ((unit == null) || unit.isUnitGroup()) { + return UNDETERMINED; + } + + if (unit.isAero()) { + if (hasSignificantTransportCapacity(unit)) { + return TRANSPORT; + } else if (!isAeroCombatRoleCandidate(unit) || !hasMeaningfulAttack(unit)) { + return NONE; + } + return bestScoringRole(unit, AERO_ROLES, NONE); + } else if (isGroundCombatRoleCandidate(unit) && hasMeaningfulAttack(unit)) { + return bestScoringRole(unit, GROUND_ROLES, NONE); + } else { + return NONE; + } + } + /** * Applies the criteria from Alpha Strike Companion to determine whether a unit qualifies for a particular role. As * the canon unit roles do not themselves adhere strictly to the guidelines, there is some allowance for fuzziness @@ -211,8 +266,40 @@ public boolean qualifiesForRole(AlphaStrikeElement unit) { * @return Boolean value indicating whether the unit meets the qualifications for this role. */ public boolean qualifiesForRole(AlphaStrikeElement unit, double tolerance) { - if (!isAvailableTo(unit)) { - return false; + if (this == NONE) { + return bestRoleFor(unit) == NONE; + } + + double score = roleScore(unit); + return (score > NO_MATCH) && (score >= tolerance); + } + + private static UnitRole bestScoringRole(AlphaStrikeElement unit, UnitRole[] candidateRoles, UnitRole fallback) { + UnitRole bestRole = fallback; + double bestScore = NO_MATCH; + for (UnitRole candidateRole : candidateRoles) { + double candidateScore = candidateRole.roleScore(unit); + if (candidateScore > bestScore) { + bestRole = candidateRole; + bestScore = candidateScore; + } + } + return bestRole; + } + + + public double roleScore(AlphaStrikeElement unit) { + if ((unit == null) || !isAvailableTo(unit)) { + return logRoleScore(unit, NO_MATCH); + } + + if (isGroundRole(this) && (!isGroundCombatRoleCandidate(unit) || !hasMeaningfulAttack(unit))) { + return logRoleScore(unit, NO_MATCH); + } + + if (isAeroRole(this) && (this != TRANSPORT) + && (!unit.isAero() || !isAeroCombatRoleCandidate(unit) || !hasMeaningfulAttack(unit))) { + return logRoleScore(unit, NO_MATCH); } double score = 0; @@ -282,20 +369,24 @@ public boolean qualifiesForRole(AlphaStrikeElement unit, double tolerance) { break; case MISSILE_BOAT: /* Any artillery piece or can do damage by indirect fire at long range */ - return (unit.getStandardDamage().L().damage > 0 && unit.hasSUA(BattleForceSUA.IF)) - || unit.hasSUA(BattleForceSUA.ARTAIS) - || unit.hasSUA(BattleForceSUA.ARTAC) - || unit.hasSUA(BattleForceSUA.ARTBA) - || unit.hasSUA(BattleForceSUA.ARTCM5) - || unit.hasSUA(BattleForceSUA.ARTCM7) - || unit.hasSUA(BattleForceSUA.ARTCM9) - || unit.hasSUA(BattleForceSUA.ARTCM12) - || unit.hasSUA(BattleForceSUA.ARTT) - || unit.hasSUA(BattleForceSUA.ARTS) - || unit.hasSUA(BattleForceSUA.ARTLT) - || unit.hasSUA(BattleForceSUA.ARTTC) - || unit.hasSUA(BattleForceSUA.ARTSC) - || unit.hasSUA(BattleForceSUA.ARTLTC); + if (unit.getStandardDamage().L().damage > 0 && unit.hasSUA(BattleForceSUA.IF)) { + score++; + } + if (unit.hasSUA(BattleForceSUA.ARTAIS) + || unit.hasSUA(BattleForceSUA.ARTAC) + || unit.hasSUA(BattleForceSUA.ARTBA) + || unit.hasSUA(BattleForceSUA.ARTCM5) + || unit.hasSUA(BattleForceSUA.ARTCM7) + || unit.hasSUA(BattleForceSUA.ARTCM9) + || unit.hasSUA(BattleForceSUA.ARTCM12) + || unit.hasSUA(BattleForceSUA.ARTT) + || unit.hasSUA(BattleForceSUA.ARTS) + || unit.hasSUA(BattleForceSUA.ARTLT) + || unit.hasSUA(BattleForceSUA.ARTTC) + || unit.hasSUA(BattleForceSUA.ARTSC) + || unit.hasSUA(BattleForceSUA.ARTLTC)) { + score++; + } case SCOUT: /* * Fast (jump, WiGE, or VTOL helpful but not required), lightly armored, @@ -342,7 +433,9 @@ public boolean qualifiesForRole(AlphaStrikeElement unit, double tolerance) { break; case SNIPER: /* Can do damage at long range without LRMs */ - return unit.getStandardDamage().L().damage - unit.getLRM().L().damage > 0; + double damageWithoutLRMs = unit.getStandardDamage().L().damage - unit.getLRM().L().damage; + score += (damageWithoutLRMs * 0.25); + break; case STRIKER: /* Fast and light-medium armor, preference for short range */ score += Math.min(0, speed - 9) * 0.5; @@ -385,7 +478,7 @@ public boolean qualifiesForRole(AlphaStrikeElement unit, double tolerance) { case FIRE_SUPPORT: /* Not too slow and can do damage at long range */ if (unit.getStandardDamage().L().damage < 0.5) { - return false; + return logRoleScore(unit, NO_MATCH); } score += Math.min(0, speed - 5) + Math.min(0, 7 - speed); break; @@ -401,7 +494,7 @@ public boolean qualifiesForRole(AlphaStrikeElement unit, double tolerance) { break; case TRANSPORT: /* Has transport capacity */ - return unit.hasSUA(BattleForceSUA.CK) + if (unit.hasSUA(BattleForceSUA.CK) || unit.hasSUA(BattleForceSUA.IT) || unit.hasSUA(BattleForceSUA.AT) || unit.hasSUA(BattleForceSUA.PT) @@ -410,11 +503,75 @@ public boolean qualifiesForRole(AlphaStrikeElement unit, double tolerance) { || unit.hasSUA(BattleForceSUA.VTS) || unit.hasSUA(BattleForceSUA.MT) || unit.hasSUA(BattleForceSUA.CT) - || unit.hasSUA(BattleForceSUA.ST); + || unit.hasSUA(BattleForceSUA.ST)) { + score++; + } + break; default: break; } - return score >= tolerance; + return logRoleScore(unit, score); + } + + private double logRoleScore(AlphaStrikeElement unit, double score) { + LOGGER.info("{} for {}: {}", this, unitName(unit), score); + return score; + } + + private static String unitName(AlphaStrikeElement unit) { + return (unit == null) ? "null" : unit.getName(); + } + + private static boolean isGroundRole(UnitRole role) { + return role.isAnyOf(AMBUSHER, BRAWLER, JUGGERNAUT, MISSILE_BOAT, SCOUT, SKIRMISHER, SNIPER, STRIKER); + } + + private static boolean isAeroRole(UnitRole role) { + return role.isAnyOf(ATTACK_FIGHTER, DOGFIGHTER, FAST_DOGFIGHTER, FIRE_SUPPORT, INTERCEPTOR, TRANSPORT); + } + + private static boolean isGroundCombatRoleCandidate(AlphaStrikeElement unit) { + return !unit.isAero() && (unit.isBattleMek() || unit.isProtoMek() || unit.isCombatVehicle() + || unit.isSupportVehicle() || unit.isBattleArmor()); + } + + private static boolean isAeroCombatRoleCandidate(AlphaStrikeElement unit) { + return unit.isFighter() || unit.hasMovementMode("a"); + } + + private static boolean hasMeaningfulAttack(AlphaStrikeElement unit) { + return unit.getStandardDamage().hasDamage() + || unit.getIF().hasDamage() + || unit.getLRM().hasDamage() + || unit.getIATM().hasDamage() + || unit.getREAR().hasDamage() + || unit.getAC().hasDamage() + || unit.getSRM().hasDamage() + || unit.getTOR().hasDamage() + || unit.getHT().hasDamage() + || unit.getFLK().hasDamage() + || unit.getCAP().hasDamage() + || unit.getSCAP().hasDamage() + || unit.getMSL().hasDamage() + || hasArtillery(unit) + || unit.hasSUA(BattleForceSUA.BOMB); + } + + private static boolean hasSignificantTransportCapacity(AlphaStrikeElement unit) { + return unit.hasSUA(BattleForceSUA.CK) + || unit.hasSUA(BattleForceSUA.IT) + || unit.hasSUA(BattleForceSUA.AT) + || unit.hasSUA(BattleForceSUA.PT) + || unit.hasSUA(BattleForceSUA.VTM) + || unit.hasSUA(BattleForceSUA.VTH) + || unit.hasSUA(BattleForceSUA.VTS) + || unit.hasSUA(BattleForceSUA.MT) + || unit.hasSUA(BattleForceSUA.ST) + || (unit.hasSUA(BattleForceSUA.CT) && (unit.getCT() >= 50)); + } + + private static boolean hasArtillery(AlphaStrikeElement unit) { + return Arrays.stream(BattleForceSUA.values()).anyMatch(sua -> sua.isArtillery() && unit.hasSUA(sua)); } @Override From d0d8b78c8a1214d9a46b3f8a77a0ec10422a0012 Mon Sep 17 00:00:00 2001 From: exeea <48771891+exeea@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:25:18 +0200 Subject: [PATCH 02/10] removed debug logs --- megamek/src/megamek/common/units/UnitRole.java | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/megamek/src/megamek/common/units/UnitRole.java b/megamek/src/megamek/common/units/UnitRole.java index e840ee70b5d..8e7faf0a9f1 100644 --- a/megamek/src/megamek/common/units/UnitRole.java +++ b/megamek/src/megamek/common/units/UnitRole.java @@ -290,16 +290,16 @@ private static UnitRole bestScoringRole(AlphaStrikeElement unit, UnitRole[] cand public double roleScore(AlphaStrikeElement unit) { if ((unit == null) || !isAvailableTo(unit)) { - return logRoleScore(unit, NO_MATCH); + return NO_MATCH; } if (isGroundRole(this) && (!isGroundCombatRoleCandidate(unit) || !hasMeaningfulAttack(unit))) { - return logRoleScore(unit, NO_MATCH); + return NO_MATCH; } if (isAeroRole(this) && (this != TRANSPORT) && (!unit.isAero() || !isAeroCombatRoleCandidate(unit) || !hasMeaningfulAttack(unit))) { - return logRoleScore(unit, NO_MATCH); + return NO_MATCH; } double score = 0; @@ -478,7 +478,7 @@ public double roleScore(AlphaStrikeElement unit) { case FIRE_SUPPORT: /* Not too slow and can do damage at long range */ if (unit.getStandardDamage().L().damage < 0.5) { - return logRoleScore(unit, NO_MATCH); + return NO_MATCH; } score += Math.min(0, speed - 5) + Math.min(0, 7 - speed); break; @@ -510,18 +510,9 @@ public double roleScore(AlphaStrikeElement unit) { default: break; } - return logRoleScore(unit, score); - } - - private double logRoleScore(AlphaStrikeElement unit, double score) { - LOGGER.info("{} for {}: {}", this, unitName(unit), score); return score; } - private static String unitName(AlphaStrikeElement unit) { - return (unit == null) ? "null" : unit.getName(); - } - private static boolean isGroundRole(UnitRole role) { return role.isAnyOf(AMBUSHER, BRAWLER, JUGGERNAUT, MISSILE_BOAT, SCOUT, SKIRMISHER, SNIPER, STRIKER); } From 3ebfe61d90da9af43a1048ec3d88eae33451b997 Mon Sep 17 00:00:00 2001 From: exeea <48771891+exeea@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:26:09 +0200 Subject: [PATCH 03/10] in SNIPER calculation, LRM damage doubles the negative score --- megamek/src/megamek/common/units/UnitRole.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/megamek/src/megamek/common/units/UnitRole.java b/megamek/src/megamek/common/units/UnitRole.java index 8e7faf0a9f1..468a541cc82 100644 --- a/megamek/src/megamek/common/units/UnitRole.java +++ b/megamek/src/megamek/common/units/UnitRole.java @@ -433,7 +433,8 @@ public double roleScore(AlphaStrikeElement unit) { break; case SNIPER: /* Can do damage at long range without LRMs */ - double damageWithoutLRMs = unit.getStandardDamage().L().damage - unit.getLRM().L().damage; + double lrmDamage = unit.getLRM().L().damage; + double damageWithoutLRMs = unit.getStandardDamage().L().damage - (lrmDamage * 2); score += (damageWithoutLRMs * 0.25); break; case STRIKER: From cfe648df56d463b8f51a3a1b65e84701549dc4ff Mon Sep 17 00:00:00 2001 From: exeea <48771891+exeea@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:44:24 +0200 Subject: [PATCH 04/10] missing break --- megamek/src/megamek/common/units/UnitRole.java | 1 + 1 file changed, 1 insertion(+) diff --git a/megamek/src/megamek/common/units/UnitRole.java b/megamek/src/megamek/common/units/UnitRole.java index 468a541cc82..b8303667f60 100644 --- a/megamek/src/megamek/common/units/UnitRole.java +++ b/megamek/src/megamek/common/units/UnitRole.java @@ -387,6 +387,7 @@ public double roleScore(AlphaStrikeElement unit) { || unit.hasSUA(BattleForceSUA.ARTLTC)) { score++; } + break; case SCOUT: /* * Fast (jump, WiGE, or VTOL helpful but not required), lightly armored, From 6defad7377ca9bd21b07e486e4716cce2adeaf22 Mon Sep 17 00:00:00 2001 From: exeea <48771891+exeea@users.noreply.github.com> Date: Sun, 7 Jun 2026 23:49:27 +0200 Subject: [PATCH 05/10] tweaks in role identification --- .../src/megamek/common/units/UnitRole.java | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/megamek/src/megamek/common/units/UnitRole.java b/megamek/src/megamek/common/units/UnitRole.java index b8303667f60..bde8f1818a3 100644 --- a/megamek/src/megamek/common/units/UnitRole.java +++ b/megamek/src/megamek/common/units/UnitRole.java @@ -344,6 +344,7 @@ public double roleScore(AlphaStrikeElement unit) { * juggernaut. */ score += Math.min(0, unit.getFullArmor() - (unit.getSize() + 4)); + score += Math.max(0, unit.getFullArmor() - 7) * 0.2; if (Math.max(unit.getStandardDamage().S().damage, unit.getStandardDamage().M().damage) * 2 >= unit.getFullArmor()) { score++; @@ -368,10 +369,20 @@ public double roleScore(AlphaStrikeElement unit) { } break; case MISSILE_BOAT: - /* Any artillery piece or can do damage by indirect fire at long range */ + if (unit.hasSUA(BattleForceSUA.ENE)) { + score--; + } + if (unit.hasSUA(BattleForceSUA.AC)) { + score -= unit.getAC().L().damage * 0.1; + score -= unit.getAC().M().damage * 0.1; + score -= unit.getAC().S().damage * 0.1; + } + /* Can do damage by indirect fire at long range */ + score -= unit.getStandardDamage().S().damage * 0.1; if (unit.getStandardDamage().L().damage > 0 && unit.hasSUA(BattleForceSUA.IF)) { - score++; + score += unit.getIF().damage; } + /* Any artillery piece */ if (unit.hasSUA(BattleForceSUA.ARTAIS) || unit.hasSUA(BattleForceSUA.ARTAC) || unit.hasSUA(BattleForceSUA.ARTBA) @@ -412,10 +423,14 @@ public double roleScore(AlphaStrikeElement unit) { score++; } if (unit.getStandardDamage().S().damage > unit.getStandardDamage().M().damage) { - score++; - } else if (unit.getStandardDamage().S().damage > unit.getStandardDamage().L().damage) { score += 0.5; } + if (unit.getStandardDamage().S().damage > unit.getStandardDamage().L().damage) { + score += 0.5; + } + score -= (unit.getStandardDamage().S().damage * 0.1); + score -= (unit.getStandardDamage().M().damage * 0.25); + score -= (unit.getStandardDamage().L().damage * 0.5); break; case SKIRMISHER: /* Fast, medium-heavy armor with preference for medium range */ @@ -434,9 +449,9 @@ public double roleScore(AlphaStrikeElement unit) { break; case SNIPER: /* Can do damage at long range without LRMs */ - double lrmDamage = unit.getLRM().L().damage; - double damageWithoutLRMs = unit.getStandardDamage().L().damage - (lrmDamage * 2); - score += (damageWithoutLRMs * 0.25); + double indirectFireValue = unit.getIF().damage; + double damageWithoutIF = unit.getStandardDamage().L().damage - (indirectFireValue * 2); + score += (damageWithoutIF * 0.25); break; case STRIKER: /* Fast and light-medium armor, preference for short range */ @@ -444,8 +459,12 @@ public double roleScore(AlphaStrikeElement unit) { score -= Math.max(0, unit.getFullArmor() - 5); if (unit.getStandardDamage().S().damage > unit.getStandardDamage().M().damage) { score++; - } else if (unit.getStandardDamage().S().damage > unit.getStandardDamage().L().damage) { + } + if (unit.getStandardDamage().S().damage > unit.getStandardDamage().L().damage) { score += 0.5; + if (unit.getStandardDamage().S().damage == unit.getStandardDamage().M().damage) { + score += 0.5; + } } break; case ATTACK_FIGHTER: From ebeaaa7d4575f10ea493c453820688bc2e65aea1 Mon Sep 17 00:00:00 2001 From: exeea <48771891+exeea@users.noreply.github.com> Date: Mon, 8 Jun 2026 00:03:24 +0200 Subject: [PATCH 06/10] tweaks on missile boat detection --- megamek/src/megamek/common/units/UnitRole.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/megamek/src/megamek/common/units/UnitRole.java b/megamek/src/megamek/common/units/UnitRole.java index bde8f1818a3..4b8d65992a8 100644 --- a/megamek/src/megamek/common/units/UnitRole.java +++ b/megamek/src/megamek/common/units/UnitRole.java @@ -343,7 +343,7 @@ public double roleScore(AlphaStrikeElement unit) { * number of smaller units with lower armor values that have an official role of * juggernaut. */ - score += Math.min(0, unit.getFullArmor() - (unit.getSize() + 4)); + score += Math.min(0, unit.getFullArmor() - (unit.getSize() + 3)); score += Math.max(0, unit.getFullArmor() - 7) * 0.2; if (Math.max(unit.getStandardDamage().S().damage, unit.getStandardDamage().M().damage) * 2 >= unit.getFullArmor()) { @@ -381,6 +381,14 @@ public double roleScore(AlphaStrikeElement unit) { score -= unit.getStandardDamage().S().damage * 0.1; if (unit.getStandardDamage().L().damage > 0 && unit.hasSUA(BattleForceSUA.IF)) { score += unit.getIF().damage; + if (unit.getStandardDamage().L().damage > unit.getIF().damage) { + score -= (unit.getStandardDamage().L().damage - unit.getIF().damage) * 0.5; + } + } else { + score--; + } + if (unit.hasSUA(BattleForceSUA.LRM)) { + score += unit.getLRM().L().damage * 0.5; } /* Any artillery piece */ if (unit.hasSUA(BattleForceSUA.ARTAIS) From 4f2f5ff741c3614801b2225013d55236821095a8 Mon Sep 17 00:00:00 2001 From: exeea <48771891+exeea@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:25:46 +0200 Subject: [PATCH 07/10] missile boat tweaks --- megamek/src/megamek/common/units/UnitRole.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/megamek/src/megamek/common/units/UnitRole.java b/megamek/src/megamek/common/units/UnitRole.java index 4b8d65992a8..62192c2f900 100644 --- a/megamek/src/megamek/common/units/UnitRole.java +++ b/megamek/src/megamek/common/units/UnitRole.java @@ -372,18 +372,11 @@ public double roleScore(AlphaStrikeElement unit) { if (unit.hasSUA(BattleForceSUA.ENE)) { score--; } - if (unit.hasSUA(BattleForceSUA.AC)) { - score -= unit.getAC().L().damage * 0.1; - score -= unit.getAC().M().damage * 0.1; - score -= unit.getAC().S().damage * 0.1; - } + score -= Math.max(0,unit.getStandardDamage().S().damage - 1 - unit.getIF().damage) * 0.5; /* Can do damage by indirect fire at long range */ - score -= unit.getStandardDamage().S().damage * 0.1; if (unit.getStandardDamage().L().damage > 0 && unit.hasSUA(BattleForceSUA.IF)) { score += unit.getIF().damage; - if (unit.getStandardDamage().L().damage > unit.getIF().damage) { - score -= (unit.getStandardDamage().L().damage - unit.getIF().damage) * 0.5; - } + score -= Math.max(0, (unit.getStandardDamage().L().damage - 1 - unit.getIF().damage)) * 0.5; } else { score--; } From 1628330e96bc96139864d147324b0fc4bacc5ea8 Mon Sep 17 00:00:00 2001 From: exeea <48771891+exeea@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:00:21 +0200 Subject: [PATCH 08/10] artillery --- .../src/megamek/common/units/UnitRole.java | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/megamek/src/megamek/common/units/UnitRole.java b/megamek/src/megamek/common/units/UnitRole.java index 62192c2f900..1080b56ca32 100644 --- a/megamek/src/megamek/common/units/UnitRole.java +++ b/megamek/src/megamek/common/units/UnitRole.java @@ -384,20 +384,26 @@ public double roleScore(AlphaStrikeElement unit) { score += unit.getLRM().L().damage * 0.5; } /* Any artillery piece */ - if (unit.hasSUA(BattleForceSUA.ARTAIS) - || unit.hasSUA(BattleForceSUA.ARTAC) - || unit.hasSUA(BattleForceSUA.ARTBA) - || unit.hasSUA(BattleForceSUA.ARTCM5) - || unit.hasSUA(BattleForceSUA.ARTCM7) - || unit.hasSUA(BattleForceSUA.ARTCM9) - || unit.hasSUA(BattleForceSUA.ARTCM12) - || unit.hasSUA(BattleForceSUA.ARTT) - || unit.hasSUA(BattleForceSUA.ARTS) - || unit.hasSUA(BattleForceSUA.ARTLT) - || unit.hasSUA(BattleForceSUA.ARTTC) - || unit.hasSUA(BattleForceSUA.ARTSC) - || unit.hasSUA(BattleForceSUA.ARTLTC)) { - score++; + if (unit.hasSUA(BattleForceSUA.ARTCM7) + || unit.hasSUA(BattleForceSUA.ARTCM9) + || unit.hasSUA(BattleForceSUA.ARTCM12)) { + score += 5; + } + if (unit.hasSUA(BattleForceSUA.ARTCM5) + || unit.hasSUA(BattleForceSUA.ARTLT)) { + score += 4; + } + if (unit.hasSUA(BattleForceSUA.ARTAC) + || unit.hasSUA(BattleForceSUA.ARTAIS) + || unit.hasSUA(BattleForceSUA.ARTS) + || unit.hasSUA(BattleForceSUA.ARTLTC)) { + score += 3; + } + if (unit.hasSUA(BattleForceSUA.ARTBA) + ||unit.hasSUA(BattleForceSUA.ARTT) + || unit.hasSUA(BattleForceSUA.ARTTC) + || unit.hasSUA(BattleForceSUA.ARTSC)) { + score += 1.5; } break; case SCOUT: From 72302c09e954c0a5e49beb33816b116bdf5cd189 Mon Sep 17 00:00:00 2001 From: exeea <48771891+exeea@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:30:19 +0200 Subject: [PATCH 09/10] sniper --- megamek/src/megamek/common/units/UnitRole.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/megamek/src/megamek/common/units/UnitRole.java b/megamek/src/megamek/common/units/UnitRole.java index 1080b56ca32..ea098676bd9 100644 --- a/megamek/src/megamek/common/units/UnitRole.java +++ b/megamek/src/megamek/common/units/UnitRole.java @@ -457,8 +457,20 @@ public double roleScore(AlphaStrikeElement unit) { case SNIPER: /* Can do damage at long range without LRMs */ double indirectFireValue = unit.getIF().damage; - double damageWithoutIF = unit.getStandardDamage().L().damage - (indirectFireValue * 2); - score += (damageWithoutIF * 0.25); + double damageWithoutIF = unit.getStandardDamage().L().damage - indirectFireValue; + score += (damageWithoutIF * 0.5); + score -= unit.getLRM().L().damage * 0.25; + score += (unit.getStandardDamage().L().damage - Math.max(unit.getStandardDamage().S().damage, + unit.getStandardDamage().M().damage)) * 0.25; + if (unit.hasSUA(BattleForceSUA.AMS) + || unit.hasSUA(BattleForceSUA.ECM)) { + score+=0.1; + } + if (unit.hasSUA(BattleForceSUA.C3M) + || unit.hasSUA(BattleForceSUA.C3S) + || unit.hasSUA(BattleForceSUA.C3I)) { + score+=0.1; + } break; case STRIKER: /* Fast and light-medium armor, preference for short range */ From 8cc0308f29166a323c1fe15f498763d1283fc436 Mon Sep 17 00:00:00 2001 From: exeea <48771891+exeea@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:09:52 +0200 Subject: [PATCH 10/10] consider also IF* --- megamek/src/megamek/common/units/UnitRole.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/megamek/src/megamek/common/units/UnitRole.java b/megamek/src/megamek/common/units/UnitRole.java index ea098676bd9..352c8ea346d 100644 --- a/megamek/src/megamek/common/units/UnitRole.java +++ b/megamek/src/megamek/common/units/UnitRole.java @@ -372,11 +372,16 @@ public double roleScore(AlphaStrikeElement unit) { if (unit.hasSUA(BattleForceSUA.ENE)) { score--; } - score -= Math.max(0,unit.getStandardDamage().S().damage - 1 - unit.getIF().damage) * 0.5; + double missileBoatIndirectFireValue = unit.getIF().damage; + if (missileBoatIndirectFireValue == 0 && unit.hasSUA(BattleForceSUA.IF)) { + missileBoatIndirectFireValue = 0.5; + } + score -= Math.max(0,unit.getStandardDamage().S().damage - 1 - missileBoatIndirectFireValue) * 0.5; + /* Can do damage by indirect fire at long range */ if (unit.getStandardDamage().L().damage > 0 && unit.hasSUA(BattleForceSUA.IF)) { - score += unit.getIF().damage; - score -= Math.max(0, (unit.getStandardDamage().L().damage - 1 - unit.getIF().damage)) * 0.5; + score += missileBoatIndirectFireValue; + score -= Math.max(0, (unit.getStandardDamage().L().damage - 1 - missileBoatIndirectFireValue)) * 0.5; } else { score--; } @@ -456,8 +461,11 @@ public double roleScore(AlphaStrikeElement unit) { break; case SNIPER: /* Can do damage at long range without LRMs */ - double indirectFireValue = unit.getIF().damage; - double damageWithoutIF = unit.getStandardDamage().L().damage - indirectFireValue; + double sniperIndirectFireValue = unit.getIF().damage; + if (sniperIndirectFireValue == 0 && unit.hasSUA(BattleForceSUA.IF)) { + sniperIndirectFireValue = 0.5; + } + double damageWithoutIF = unit.getStandardDamage().L().damage - sniperIndirectFireValue; score += (damageWithoutIF * 0.5); score -= unit.getLRM().L().damage * 0.25; score += (unit.getStandardDamage().L().damage - Math.max(unit.getStandardDamage().S().damage,