diff --git a/megamek/mmconf/munitionLoadoutSettings.xml b/megamek/mmconf/munitionLoadoutSettings.xml
index 67fbbb0b57a..4cb6f127bd4 100644
--- a/megamek/mmconf/munitionLoadoutSettings.xml
+++ b/megamek/mmconf/munitionLoadoutSettings.xml
@@ -19,6 +19,8 @@
3.0
2.0
1.0
+ 4.0
+ 1.0
This count determines how many munition types (out of all options) are actually selected
4
The following entries are used by Bombs:
@@ -75,6 +77,7 @@
2.0
2.0
3.0
+ 0.0
2.0
1.0
2.0
diff --git a/megamek/src/megamek/client/generator/ReconfigurationParameters.java b/megamek/src/megamek/client/generator/ReconfigurationParameters.java
index 0f09be46993..4b2cd40c3e2 100644
--- a/megamek/src/megamek/client/generator/ReconfigurationParameters.java
+++ b/megamek/src/megamek/client/generator/ReconfigurationParameters.java
@@ -53,6 +53,7 @@ public class ReconfigurationParameters {
public long enemyFastMovers = 0;
public long enemyOffBoard = 0;
public long enemyECMCount = 0;
+ public long enemyTSMCount = 0;
public HashSet enemyFactions = new HashSet();
// Friendly stats
diff --git a/megamek/src/megamek/client/generator/TeamLoadoutGenerator.java b/megamek/src/megamek/client/generator/TeamLoadoutGenerator.java
index 36909b39c54..ff3acb03d52 100644
--- a/megamek/src/megamek/client/generator/TeamLoadoutGenerator.java
+++ b/megamek/src/megamek/client/generator/TeamLoadoutGenerator.java
@@ -57,7 +57,8 @@ public class TeamLoadoutGenerator {
try (InputStream is = new FileInputStream(LOADOUT_SETTINGS_PATH)) {
weightProperties.loadFromXML(is);
} catch (Exception e) {
- LogManager.getLogger().error("Munition weight properties could not be loaded! Using defaults...", e);
+ LogManager.getLogger().warn("Munition weight properties could not be loaded! Using defaults...", e);
+ LogManager.getLogger().debug(LOADOUT_SETTINGS_PATH + " was not loaded: ", e);
}
}
@@ -105,17 +106,20 @@ public class TeamLoadoutGenerator {
// TODO Anti-Radiation Missiles See IO pg 62 (TO 368)
public static final ArrayList SEEKING_MUNITIONS = new ArrayList<>(List.of(
- "Heat-Seeking", "Listen-Kill", "Swarm", "Swarm-I"));
+ "Heat-Seeking", "Listen-Kill", "Swarm", "Swarm-I"
+ ));
public static final ArrayList AMMO_REDUCING_MUNITIONS = new ArrayList<>(List.of(
"Acid", "Laser Inhibiting", "Follow The Leader", "Heat-Seeking", "Tandem-Charge",
"Thunder-Active", "Thunder-Augmented", "Thunder-Vibrabomb", "Thunder-Inferno",
"AAAMissile Ammo", "ASMissile Ammo", "ASWEMissile Ammo", "ArrowIVMissile Ammo",
- "AlamoMissile Ammo"));
+ "AlamoMissile Ammo"
+ ));
public static final ArrayList TYPE_LIST = new ArrayList(List.of(
"LRM", "SRM", "AC", "ATM", "Arrow IV", "Artillery", "Artillery Cannon",
- "Mek Mortar", "Narc", "Bomb"));
+ "Mek Mortar", "Narc", "Bomb"
+ ));
public static final Map> TYPE_MAP = Map.ofEntries(
entry("LRM", MunitionTree.LRM_MUNITION_NAMES),
@@ -127,7 +131,8 @@ public class TeamLoadoutGenerator {
entry("Artillery Cannon", MunitionTree.MEK_MORTAR_MUNITION_NAMES),
entry("Mek Mortar", MunitionTree.MEK_MORTAR_MUNITION_NAMES),
entry("Narc", MunitionTree.NARC_MUNITION_NAMES),
- entry("Bomb", MunitionTree.BOMB_MUNITION_NAMES));
+ entry("Bomb", MunitionTree.BOMB_MUNITION_NAMES)
+ );
// subregion Bombs
// bomb types assignable to aerospace units on ground maps
@@ -311,23 +316,31 @@ public void updateOptionValues(GameOptions gameOpts) {
showExtinct = gameOptions.booleanOption((OptionsConstants.ALLOWED_SHOW_EXTINCT));
}
- // See if selected ammoType is legal under current game rules, availability, TL,
- // tech base, etc.
+ /**
+ * Calculates legality of ammo types given a faction, tech base (IS/CL), mixed tech, and the instance's
+ * already-set year, tech level, and option for showing extinct equipment.
+ * @param aType the AmmoType of the munition under consideration. q.v.
+ * @param faction MM-style faction code, per factions.xml and FactionRecord keys
+ * @param techBase either 'IS' or 'CL', used for clan boolean check.
+ * @param mixedTech makes munitions checks more lenient by allowing faction to access both IS and CL techbases.
+ * @return boolean true if legal for combination of inputs, false otherwise. Determins if an AmmoType is loaded.
+ */
public boolean checkLegality(AmmoType aType, String faction, String techBase, boolean mixedTech) {
boolean legal = false;
boolean clan = techBase.equals("CL");
+ // Check if tech exists at all (or is explicitly allowed despite being extinct)
+ // and whether it is available at the current tech level.
+ legal = aType.isAvailableIn(allowedYear, showExtinct)
+ && aType.isLegal(allowedYear, legalLevel, clan, mixedTech, showExtinct);
+
if (eraBasedTechLevel) {
- // Check if tech is legal to use in this game based on year, tech level, etc.
- legal = aType.isLegal(allowedYear, legalLevel, clan,
- mixedTech, showExtinct);
- // Check if tech is widely available, or if the specific faction has access to
- // it
- legal &= aType.isAvailableIn(allowedYear, showExtinct)
- || aType.isAvailableIn(allowedYear, clan, ITechnology.getCodeFromIOAbbr(faction));
- } else {
- // Basic year check only
- legal = aType.getStaticTechLevel().ordinal() <= legalLevel.ordinal();
+ // Check if tech is available to this specific faction with the current year and tech base.
+ boolean eraBasedLegal = aType.isAvailableIn(allowedYear, clan, ITechnology.getCodeFromMMAbbr(faction));
+ if (mixedTech) {
+ eraBasedLegal |= aType.isAvailableIn(allowedYear, !clan, ITechnology.getCodeFromMMAbbr(faction));
+ }
+ legal &= eraBasedLegal;
}
// Nukes are not allowed... unless they are!
@@ -402,7 +415,7 @@ private static long checkForMeks(ArrayList el) {
* @return
*/
private static long checkForEnergyBoats(ArrayList el) {
- return el.stream().filter(e -> e.getAmmo().isEmpty()).count();
+ return el.stream().filter(e -> e.tracksHeat() && e.getAmmo().isEmpty()).count();
}
/**
@@ -474,6 +487,10 @@ private static long checkForECM(ArrayList el) {
return el.stream().filter(
Entity::hasECM).count();
}
+
+ private static long checkForTSM(ArrayList el) {
+ return el.stream().filter(e -> e.isMek() && ((Mech) e).hasTSM(false)).count();
+ }
// endregion Check for various unit types, armor types, etc.
// region generateParameters
@@ -610,6 +627,7 @@ public static ReconfigurationParameters generateParameters(
rp.enemyFastMovers += checkForFastMovers(etEntities);
rp.enemyOffBoard = checkForOffboard(etEntities);
rp.enemyECMCount = checkForECM(etEntities);
+ rp.enemyTSMCount = checkForTSM(etEntities);
} else {
// Assume we know _nothing_ about enemies if Double Blind is on.
rp.enemiesVisible = false;
@@ -790,11 +808,23 @@ public static MunitionTree generateMunitionTree(ReconfigurationParameters rp, Ar
mwc.decreaseHeatMunitions();
}
+ // Energy boats run hot; increase heat munitions and heat-seeking specifically
+ if (rp.enemyEnergyBoats > rp.enemyCount / castPropertyDouble("mtEnergyBoatEnemyFractionDivisor", 4.0)) {
+ mwc.increaseHeatMunitions();
+ mwc.increaseHeatMunitions();
+ mwc.increaseMunitions(new ArrayList<>(List.of("Heat-Seeking")));
+ }
+
// Counter EMC by swapping Seeking in for Guided
if (rp.enemyECMCount > castPropertyDouble("mtSeekingAmmoEnemyECMExceedThreshold", 1.0)) {
mwc.decreaseGuidedMunitions();
mwc.increaseSeekingMunitions();
- } else {
+ }
+ if (rp.enemyTSMCount > castPropertyDouble("mtSeekingAmmoEnemyTSMExceedThreshold", 1.0)) {
+ // Seeking
+ mwc.increaseSeekingMunitions();
+ }
+ if (rp.enemyECMCount == 0.0 && rp.enemyTSMCount == 0.0 && rp.enemyEnergyBoats == 0.0) {
// Seeking munitions are generally situational
mwc.decreaseSeekingMunitions();
}
@@ -1810,6 +1840,8 @@ private static HashMap initializeMissileWeaponWeights(ArrayList<
weights.put("Standard", getPropDouble("defaultMissileStandardMunitionWeight", 2.0));
// Dead-Fire should be even higher to start
weights.put("Dead-Fire", getPropDouble("defaultDeadFireMunitionWeight", 3.0));
+ // Artemis should be zeroed; Artemis-equipped launchers will be handled separately
+ weights.put("Artemis-capable", getPropDouble("defaultArtemiscapableMunitionWeight", 0.0));
return weights;
}
diff --git a/megamek/src/megamek/common/ITechnology.java b/megamek/src/megamek/common/ITechnology.java
index 425f212f2b3..73b15271fb6 100644
--- a/megamek/src/megamek/common/ITechnology.java
+++ b/megamek/src/megamek/common/ITechnology.java
@@ -284,6 +284,9 @@ && getExtinctionDate() < year
}
default boolean isAvailableIn(int year, boolean clan, boolean ignoreExtinction) {
+ // For technology created in the IS after the Clan Invasion, Clan availability
+ // matches IS (TO pg 33)
+ clan = clan && ITechnology.getTechEra(year) < ITechnology.ERA_CLAN;
return year >= getIntroductionDate(clan) && (getIntroductionDate(clan) != DATE_NONE)
&& (ignoreExtinction || !isExtinct(year, clan));
}
@@ -294,6 +297,9 @@ default boolean isAvailableIn(int year, boolean ignoreExtinction) {
}
default boolean isAvailableIn(int year, boolean clan, int faction) {
+ // For technology created in the IS after the Clan Invasion, Clan availability
+ // matches IS (TO pg 33)
+ clan = clan && ITechnology.getTechEra(year) < ITechnology.ERA_CLAN;
return year >= getIntroductionDate(clan, faction)
&& getIntroductionDate(clan, faction) != DATE_NONE && !isExtinct(year, clan, faction);
}
@@ -304,6 +310,9 @@ default boolean isLegal(int year, int techLevel, boolean mixedTech) {
}
default boolean isLegal(int year, SimpleTechLevel simpleRulesLevel, boolean clanBase, boolean mixedTech, boolean ignoreExtinct) {
+ // For technology created in the IS after the Clan Invasion, Clan availability
+ // matches IS (TO pg 33)
+ clanBase = clanBase && ITechnology.getTechEra(year) < ITechnology.ERA_CLAN;
if (mixedTech) {
if (!isAvailableIn(year, ignoreExtinct)) {
return false;
diff --git a/megamek/src/megamek/common/TechAdvancement.java b/megamek/src/megamek/common/TechAdvancement.java
index 40927f4b097..db6db320621 100644
--- a/megamek/src/megamek/common/TechAdvancement.java
+++ b/megamek/src/megamek/common/TechAdvancement.java
@@ -269,8 +269,10 @@ public int getPrototypeDate(boolean clan, int faction) {
// other factions after 3d6+5 years if it hasn't gone extinct by then.
// Using the minimum value here.
int date = getDate(PROTOTYPE, clan) + 8;
- if ((getDate(PRODUCTION, clan) < date)
- || (getDate(COMMON, clan) < date)
+ int dateProduction = getDate(PRODUCTION, clan);
+ int dateCommon = getDate(COMMON, clan);
+ if ((dateProduction != DATE_NONE && dateProduction < date)
+ || (dateCommon != DATE_NONE && dateCommon < date)
|| isExtinct(date, clan)) {
return DATE_NONE;
}
@@ -311,7 +313,7 @@ public int getProductionDate(boolean clan, int faction) {
// Per IO p. 34, tech with no common date becomes available to
// other factions after 10 years if it hasn't gone extinct by then.
int date = getDate(PRODUCTION, clan) + 10;
- if ((getDate(COMMON, clan) <= date)
+ if ((getDate(COMMON, clan) != DATE_NONE && getDate(COMMON, clan) <= date)
|| isExtinct(date, clan)) {
return DATE_NONE;
}
diff --git a/megamek/unittests/megamek/client/generator/TeamLoadoutGeneratorTest.java b/megamek/unittests/megamek/client/generator/TeamLoadoutGeneratorTest.java
index 7120926d479..612794340bb 100644
--- a/megamek/unittests/megamek/client/generator/TeamLoadoutGeneratorTest.java
+++ b/megamek/unittests/megamek/client/generator/TeamLoadoutGeneratorTest.java
@@ -385,17 +385,17 @@ void testAmmoTypeIllegalByTechLevel() {
assertFalse(tlg.checkLegality(mType, "CC", "IS", false));
assertFalse(tlg.checkLegality(mType, "FS", "IS", false));
assertFalse(tlg.checkLegality(mType, "IS", "IS", false));
- assertFalse(tlg.checkLegality(mType, "CL", "CL", false));
- assertFalse(tlg.checkLegality(mType, "CL", "CL", true));
+ assertFalse(tlg.checkLegality(mType, "CLAN", "CL", false));
+ assertFalse(tlg.checkLegality(mType, "CLAN", "CL", true));
- // Should be available to everyone, although only as Mixed Tech for Clans
+ // Should be available to everyone
when(mockGameOptions.stringOption(OptionsConstants.ALLOWED_TECHLEVEL)).thenReturn("Advanced");
tlg.updateOptionValues();
assertTrue(tlg.checkLegality(mType, "CC", "IS", false));
assertTrue(tlg.checkLegality(mType, "FS", "IS", false));
assertTrue(tlg.checkLegality(mType, "IS", "IS", false));
- assertTrue(tlg.checkLegality(mType, "CL", "CL", true));
- assertFalse(tlg.checkLegality(mType, "CL", "CL", false));
+ assertTrue(tlg.checkLegality(mType, "CLAN", "CL", true));
+ assertTrue(tlg.checkLegality(mType, "CLAN", "CL", true));
}
@Test
@@ -408,7 +408,9 @@ void testAmmoTypeIllegalBeforeCreation() {
assertTrue(tlg.checkLegality(mType, "CC", "IS", false));
assertTrue(tlg.checkLegality(mType, "FS", "IS", false));
assertTrue(tlg.checkLegality(mType, "IS", "IS", false));
- assertTrue(tlg.checkLegality(mType, "CL", "CL", true));
+ // Check mixed-tech and regular Clan tech, which should match IS at this point
+ assertTrue(tlg.checkLegality(mType, "CLAN", "CL", true));
+ assertTrue(tlg.checkLegality(mType, "CLAN", "CL", false));
// Set year back to 3025
when(mockGameOptions.intOption(OptionsConstants.ALLOWED_YEAR)).thenReturn(3025);
@@ -416,7 +418,7 @@ void testAmmoTypeIllegalBeforeCreation() {
assertFalse(tlg.checkLegality(mType, "CC", "IS", false));
assertFalse(tlg.checkLegality(mType, "FS", "IS", false));
assertFalse(tlg.checkLegality(mType, "IS", "IS", false));
- assertFalse(tlg.checkLegality(mType, "CL", "CL", true));
+ assertFalse(tlg.checkLegality(mType, "CLAN", "CL", true));
// Move up to 3070. Because of game settings and lack of "Common" year, ADA
// becomes available
@@ -424,9 +426,9 @@ void testAmmoTypeIllegalBeforeCreation() {
when(mockGameOptions.intOption(OptionsConstants.ALLOWED_YEAR)).thenReturn(3070);
tlg.updateOptionValues();
assertTrue(tlg.checkLegality(mType, "CC", "IS", false));
- assertTrue(tlg.checkLegality(mType, "FS", "IS", false));
- assertTrue(tlg.checkLegality(mType, "IS", "IS", false));
- assertFalse(tlg.checkLegality(mType, "CL", "CL", false));
+ assertFalse(tlg.checkLegality(mType, "FS", "IS", false));
+ assertFalse(tlg.checkLegality(mType, "IS", "IS", false));
+ assertFalse(tlg.checkLegality(mType, "CLAN", "CL", true));
}
@Test