diff --git a/MekHQ/data/scenariotemplates/Low-Atmosphere DropShip Escort.xml b/MekHQ/data/scenariotemplates/Low-Atmosphere DropShip Escort.xml index fd561de952..5e311109a4 100644 --- a/MekHQ/data/scenariotemplates/Low-Atmosphere DropShip Escort.xml +++ b/MekHQ/data/scenariotemplates/Low-Atmosphere DropShip Escort.xml @@ -156,7 +156,7 @@ 5 0 2 - 1.0 + 2.0 OpFor 1 5 @@ -178,9 +178,9 @@ 11 0 false - true + false true - true + false false 6 diff --git a/MekHQ/data/scenariotemplates/Space Blockade Run.xml b/MekHQ/data/scenariotemplates/Space Blockade Run.xml index 58c9816f07..df46580fda 100644 --- a/MekHQ/data/scenariotemplates/Space Blockade Run.xml +++ b/MekHQ/data/scenariotemplates/Space Blockade Run.xml @@ -195,7 +195,7 @@ 5 1 2 - 1.0 + 2.0 OpFor 1 5 diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java index e3466b8573..677bb23b50 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java @@ -68,8 +68,11 @@ import static java.lang.Math.round; import static megamek.client.ratgenerator.MissionRole.CIVILIAN; +import static megamek.common.Compute.randomInt; +import static megamek.common.UnitType.*; import static megamek.common.planetaryconditions.Wind.TORNADO_F4; import static mekhq.campaign.mission.Scenario.T_GROUND; +import static mekhq.campaign.mission.ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX; import static mekhq.campaign.mission.ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_CIVILIANS; import static mekhq.campaign.mission.ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX; @@ -165,7 +168,7 @@ public static AtBDynamicScenario initializeScenarioFromTemplate(ScenarioTemplate if (template.mapParameters.getMapLocation() == MapLocation.LowAtmosphere) { defaultReinforcements.setAllowedUnitType(ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX); } else if (template.mapParameters.getMapLocation() == MapLocation.Space) { - defaultReinforcements.setAllowedUnitType(UnitType.AEROSPACEFIGHTER); + defaultReinforcements.setAllowedUnitType(AEROSPACEFIGHTER); } template.getScenarioForces().put(defaultReinforcements.getForceName(), defaultReinforcements); @@ -270,7 +273,7 @@ private static int generateForces(AtBDynamicScenario scenario, AtBContract contr // recalculate effective BV and unit count each time we change levels // how close to the allowances do we want to get? - int targetPercentage = 100 + ((Compute.randomInt(8) - 3) * 5); + int targetPercentage = 100 + ((randomInt(8) - 3) * 5); logger.info(String.format("Target Percentage: %s", targetPercentage)); logger.info(String.format("Difficulty Multiplier: %s", getDifficultyMultiplier(campaign))); @@ -410,7 +413,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac factionCode = "PIR"; } - int randomInt = Compute.randomInt(6); + int randomInt = randomInt(6); skill = switch (randomInt) { case 1, 2, 3 -> SkillLevel.REGULAR; @@ -442,7 +445,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac int forceBV = 0; double forceMultiplier = forceTemplate.getForceMultiplier(); - if (forceTemplate.getForceMultiplier() != 1) { + if (forceMultiplier != 1) { logger.info(String.format("Force BV Multiplier: %s (from scenario template)", forceMultiplier)); } @@ -522,14 +525,14 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac if (!baseRoles.isEmpty()) { if (forceTemplate.getAllowedUnitType() == SPECIAL_UNIT_TYPE_ATB_MIX) { - requiredRoles.put(UnitType.MEK, new ArrayList<>(baseRoles)); - requiredRoles.put(UnitType.TANK, new ArrayList<>(baseRoles)); + requiredRoles.put(MEK, new ArrayList<>(baseRoles)); + requiredRoles.put(TANK, new ArrayList<>(baseRoles)); } else if (forceTemplate.getAllowedUnitType() == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX) { - requiredRoles.put(UnitType.CONV_FIGHTER, new ArrayList<>(baseRoles)); - requiredRoles.put(UnitType.AEROSPACEFIGHTER, new ArrayList<>(baseRoles)); + requiredRoles.put(CONV_FIGHTER, new ArrayList<>(baseRoles)); + requiredRoles.put(AEROSPACEFIGHTER, new ArrayList<>(baseRoles)); } else if (forceTemplate.getAllowedUnitType() == SPECIAL_UNIT_TYPE_ATB_CIVILIANS) { // TODO: this will need to be adjusted to cover SUPPORT and CIVILIAN separately - for (int i = 0; i <= UnitType.AERO; i++) { + for (int i = 0; i <= AERO; i++) { if (CIVILIAN.fitsUnitType(i)) { requiredRoles.put(i, new ArrayList<>(baseRoles)); } @@ -547,10 +550,10 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac } else { infantryRoles.add(MissionRole.XCT); } - if (requiredRoles.containsKey(UnitType.INFANTRY)) { - requiredRoles.get(UnitType.INFANTRY).addAll(infantryRoles); + if (requiredRoles.containsKey(INFANTRY)) { + requiredRoles.get(INFANTRY).addAll(infantryRoles); } else { - requiredRoles.put(UnitType.INFANTRY, infantryRoles); + requiredRoles.put(INFANTRY, infantryRoles); } } @@ -559,23 +562,23 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac if (forceTemplate.getUseArtillery()) { int artilleryCarriers = forceTemplate.getAllowedUnitType(); - if (artilleryCarriers == SPECIAL_UNIT_TYPE_ATB_MIX || artilleryCarriers == UnitType.MEK) { - if (!requiredRoles.containsKey(UnitType.MEK)) { - requiredRoles.put(UnitType.MEK, new HashSet<>()); + if (artilleryCarriers == SPECIAL_UNIT_TYPE_ATB_MIX || artilleryCarriers == MEK) { + if (!requiredRoles.containsKey(MEK)) { + requiredRoles.put(MEK, new HashSet<>()); } - requiredRoles.get(UnitType.MEK).add((MissionRole.ARTILLERY)); + requiredRoles.get(MEK).add((MissionRole.ARTILLERY)); } - if (artilleryCarriers == SPECIAL_UNIT_TYPE_ATB_MIX || artilleryCarriers == UnitType.TANK) { - if (!requiredRoles.containsKey(UnitType.TANK)) { - requiredRoles.put(UnitType.TANK, new HashSet<>()); + if (artilleryCarriers == SPECIAL_UNIT_TYPE_ATB_MIX || artilleryCarriers == TANK) { + if (!requiredRoles.containsKey(TANK)) { + requiredRoles.put(TANK, new HashSet<>()); } - requiredRoles.get(UnitType.TANK).add((MissionRole.ARTILLERY)); + requiredRoles.get(TANK).add((MissionRole.ARTILLERY)); } - if (artilleryCarriers == UnitType.INFANTRY) { - if (!requiredRoles.containsKey(UnitType.INFANTRY)) { - requiredRoles.put(UnitType.INFANTRY, new HashSet<>()); + if (artilleryCarriers == INFANTRY) { + if (!requiredRoles.containsKey(INFANTRY)) { + requiredRoles.put(INFANTRY, new HashSet<>()); } - requiredRoles.get(UnitType.INFANTRY).add((MissionRole.ARTILLERY)); + requiredRoles.get(INFANTRY).add((MissionRole.ARTILLERY)); } } @@ -595,50 +598,46 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac int actualUnitType = forceTemplate.getAllowedUnitType(); - // The SPECIAL_UNIT_TYPE_ATB_AERO_MIX value allows for random selection of - // aerospace or conventional fighters. - // Only allow for conventional fighters where this force controls the system, and where - // there is an atmosphere. Aerospace fighters are added in single flights/points, while - // conventional fighters are added in full squadrons (1-3 flights, 2-6 total). - if (isPlanetOwner && actualUnitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX - && scenario.getTemplate().mapParameters.getMapLocation() != MapLocation.Space && scenario.getAtmosphere().isDenserThan(Atmosphere.THIN)) { - actualUnitType = Compute.d6() > 3 ? UnitType.AEROSPACEFIGHTER : UnitType.CONV_FIGHTER; - lanceSize = getAeroLanceSize(actualUnitType, isPlanetOwner, factionCode); - } else if (actualUnitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX) { - actualUnitType = UnitType.AEROSPACEFIGHTER; - lanceSize = getAeroLanceSize(actualUnitType, isPlanetOwner, factionCode); - } - // If there are no weight classes available, something went wrong so don't // bother trying to generate units if (currentLanceWeightString == null) { generatedLance = new ArrayList<>(); } else { // Hazardous conditions may prohibit deploying infantry or vehicles - if ((actualUnitType == UnitType.INFANTRY && !allowsConvInfantry) - || (actualUnitType == UnitType.BATTLE_ARMOR && !allowsBattleArmor)) { + if ((actualUnitType == INFANTRY && !allowsConvInfantry) + || (actualUnitType == BATTLE_ARMOR && !allowsBattleArmor)) { logger.warn("Unable to generate Infantry due to hostile conditions." + " Switching to Tank."); - actualUnitType = UnitType.TANK; + actualUnitType = TANK; } - if (actualUnitType == UnitType.TANK && !allowsTanks) { + if (actualUnitType == TANK && !allowsTanks) { logger.warn("Unable to generate Tank due to hostile conditions." + " Switching to Mek."); - actualUnitType = UnitType.MEK; + actualUnitType = MEK; } // Gun emplacements use fixed tables instead of the force generator system - if (actualUnitType == UnitType.GUN_EMPLACEMENT) { + if (actualUnitType == GUN_EMPLACEMENT) { generatedLance = generateTurrets(4, skill, quality, campaign, faction); // All other unit types use the force generator system to randomly select units } else { + boolean allowConventionalAircraft = isPlanetOwner + && actualUnitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX + && scenario.getTemplate().mapParameters.getMapLocation() != MapLocation.Space + && scenario.getAtmosphere().isDenserThan(Atmosphere.THIN); + + if (actualUnitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX) { + lanceSize = getAeroLanceSize(faction); + } + // Determine unit types for each unit of the formation. Normally this is all one // type, but SPECIAL_UNIT_TYPE_ATB_MIX may generate all Meks, all vehicles, or - // a Mek/vehicle mixed formation. + // a Mek/vehicle mixed formation. Similarly, SPECIAL_UNIT_TYPE_ATB_AERO_MIX may + // generate all Aerospace Fighters, Conventional Fighters, or a mixed formation. List unitTypes = generateUnitTypes(actualUnitType, lanceSize, quality, - factionCode, allowsTanks, campaign); + factionCode, allowsTanks, allowConventionalAircraft, campaign); // Formations composed entirely of Meks, aerospace fighters (but not conventional), // and ground vehicles use weight categories as do SPECIAL_UNIT_TYPE_ATB_MIX. @@ -672,7 +671,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac // If extreme temperatures are present and XCT infantry is not being generated, // swap out standard armor for snowsuits or heat suits as appropriate - if (actualUnitType == UnitType.INFANTRY) { + if (actualUnitType == INFANTRY) { for (Entity curPlatoon : generatedLance) { changeInfantryKit((Infantry) curPlatoon, isLowPressure, isTainted, scenario.getTemperature()); } @@ -707,7 +706,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac case Third: // Slight hack, assume "Unidentified Hostiles" are pirates with variable // quality - ownerBaseQuality = Compute.randomInt(3); + ownerBaseQuality = randomInt(3); isPirate = forceTemplate.getForceName().toLowerCase().contains("unidentified"); break; default: @@ -754,13 +753,17 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac // Add the formation member BVs to the running total, and the entities to the tracking // list - for (Entity ent : generatedLance) { + for (Entity entity : generatedLance) { + int individualBV; + if (campaign.getCampaignOptions().isUseGenericBattleValue()) { - forceBV += ent.getGenericBattleValue(); + individualBV = entity.getGenericBattleValue(); } else { - forceBV += ent.calculateBattleValue(); + individualBV = entity.calculateBattleValue(); } - generatedEntities.add(ent); + + forceBV += individualBV; + generatedEntities.add(entity); } // Terminate force generation if we've gone over the unit count or BV budget. @@ -782,39 +785,68 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac // If over budget for BV or unit count, pull units until it works while (forceUnitBudget > 0 && generatedEntities.size() > forceUnitBudget) { - int targetUnit = Compute.randomInt(generatedEntities.size()); + int targetUnit = randomInt(generatedEntities.size()); generatedEntities.remove(targetUnit); } if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal()) { String balancingType = ""; + if (campaign.getCampaignOptions().isUseGenericBattleValue()) { balancingType = " Generic"; } + logger.info(String.format("%s generated a force with %s / %s %s BV", forceTemplate.getForceName(), forceBV, forceBVBudget, balancingType)); - int adjustedBvBudget = (int) (forceBVBudget * 1.25); + if ((forceBV > forceBVBudget) && generatedEntities.size() != 1) { + List culledEntities = new ArrayList<>(); + Collections.shuffle(generatedEntities); - while ((forceBV > adjustedBvBudget) && (generatedEntities.size() > 1)) { - int targetUnit = Compute.randomInt(generatedEntities.size()); + forceBV = 0; - int battleValue; - if (campaign.getCampaignOptions().isUseGenericBattleValue()) { - battleValue = generatedEntities.get(targetUnit).getGenericBattleValue(); - } else { - battleValue = generatedEntities.get(targetUnit).calculateBattleValue(); + for (Entity entity : generatedEntities) { + int battleValue; + + if (campaign.getCampaignOptions().isUseGenericBattleValue()) { + battleValue = entity.getGenericBattleValue(); + } else { + battleValue = entity.calculateBattleValue(); + } + + if ((forceBV + battleValue) > forceBVBudget) { + culledEntities.add(entity); + + logger.info(String.format("Culled %s (%s %s BV)", + entity.getDisplayName(), battleValue, balancingType)); + + continue; + } + + forceBV += battleValue; } - forceBV -= battleValue; + if (generatedEntities.isEmpty()) { + Entity entity = culledEntities.get(0); - logger.info(String.format("Culled %s (%s %s BV)", - generatedEntities.get(targetUnit).getDisplayName(), battleValue, balancingType)); + int battleValue; + if (campaign.getCampaignOptions().isUseGenericBattleValue()) { + battleValue = entity.getGenericBattleValue(); + } else { + battleValue = entity.calculateBattleValue(); + } + + logger.info(String.format("Ended up with an empty force, restoring %s (%s %s BV)", + entity.getDisplayName(), battleValue, balancingType)); - generatedEntities.remove(targetUnit); + culledEntities.remove(0); + } + + generatedEntities.removeAll(culledEntities); } - logger.info(String.format("Final force %s / %s %s BV (may exceed by *1.25)", + + logger.info(String.format("Final force %s / %s %s BV", forceBV, forceBVBudget, balancingType)); } @@ -825,7 +857,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac if (!transportedEntities.isEmpty()) { // Transported units need to filter out battle armor before applying armor changes - for (Entity curPlatoon : transportedEntities.stream().filter(i -> i.getUnitType() == UnitType.INFANTRY).toList()) { + for (Entity curPlatoon : transportedEntities.stream().filter(i -> i.getUnitType() == INFANTRY).toList()) { changeInfantryKit((Infantry) curPlatoon, isLowPressure, isTainted, scenario.getTemperature()); } } @@ -844,7 +876,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac && BatchallFactions.usesBatchalls(factionCode) && contract.isBatchallAccepted()) { // Simulate bidding away of forces - List bidAwayForces = new ArrayList<>(); + List bidAwayForces = new ArrayList<>(); int supplementedForces = 0; if (generatedForce.getTeam() != 1 @@ -852,74 +884,82 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac && BatchallFactions.usesBatchalls(factionCode) && contract.isBatchallAccepted()) { - // Add dialog - bidAwayForces = new ArrayList<>(); - // Player force values int playerBattleValue = calculateEffectiveBV(scenario, campaign, true); int playerUnitValue = calculateEffectiveUnitCount(scenario, campaign, true); - // Bot force values - int botBattleValue = 0; - for (Entity entity : generatedForce.getFullEntityList(campaign)) { - botBattleValue += entity.calculateBattleValue(); + // First bid away units that exceed the player's estimated Battle Value + int targetBattleValue = (int) round(playerBattleValue * getHonorRating(campaign, factionCode)); + int currentBattleValue = 0; + + List entities = generatedForce.getFullEntityList(campaign); + Collections.shuffle(entities); + + for (Entity entity : entities) { + int battleValue = entity.calculateBattleValue(); + if ((currentBattleValue + battleValue) > targetBattleValue) { + bidAwayForces.add(entity); + continue; + } + + currentBattleValue += battleValue; } - // First bid away units that exceed the player's estimated Battle Value - while ((botBattleValue > (playerBattleValue * getHonorRating(campaign, factionCode))) - && (generatedForce.getFullEntityList(campaign).size() > 1)) { - int targetUnit = Compute.randomInt(generatedForce.getFullEntityList(campaign).size()); - bidAwayForces.add(generatedForce.getFullEntityList(campaign).get(targetUnit).getShortNameRaw()); - botBattleValue -= generatedForce.getFullEntityList(campaign).get(targetUnit).calculateBattleValue(); - generatedForce.removeEntity(targetUnit); - } - - // There is no point in adding extra Battle Armor to non-ground scenarios - // Similarly, there is no point adding Battle Armor to scenarios they cannot survive in. - if (scenario.getBoardType() == T_GROUND && scenario.getWind() != TORNADO_F4) { - // We want to purposefully exclude off-board artillery, to stop them being - // assigned random units of Battle Armor. - // If we ever implement the ability to move those units on-board, or for players - // to intercept off-board units, we'll probably want to remove this exclusion, - // so they can have some bodyguards. - if (!forceTemplate.getUseArtillery() && !forceTemplate.getDeployOffboard()) { - // Similarly, there is no value in adding random Battle Armor to aircraft forces - if (forceTemplate.getAllowedUnitType() != SPECIAL_UNIT_TYPE_ATB_MIX) { - // Next, if the size of the forces results in a Player:Bot unit count ratio of >= 2:1, - // add additional units of Battle Armor to compensate. - int sizeDisparity = playerUnitValue - generatedForce.getFullEntityList(campaign).size(); - sizeDisparity = (int) round(sizeDisparity * 0.5); - - List allRemainingUnits = new ArrayList<>(generatedForce.getFullEntityList(campaign)); - Collections.shuffle(allRemainingUnits); - - // First, attempt to add Mechanized Battle Armor - Iterator entityIterator = allRemainingUnits.iterator(); - while (entityIterator.hasNext() && sizeDisparity > 0) { - Entity entity = entityIterator.next(); - if (!entity.isOmni()) { - continue; - } + for (Entity entity : bidAwayForces) { + int entityIndex = entities.indexOf(entity); + generatedForce.removeEntity(entityIndex); + } + + // We don't want to sub in Battle Armor for forces that are meant to only have a + // certain number of units. + if (forceTemplate.getGenerationMethod() != ForceGenerationMethod.FixedUnitCount.ordinal()) { + // There is no point in adding extra Battle Armor to non-ground scenarios + // Similarly, there is no point adding Battle Armor to scenarios they cannot survive in. + if (scenario.getBoardType() == T_GROUND && scenario.getWind() != TORNADO_F4) { + // We want to purposefully exclude off-board artillery, to stop them being + // assigned random units of Battle Armor. + // If we ever implement the ability to move those units on-board, or for players + // to intercept off-board units, we'll probably want to remove this exclusion, + // so they can have some bodyguards. + if (!forceTemplate.getUseArtillery() && !forceTemplate.getDeployOffboard()) { + // Similarly, there is no value in adding random Battle Armor to aircraft forces + if (forceTemplate.getAllowedUnitType() != SPECIAL_UNIT_TYPE_ATB_MIX) { + // Next, if the size of the forces results in a Player:Bot unit count ratio of >= 2:1, + // add additional units of Battle Armor to compensate. + int sizeDisparity = playerUnitValue - generatedForce.getFullEntityList(campaign).size(); + sizeDisparity = (int) round(sizeDisparity * 0.5); + + List allRemainingUnits = new ArrayList<>(generatedForce.getFullEntityList(campaign)); + Collections.shuffle(allRemainingUnits); + + // First, attempt to add Mechanized Battle Armor + Iterator entityIterator = allRemainingUnits.iterator(); + while (entityIterator.hasNext() && sizeDisparity > 0) { + Entity entity = entityIterator.next(); + if (!entity.isOmni()) { + continue; + } - List generatedBA = generateBAForNova(scenario, List.of(entity), + List generatedBA = generateBAForNova(scenario, List.of(entity), factionCode, skill, quality, campaign, true); - if (!generatedBA.isEmpty()) { - for (Entity battleArmor : generatedBA) { - generatedForce.addEntity(battleArmor); + if (!generatedBA.isEmpty()) { + for (Entity battleArmor : generatedBA) { + generatedForce.addEntity(battleArmor); + } + supplementedForces += generatedBA.size(); + sizeDisparity -= generatedBA.size(); } - supplementedForces += generatedBA.size(); - sizeDisparity -= generatedBA.size(); } - } - // If there is still a disproportionate size disparity, add loose Battle Armor - for (int i = 0; i < sizeDisparity; i++) { - Entity newEntity = getEntity(factionCode, skill, quality, - UnitType.BATTLE_ARMOR, UNIT_WEIGHT_UNSPECIFIED, campaign); - if (newEntity != null) { - generatedForce.addEntity(newEntity); - supplementedForces++; + // If there is still a disproportionate size disparity, add loose Battle Armor + for (int i = 0; i < sizeDisparity; i++) { + Entity newEntity = getEntity(factionCode, skill, quality, + BATTLE_ARMOR, UNIT_WEIGHT_UNSPECIFIED, campaign); + if (newEntity != null) { + generatedForce.addEntity(newEntity); + supplementedForces++; + } } } } @@ -932,8 +972,8 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac && campaign.getCampaignOptions().isUseGenericBattleValue() && BatchallFactions.usesBatchalls(factionCode) && contract.isBatchallAccepted()) { - reportResultsOfBidding(scenario, campaign, bidAwayForces, generatedForce, - supplementedForces, factionCode); + reportResultsOfBidding(campaign, bidAwayForces, generatedForce, supplementedForces, + factionCode); } } @@ -949,7 +989,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac */ private static double getHonorRating(Campaign campaign, String factionCode) { // Our research showed the post-Invasion shift in Clan doctrine to occur between 3053 and 3055 - boolean isPostInvasion = campaign.getLocalDate().getYear() >= 3053 + Compute.randomInt(2); + boolean isPostInvasion = campaign.getLocalDate().getYear() >= 3053 + randomInt(2); // This is based on the table found on page 274 of Total Warfare // Any Clan not mentioned on that table is assumed to be Strict → Opportunistic @@ -984,15 +1024,14 @@ private static double getHonorRating(Campaign campaign, String factionCode) { /** * Reports the results of Clan bidding for a scenario. * - * @param scenario the scenario for which bidding was done * @param campaign the campaign in which the bidding took place * @param bidAwayForces the list of forces bid away by the generated force * @param generatedForce the force that generated the bid * @param supplementedForces the number of additional Battle Armor units supplemented to the force */ - private static void reportResultsOfBidding(AtBDynamicScenario scenario, Campaign campaign, - List bidAwayForces, BotForce generatedForce, - int supplementedForces, String factionCode) { + private static void reportResultsOfBidding(Campaign campaign, List bidAwayForces, + BotForce generatedForce, int supplementedForces, + String factionCode) { double honor = getHonorRating(campaign, factionCode); String honorLevel; @@ -1012,12 +1051,12 @@ private static void reportResultsOfBidding(AtBDynamicScenario scenario, Campaign StringBuilder report = new StringBuilder(); if (useVerboseBidding) { - for (String unitName : bidAwayForces) { + for (Entity entity : bidAwayForces) { if (report.isEmpty()) { report.append(String.format(resources.getString("bidAwayForcesVerbose.text"), - generatedForce.getName(), unitName)); + generatedForce.getName(), entity.getFullChassis())); } else { - report.append(unitName).append("
"); + report.append(entity.getFullChassis()).append("
"); } } } else { @@ -1026,13 +1065,13 @@ private static void reportResultsOfBidding(AtBDynamicScenario scenario, Campaign generatedForce.getName(), bidAwayForces.size(), bidAwayForces.size() > 1 ? "s" : "")); boolean isUseLoggerHeader = true; - for (String unitName : bidAwayForces) { + for (Entity entity : bidAwayForces) { if (isUseLoggerHeader) { logger.info(String.format(resources.getString("bidAwayForcesLogger.text"), generatedForce.getName())); isUseLoggerHeader = false; } - logger.info(unitName); + logger.info(String.format("%s, %s", entity.getFullChassis(), entity.getGenericBattleValue())); } } } @@ -1299,7 +1338,7 @@ public static void setTerrain(AtBDynamicScenario scenario) { List allowedTerrain = biomeManifest.getTempMap(StratconBiomeManifest.TERRAN_BIOME) .floorEntry(kelvinTemp).getValue().allowedTerrainTypes; - int terrainIndex = Compute.randomInt(allowedTerrain.size()); + int terrainIndex = randomInt(allowedTerrain.size()); scenario.setTerrainType(allowedTerrain.get(terrainIndex)); scenario.setMapFile(); } else if (scenario.getTemplate().mapParameters.getMapLocation() == MapLocation.Space) { @@ -1325,7 +1364,7 @@ public static void setTerrain(AtBDynamicScenario scenario) { allowedTemplate = !allowedTemplate.isEmpty() ? allowedTemplate : scenario.getTemplate().mapParameters.allowedTerrainTypes; - int terrainIndex = Compute.randomInt(allowedTemplate.size()); + int terrainIndex = randomInt(allowedTemplate.size()); scenario.setTerrainType(scenario.getTemplate().mapParameters.allowedTerrainTypes.get(terrainIndex)); scenario.setMapFile(); } @@ -1374,7 +1413,7 @@ public static void setScenarioMapSize(AtBDynamicScenario scenario) { // if the template says to use standard AtB sizing, determine it randomly here if (template.mapParameters.isUseStandardAtBSizing()) { - int roll = Compute.randomInt(20) + 1; + int roll = randomInt(20) + 1; if (roll < 6) { mapSizeX = 20; mapSizeY = 10; @@ -1409,7 +1448,7 @@ public static void setScenarioMapSize(AtBDynamicScenario scenario) { // 50/50 odds to rotate the map 90 degrees if specified. if (template.mapParameters.isAllowRotation()) { - int roll = Compute.randomInt(20) + 1; + int roll = randomInt(20) + 1; if (roll <= 10) { int swap = mapSizeX; mapSizeX = mapSizeY; @@ -1437,7 +1476,7 @@ public static void setScenarioModifiers(CampaignOptions campaignOptions, AtBDyna if (modMax != 0) { while (addMods) { - if (Compute.randomInt(100) < modChance) { + if (randomInt(100) < modChance) { numMods++; if (numMods >= modMax) { @@ -1545,9 +1584,9 @@ public static Entity getEntity(String faction, } // Vehicles and infantry require some additional processing - if (unitType == UnitType.TANK) { + if (unitType == TANK) { return getTankEntity(params, skill, campaign); - } else if (unitType == UnitType.INFANTRY) { + } else if (unitType == INFANTRY) { return getInfantryEntity(params, skill, true, campaign); } else { unitData = campaign.getUnitGenerator().generate(params); @@ -1562,7 +1601,7 @@ public static Entity getEntity(String faction, logger.warn(String.format("Unable to randomly generate %s %s with roles: %s." + " Second chance generation also failed.", EntityWeightClass.getClassName(params.getWeightClass()), - UnitType.getTypeName(unitType), + getTypeName(unitType), params.getMissionRoles().stream().map(Enum::name).collect(Collectors.joining(",")))); } else { return secondChanceEntity; @@ -1611,7 +1650,7 @@ public static Entity getTankEntity(UnitGeneratorParameters params, if (!params.getMissionRoles().isEmpty()) { logger.warn(String.format("Unable to randomly generate %s %s with roles: %s", EntityWeightClass.getClassName(params.getWeightClass()), - UnitType.getTypeName(UnitType.TANK), + getTypeName(TANK), params.getMissionRoles().stream().map(Enum::name).collect(Collectors.joining(",")))); } return null; @@ -1662,7 +1701,7 @@ public static Entity getInfantryEntity(UnitGeneratorParameters params, if (unitData == null) { if (!params.getMissionRoles().isEmpty()) { logger.warn(String.format("Unable to randomly generate %s with roles: %s", - UnitType.getTypeName(UnitType.INFANTRY), + getTypeName(INFANTRY), params.getMissionRoles().stream().map(Enum::name).collect(Collectors.joining(",")))); } return null; @@ -1749,10 +1788,10 @@ public static List fillTransports(AtBScenario scenario, // Don't bother processing if various non-useful conditions are present if (transports == null || transports.isEmpty() || - transports.stream().map(Entity::getUnitType).allMatch(curType -> curType != UnitType.TANK && - curType != UnitType.VTOL && - curType != UnitType.NAVAL && - curType != UnitType.CONV_FIGHTER)) { + transports.stream().map(Entity::getUnitType).allMatch(curType -> curType != TANK && + curType != VTOL && + curType != NAVAL && + curType != CONV_FIGHTER)) { return new ArrayList<>(); } @@ -1762,16 +1801,16 @@ public static List fillTransports(AtBScenario scenario, if (requiredRoles != null) { - transportedRoles.put(UnitType.INFANTRY, - requiredRoles.containsKey(UnitType.INFANTRY) ? new ArrayList<>(requiredRoles.get(UnitType.INFANTRY)) + transportedRoles.put(INFANTRY, + requiredRoles.containsKey(INFANTRY) ? new ArrayList<>(requiredRoles.get(INFANTRY)) : new ArrayList<>()); - transportedRoles.get(UnitType.INFANTRY).remove((MissionRole.ARTILLERY)); + transportedRoles.get(INFANTRY).remove((MissionRole.ARTILLERY)); - transportedRoles.put(UnitType.BATTLE_ARMOR, - requiredRoles.containsKey(UnitType.BATTLE_ARMOR) - ? new ArrayList<>(requiredRoles.get(UnitType.BATTLE_ARMOR)) + transportedRoles.put(BATTLE_ARMOR, + requiredRoles.containsKey(BATTLE_ARMOR) + ? new ArrayList<>(requiredRoles.get(BATTLE_ARMOR)) : new ArrayList<>()); - transportedRoles.get(UnitType.BATTLE_ARMOR).remove((MissionRole.ARTILLERY)); + transportedRoles.get(BATTLE_ARMOR).remove((MissionRole.ARTILLERY)); } List transportedUnits = new ArrayList<>(); @@ -1784,10 +1823,10 @@ public static List fillTransports(AtBScenario scenario, // Only check unit types that can have an infantry bay for (Entity transport : transports) { - if (IntStream.of(UnitType.TANK, - UnitType.VTOL, - UnitType.NAVAL, - UnitType.CONV_FIGHTER).anyMatch(i -> transport.getUnitType() == i)) { + if (IntStream.of(TANK, + VTOL, + NAVAL, + CONV_FIGHTER).anyMatch(i -> transport.getUnitType() == i)) { transportedUnits.addAll(fillTransport(scenario, transport, params, @@ -1852,7 +1891,7 @@ private static List fillTransport(AtBScenario scenario, // If a roll against the battle armor target number succeeds, try to generate a // battle armor unit first if (Compute.d6(2) >= infantryToBAUpgradeTNs[params.getQuality()]) { - newParams.setMissionRoles(requiredRoles.getOrDefault(UnitType.BATTLE_ARMOR, new HashSet<>())); + newParams.setMissionRoles(requiredRoles.getOrDefault(BATTLE_ARMOR, new HashSet<>())); transportedUnit = generateTransportedBAUnit(newParams, bayCapacity, skill, false, campaign); // If the transporter has both bay space and is an omni unit, try to add a @@ -1866,8 +1905,8 @@ private static List fillTransport(AtBScenario scenario, // If a battle armor unit wasn't generated and conditions permit, try generating // conventional infantry. Generate air assault infantry for VTOL transports. if (transportedUnit == null && allowInfantry) { - newParams.setMissionRoles(requiredRoles.getOrDefault(UnitType.INFANTRY, new HashSet<>())); - if (transport.getUnitType() == UnitType.VTOL + newParams.setMissionRoles(requiredRoles.getOrDefault(INFANTRY, new HashSet<>())); + if (transport.getUnitType() == VTOL && !newParams.getMissionRoles().contains(MissionRole.XCT)) { UnitGeneratorParameters paratrooperParams = newParams.clone(); paratrooperParams.addMissionRole(MissionRole.PARATROOPER); @@ -1937,7 +1976,7 @@ private static Entity generateTransportedInfantryUnit(UnitGeneratorParameters pa Campaign campaign) { UnitGeneratorParameters newParams = params.clone(); - newParams.setUnitType(UnitType.INFANTRY); + newParams.setUnitType(INFANTRY); MekSummary unitData; boolean temporaryXCT = false; UnitGeneratorParameters noXCTParams; @@ -2043,7 +2082,7 @@ private static Entity generateTransportedBAUnit(UnitGeneratorParameters params, } UnitGeneratorParameters newParams = params.clone(); - newParams.setUnitType(UnitType.BATTLE_ARMOR); + newParams.setUnitType(BATTLE_ARMOR); newParams.getMovementModes().addAll(IUnitGenerator.ALL_BATTLE_ARMOR_MODES); @@ -2226,7 +2265,7 @@ private static Entity getEntityByName(String name, String factionCode, SkillLeve Gender gender; int nonBinaryDiceSize = campaign.getCampaignOptions().getNonBinaryDiceSize(); - if ((nonBinaryDiceSize > 0) && (Compute.randomInt(nonBinaryDiceSize) == 0)) { + if ((nonBinaryDiceSize > 0) && (randomInt(nonBinaryDiceSize) == 0)) { gender = RandomGenderGenerator.generateOther(); } else { gender = RandomGenderGenerator.generate(); @@ -2260,30 +2299,30 @@ private static Entity getEntityByName(String name, String factionCode, SkillLeve if (faction.isClan() && (Compute.d6(2) > (6 - skill.ordinal() + skills[0] + skills[1]))) { Phenotype phenotype = Phenotype.NONE; switch (en.getUnitType()) { - case UnitType.MEK: + case MEK: phenotype = Phenotype.MEKWARRIOR; break; - case UnitType.TANK: - case UnitType.VTOL: + case TANK: + case VTOL: // The Vehicle Phenotype is unique to Clan Hell's Horses if (faction.getShortName().equals("CHH")) { phenotype = Phenotype.VEHICLE; } break; - case UnitType.BATTLE_ARMOR: + case BATTLE_ARMOR: phenotype = Phenotype.ELEMENTAL; break; - case UnitType.AEROSPACEFIGHTER: - case UnitType.CONV_FIGHTER: + case AEROSPACEFIGHTER: + case CONV_FIGHTER: phenotype = Phenotype.AEROSPACE; break; - case UnitType.PROTOMEK: + case PROTOMEK: phenotype = Phenotype.PROTOMEK; break; - case UnitType.SMALL_CRAFT: - case UnitType.DROPSHIP: - case UnitType.JUMPSHIP: - case UnitType.WARSHIP: + case SMALL_CRAFT: + case DROPSHIP: + case JUMPSHIP: + case WARSHIP: // The Naval Phenotype is unique to Clan Snow Raven and the Raven Alliance if (faction.getShortName().equals("CSR") || faction.getShortName().equals("RA")) { phenotype = Phenotype.NAVAL; @@ -2397,9 +2436,7 @@ private static String adjustWeightsForFaction(String weights, String faction) { * II, or similar * tactical formation. * TODO: generate ProtoMek points when Clan mixed stars are called for - * TODO: generate Clan mixed nova stars e.g. two points of Meks, two of - * vehicles, one ProtoMek - * point + * TODO: generate Clan mixed nova stars e.g. two points of Meks, two of vehicles, one ProtoMek point * * @param unitTypeCode The type of units to generate, also accepts * SPECIAL_UNIT_TYPE_ATB_MIX for @@ -2418,6 +2455,7 @@ private static List generateUnitTypes(int unitTypeCode, int forceQuality, String factionCode, boolean allowTanks, + boolean allowConventionalAircraft, Campaign campaign) { List unitTypes = new ArrayList<>(unitCount); int actualUnitType = unitTypeCode; @@ -2429,8 +2467,7 @@ private static List generateUnitTypes(int unitTypeCode, Faction faction = Factions.getInstance().getFaction(factionCode); // If ground vehicles are permitted in general and by environmental conditions, - // and - // for Clans if this is a Clan faction, then use them. Otherwise, only use Meks. + // and for Clans if this is a Clan faction, then use them. Otherwise, only use Meks. if (campaign.getCampaignOptions().isUseVehicles() && allowTanks && (!faction.isClan() || @@ -2471,28 +2508,40 @@ private static List generateUnitTypes(int unitTypeCode, // Roll for unit types if (totalWeight <= 0) { - actualUnitType = UnitType.MEK; + for (int x = 0; x < unitCount; x++) { + unitTypes.addAll(checkForProtoMek(faction, campaign)); + } + + return unitTypes; } else { - int roll = Compute.randomInt(totalWeight); + int roll = randomInt(totalWeight); if (roll < vehicleLanceWeight) { - actualUnitType = UnitType.TANK; - // Mixed units randomly select between Mek or ground vehicle + actualUnitType = TANK; + // Mixed units randomly select between Mek, ProtoMek, or ground vehicle } else if (roll < vehicleLanceWeight + mixedLanceWeight) { for (int x = 0; x < unitCount; x++) { - boolean addTank = Compute.randomInt(2) == 0; + boolean addTank = randomInt(2) == 0; if (addTank) { - unitTypes.add(UnitType.TANK); + unitTypes.add(TANK); } else { - unitTypes.add(UnitType.MEK); + unitTypes.addAll(checkForProtoMek(faction, campaign)); } } return unitTypes; } else { - actualUnitType = UnitType.MEK; + for (int x = 0; x < unitCount; x++) { + unitTypes.addAll(checkForProtoMek(faction, campaign)); + } + + return unitTypes; } } } else { - actualUnitType = UnitType.MEK; + for (int x = 0; x < unitCount; x++) { + unitTypes.addAll(checkForProtoMek(faction, campaign)); + } + + return unitTypes; } } else if (unitTypeCode == SPECIAL_UNIT_TYPE_ATB_CIVILIANS) { // Use the Vehicle/Mixed ratios from campaign options as weighted values for @@ -2504,24 +2553,75 @@ private static List generateUnitTypes(int unitTypeCode, // Roll for unit types if (totalWeight <= 0) { - actualUnitType = UnitType.TANK; + actualUnitType = TANK; } else { - int roll = Compute.randomInt(totalWeight); + int roll = randomInt(totalWeight); if (roll < vehicleLanceWeight) { - actualUnitType = UnitType.TANK; + actualUnitType = TANK; // Mixed units randomly select between Mek or ground vehicle } else { for (int x = 0; x < unitCount; x++) { - boolean addTank = Compute.randomInt(2) == 0; + boolean addTank = randomInt(2) == 0; if (addTank) { - unitTypes.add(UnitType.TANK); + unitTypes.add(TANK); } else { - unitTypes.add(UnitType.MEK); + unitTypes.add(MEK); } } return unitTypes; } } + } else if (unitTypeCode == SPECIAL_UNIT_TYPE_ATB_AERO_MIX) { + Faction faction = Factions.getInstance().getFaction(factionCode); + + if (campaign.getCampaignOptions().isUseVehicles() && allowConventionalAircraft + && (!faction.isClan() || (faction.isClan() && campaign.getCampaignOptions().isClanVehicles()))) { + + // Use the Mek/Vehicle/Mixed ratios from campaign options as weighted values for + // random unit types. + // Then modify based on faction. + int aeroFlightWeight = campaign.getCampaignOptions().getOpForLanceTypeVehicles(); + int mixedFlightWeight = campaign.getCampaignOptions().getOpForLanceTypeMixed(); + int conventionalLanceWeight = campaign.getCampaignOptions().getOpForLanceTypeMeks(); + + if (faction.isClan()) { + aeroFlightWeight += 2; + mixedFlightWeight = 0; + conventionalLanceWeight = Math.max(0, conventionalLanceWeight - 2); + } else if (faction.isMinorPower() || faction.isPirate()) { + aeroFlightWeight = Math.max(0, aeroFlightWeight - 1); + mixedFlightWeight++; + conventionalLanceWeight++; + } + + int totalWeight = aeroFlightWeight + mixedFlightWeight + conventionalLanceWeight; + + // Roll for unit types + if (totalWeight <= 0) { + actualUnitType = AEROSPACEFIGHTER; + } else { + int roll = randomInt(totalWeight); + + if (roll < conventionalLanceWeight) { + actualUnitType = CONV_FIGHTER; + // Mixed units randomly select between Aerospace or Conventional Fighter + } else if (roll < conventionalLanceWeight + mixedFlightWeight) { + for (int x = 0; x < unitCount; x++) { + boolean addConventional = randomInt(2) == 0; + if (addConventional) { + unitTypes.add(CONV_FIGHTER); + } else { + unitTypes.add(AEROSPACEFIGHTER); + } + } + return unitTypes; + } else { + actualUnitType = AEROSPACEFIGHTER; + } + } + } else { + actualUnitType = AEROSPACEFIGHTER; + } } // Add unit types to the list of actual unity types @@ -2532,6 +2632,33 @@ private static List generateUnitTypes(int unitTypeCode, return unitTypes; } + /** + * Checks if the given faction is a Clan faction, if the current game year is 3057 or greater, + * and if a random integer between 0 and 99 inclusive is less than 6. If all these conditions + * are met, the method returns a list containing five instances of the PROTOMEK constant. If not, + * it creates a new list with a single instance of the MEK constant. + * + * @param faction the Faction to check for Clan-ness + * @param campaign the current Campaign, used to get the current game year + * + * @return List of PROTOMEK constants if all conditions are met, otherwise a list containing MEK + */ + private static List checkForProtoMek(Faction faction, Campaign campaign) { + List unitTypes = new ArrayList<>(); + if (faction.isClan() && (campaign.getGameYear() >= 3057) && (randomInt(100) < 6)) { + // There are five ProtoMeks to a Point + for (int i = 0; i < 5; i++) { + unitTypes.add(PROTOMEK); + } + } + + if (unitTypes.isEmpty()) { + unitTypes.add(MEK); + } + + return unitTypes; + } + /** * Generates a selection of unit types, typically for a Clan star of five * points. May generate @@ -2563,8 +2690,13 @@ private static List generateClanUnitTypes(int unitCount, // Random determination of Mek or ground vehicle int roll = Compute.d6(2); - int unitType = campaign.getCampaignOptions().isClanVehicles() && (roll <= vehicleTarget) ? UnitType.TANK - : UnitType.MEK; + int unitType = campaign.getCampaignOptions().isClanVehicles() && (roll <= vehicleTarget) ? TANK + : MEK; + + if ((campaign.getGameYear() >= 3057) && (randomInt(100) < 6)) { + unitType = PROTOMEK; + } + List unitTypes = new ArrayList<>(); for (int x = 0; x < unitCount; x++) { unitTypes.add(unitType); @@ -2638,7 +2770,7 @@ private static List generateClanUnitTypes(int unitCount, weights = adjustForMinWeight(weights, minWeight); // Aerospace fighter weight cap - if (unitTypes.contains(UnitType.AEROSPACEFIGHTER)) { + if (unitTypes.contains(AEROSPACEFIGHTER)) { weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_HEAVY); } @@ -2648,27 +2780,27 @@ private static List generateClanUnitTypes(int unitCount, for (int curType : requiredRoles.keySet()) { if (requiredRoles.get(curType).contains(MissionRole.RECON)) { - if (curType == UnitType.MEK || curType == UnitType.PROTOMEK) { + if (curType == MEK || curType == PROTOMEK) { weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_MEDIUM); } } if (requiredRoles.get(curType).contains(MissionRole.APC)) { - if (curType == UnitType.TANK || curType == UnitType.VTOL) { + if (curType == TANK || curType == VTOL) { weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_MEDIUM); } } if (requiredRoles.get(curType).contains(MissionRole.CAVALRY)) { - if (curType == UnitType.MEK) { + if (curType == MEK) { weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_HEAVY); - } else if (curType == UnitType.TANK || curType == UnitType.PROTOMEK) { + } else if (curType == TANK || curType == PROTOMEK) { weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_MEDIUM); } } if (requiredRoles.get(curType).contains(MissionRole.RAIDER)) { - if (curType == UnitType.MEK || curType == UnitType.PROTOMEK) { + if (curType == MEK || curType == PROTOMEK) { weights = adjustForMaxWeight(weights, EntityWeightClass.WEIGHT_HEAVY); } } @@ -2816,7 +2948,7 @@ private static double getDifficultyMultiplier(Campaign campaign) { * @return the randomly generated {@link EntityWeightClass} */ public static int randomForceWeight() { - int roll = Compute.randomInt(89); + int roll = randomInt(89); // These values are based the random force weight table found on page 265 of Total Warfare if (roll < 19) { // 19% @@ -2906,7 +3038,7 @@ private static List generateLance(String faction, SkillLevel skill, int if (newEntity == null) { logger.info(String.format("Failed to generate unit of type %s, weight %s. Beginning substitution.", - UnitType.getTypeName(unitTypes.get(i)), + getTypeName(unitTypes.get(i)), EntityWeightClass.getClassName(AtBConfiguration.decodeWeightStr(weights, i)))); // If we've failed to get an entity, we start adjusting weight categories to see @@ -2933,9 +3065,9 @@ private static List generateLance(String faction, SkillLevel skill, int if (newEntity == null) { logger.info("Substitution unsuccessful. Using hardcoded fallbacks"); - if (unitTypes.get(0) == UnitType.DROPSHIP) { - newEntity = getNewEntity(faction, skill, quality, List.of(UnitType.DROPSHIP), - weights, Map.of(UnitType.DROPSHIP, List.of(CIVILIAN)), + if (unitTypes.get(0) == DROPSHIP) { + newEntity = getNewEntity(faction, skill, quality, List.of(DROPSHIP), + weights, Map.of(DROPSHIP, List.of(CIVILIAN)), campaign, 0); if (newEntity != null) { @@ -2943,12 +3075,19 @@ private static List generateLance(String faction, SkillLevel skill, int } } else { if (scenario.getBoardType() == T_GROUND && allowsTanks) { - newEntity = getNewEntity(faction, skill, quality, List.of(UnitType.TANK), + newEntity = getNewEntity(faction, skill, quality, List.of(TANK), weights, null, campaign, 0); if (newEntity != null) { logger.info("Substitution successful. Substituted with Tank."); } + } else { + newEntity = getNewEntity(faction, skill, quality, List.of(AEROSPACEFIGHTER), + weights, null, campaign, 0); + + if (newEntity != null) { + logger.info("Substitution successful. Substituted with Aerospace Fighter."); + } } } @@ -3069,7 +3208,7 @@ public static int calculateDeploymentZone(ScenarioForceTemplate forceTemplate, A // if we got in here without a force template somehow, just return a random // start zone if (forceTemplate == null) { - return Compute.randomInt(Board.START_CENTER); + return randomInt(Board.START_CENTER); // if we have a specific calculated deployment zone already } else if (forceTemplate.getActualDeploymentZone() != Board.START_NONE) { return forceTemplate.getActualDeploymentZone(); @@ -3078,7 +3217,7 @@ public static int calculateDeploymentZone(ScenarioForceTemplate forceTemplate, A } else if (forceTemplate.getSyncDeploymentType() == SynchronizedDeploymentType.None || Objects.equals(forceTemplate.getSyncedForceName(), originalForceTemplateID)) { calculatedEdge = forceTemplate.getDeploymentZones() - .get(Compute.randomInt(forceTemplate.getDeploymentZones().size())); + .get(randomInt(forceTemplate.getDeploymentZones().size())); } else if (forceTemplate.getSyncDeploymentType() == SynchronizedDeploymentType.SameEdge) { calculatedEdge = calculateDeploymentZone( scenario.getTemplate().getScenarioForces().get(forceTemplate.getSyncedForceName()), scenario, @@ -3093,13 +3232,13 @@ public static int calculateDeploymentZone(ScenarioForceTemplate forceTemplate, A scenario.getTemplate().getScenarioForces().get(forceTemplate.getSyncedForceName()), scenario, originalForceTemplateID); List arc = getArc(syncDeploymentZone, true); - calculatedEdge = arc.get(Compute.randomInt(arc.size())); + calculatedEdge = arc.get(randomInt(arc.size())); } else if (forceTemplate.getSyncDeploymentType() == SynchronizedDeploymentType.OppositeArc) { int syncDeploymentZone = calculateDeploymentZone( scenario.getTemplate().getScenarioForces().get(forceTemplate.getSyncedForceName()), scenario, originalForceTemplateID); List arc = getArc(syncDeploymentZone, false); - calculatedEdge = arc.get(Compute.randomInt(arc.size())); + calculatedEdge = arc.get(randomInt(arc.size())); } if (calculatedEdge == ScenarioForceTemplate.DEPLOYMENT_ZONE_NARROW_EDGE) { @@ -3113,7 +3252,7 @@ public static int calculateDeploymentZone(ScenarioForceTemplate forceTemplate, A edges.add(Board.START_S); } - calculatedEdge = edges.get(Compute.randomInt(2)); + calculatedEdge = edges.get(randomInt(2)); } forceTemplate.setActualDeploymentZone(calculatedEdge); @@ -3137,7 +3276,7 @@ public static void setDestinationZone(BotForce force, ScenarioForceTemplate forc if (forceTemplate.getDestinationZone() == ScenarioForceTemplate.DESTINATION_EDGE_RANDOM) { // compute a random cardinal edge between 0 and 3 to avoid None - actualDestinationEdge = Compute.randomInt(CardinalEdge.values().length - 1); + actualDestinationEdge = randomInt(CardinalEdge.values().length - 1); } else if (forceTemplate.getDestinationZone() == ScenarioForceTemplate.DESTINATION_EDGE_OPPOSITE_DEPLOYMENT) { actualDestinationEdge = getOppositeEdge(force.getStartingPos()); } else { @@ -3564,58 +3703,18 @@ public static int getLanceSize(String factionCode) { /** * Worker function to determine the formation size of fixed wing aircraft. - * Directly calling for - * aerospace fighters will return a single flight/point size, normally 2 except - * for CC which - * uses 3 per flight. Conventional fighters return 1-3 flights/2-6 total. The - * SPECIAL_UNIT_TYPE_ATB_AERO_MIX unit type randomly returns an aerospace flight - * or conventional - * squadron. - * - * @param unitTypeCode type of unit may be aerospace, conventional, or ATB - * 'special' - * @param isPlanetOwner true if the generating faction controls the system, - * which is required - * to generate conventional fighters - * @param factionCode Short name of faction + * @param faction The faction spawning the force * @return Number of fighters to use as a formation size */ - public static int getAeroLanceSize(int unitTypeCode, boolean isPlanetOwner, String factionCode) { - int numFightersPerFlight = factionCode.equals("CC") ? 3 : 2; - - // If this is the planet owner, it may generate a full squadron of conventional - // fighters - int useASFRoll = isPlanetOwner ? Compute.d6() : 6; - int weightCountRoll = (Compute.randomInt(3) + 1) * numFightersPerFlight; - return getAeroLanceSize(unitTypeCode, numFightersPerFlight, weightCountRoll, useASFRoll); - } - - /** - * Unwrapped inner logic of above function to be deterministic, for testing - * purposes. - * - * @param unitTypeCode {@link UnitType} value, should be - * AEROSPACEFIGHTER, - * CONV_FIGHTER, or SPECIAL_UNIT_TYPE_ATB_AERO_MIX. - * @param numFightersPerFlight Number of fighters per flight/point, typically 2 - * @param weightCountRoll Number of fighters per squadron/star, typically 6 - * or 10 - * @param useASFRoll test value for dynamic generation of aerospace or - * conventional - * @return flight size for aerospace, squadron size for conventional - */ - public static int getAeroLanceSize(int unitTypeCode, int numFightersPerFlight, int weightCountRoll, - int useASFRoll) { - if (unitTypeCode == UnitType.AEROSPACEFIGHTER) { - return numFightersPerFlight; - } else if (unitTypeCode == UnitType.CONV_FIGHTER) { - return weightCountRoll; + public static int getAeroLanceSize(Faction faction) { + if (faction.isClan()) { + return 10; + } else if (faction.isComStarOrWoB()) { + return 6; + } else if (faction.getShortName().equals("CC")) { + return randomInt(2) == 0 ? 3 : 2; } else { - // if we are the planet owner, we may use ASF or conventional fighters - boolean useASF = useASFRoll >= 4; - // if we are using ASF, we "always" use 2 at a time, otherwise, use the # of - // conventional fighters - return useASF ? numFightersPerFlight : weightCountRoll; + return 2; } } @@ -3626,8 +3725,8 @@ public static int getAeroLanceSize(int unitTypeCode, int numFightersPerFlight, i * @param entityList */ private static void deployArtilleryOffBoard(List entityList) { - OffBoardDirection direction = OffBoardDirection.getDirection(Compute.randomInt(4)); - int distance = (Compute.randomInt(2) + 1) * 17; + OffBoardDirection direction = OffBoardDirection.getDirection(randomInt(4)); + int distance = (randomInt(2) + 1) * 17; for (Entity entity : entityList) { entity.setOffBoard(distance, direction); diff --git a/MekHQ/unittests/mekhq/campaign/mission/DynamicScenarioFactoryTest.java b/MekHQ/unittests/mekhq/campaign/mission/DynamicScenarioFactoryTest.java index d2a8097a03..749cf4326d 100644 --- a/MekHQ/unittests/mekhq/campaign/mission/DynamicScenarioFactoryTest.java +++ b/MekHQ/unittests/mekhq/campaign/mission/DynamicScenarioFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2020-2024 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -19,15 +19,13 @@ package mekhq.campaign.mission; import megamek.common.Board; -import megamek.common.Compute; -import megamek.common.UnitType; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests relevant to the AtBDynamicScenarioFactory - * + * * @author NickAragua */ public class DynamicScenarioFactoryTest { @@ -57,61 +55,4 @@ public void testGetOppositeEdge() { startingEdge = Board.START_NW; assertEquals(Board.START_SE, AtBDynamicScenarioFactory.getOppositeEdge(startingEdge)); } - - private void testAeroLanceSizeInner(int unitTypeCode, int numFightersPerFlight, boolean isPlanetOwner) { - int weightCountRoll = (Compute.randomInt(3) + 1) * numFightersPerFlight; - int useASFRoll = isPlanetOwner ? Compute.d6() : 6; - int expected; - switch (unitTypeCode) { - case UnitType.AEROSPACEFIGHTER: - expected = numFightersPerFlight; - break; - case UnitType.CONV_FIGHTER: - expected = weightCountRoll; - break; - default: - expected = (useASFRoll >= 4) ? numFightersPerFlight : weightCountRoll; - } - - assertEquals(expected, AtBDynamicScenarioFactory.getAeroLanceSize(unitTypeCode, numFightersPerFlight, - weightCountRoll, useASFRoll)); - } - - @Test - public void testAeroLanceSize() { - assertEquals(2, AtBDynamicScenarioFactory.getAeroLanceSize(UnitType.AEROSPACEFIGHTER, true, "FC")); - assertEquals(3, AtBDynamicScenarioFactory.getAeroLanceSize(UnitType.AEROSPACEFIGHTER, true, "CC")); - assertEquals(2, - AtBDynamicScenarioFactory.getAeroLanceSize(ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX, false, - "FC")); - assertEquals(3, - AtBDynamicScenarioFactory.getAeroLanceSize(ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX, false, - "CC")); - - // Roll some "random" values and check inner function return values - int unitTypeCode = UnitType.AEROSPACEFIGHTER; - int numFightersPerFlight = 2; - boolean isPlanetOwner = false; - testAeroLanceSizeInner(unitTypeCode, numFightersPerFlight, isPlanetOwner); - isPlanetOwner = true; - testAeroLanceSizeInner(unitTypeCode, numFightersPerFlight, isPlanetOwner); - numFightersPerFlight = 3; - isPlanetOwner = false; - testAeroLanceSizeInner(unitTypeCode, numFightersPerFlight, isPlanetOwner); - isPlanetOwner = true; - testAeroLanceSizeInner(unitTypeCode, numFightersPerFlight, isPlanetOwner); - - unitTypeCode = UnitType.CONV_FIGHTER; - numFightersPerFlight = 2; - isPlanetOwner = false; - testAeroLanceSizeInner(unitTypeCode, numFightersPerFlight, isPlanetOwner); - testAeroLanceSizeInner(unitTypeCode, numFightersPerFlight, isPlanetOwner); - isPlanetOwner = true; - testAeroLanceSizeInner(unitTypeCode, numFightersPerFlight, isPlanetOwner); - numFightersPerFlight = 3; - isPlanetOwner = false; - testAeroLanceSizeInner(unitTypeCode, numFightersPerFlight, isPlanetOwner); - isPlanetOwner = true; - testAeroLanceSizeInner(unitTypeCode, numFightersPerFlight, isPlanetOwner); - } }