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