diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 83a4007dce..560a80fafa 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -3833,8 +3833,8 @@ public int getDeploymentDeficit(AtBContract contract) { return 0; } - int total = -contract.getRequiredLances(); - int role = -max(1, contract.getRequiredLances() / 2); + int total = -contract.getRequiredCombatTeams(); + int role = -max(1, contract.getRequiredCombatTeams() / 2); int minimumUnitCount = (int) ((double) getStandardForceSize(faction) / 2); final CombatRole requiredLanceRole = contract.getContractType().getRequiredLanceRole(); diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 7c434f1326..baf86b0420 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -570,7 +570,6 @@ public static String getTransitUnitName(final int unit) { private boolean useStrategy; private int baseStrategyDeployment; private int additionalStrategyDeployment; - private boolean adjustPaymentForStrategy; private final int[] atbBattleChance; private boolean generateChases; @@ -1207,7 +1206,6 @@ public CampaignOptions() { useStrategy = true; baseStrategyDeployment = 3; additionalStrategyDeployment = 1; - adjustPaymentForStrategy = false; atbBattleChance = new int[CombatRole.values().length - 1]; atbBattleChance[CombatRole.MANEUVER.ordinal()] = 40; atbBattleChance[CombatRole.FRONTLINE.ordinal()] = 20; @@ -4569,14 +4567,6 @@ public void setAdditionalStrategyDeployment(final int additionalStrategyDeployme this.additionalStrategyDeployment = additionalStrategyDeployment; } - public boolean isAdjustPaymentForStrategy() { - return adjustPaymentForStrategy; - } - - public void setAdjustPaymentForStrategy(final boolean adjustPaymentForStrategy) { - this.adjustPaymentForStrategy = adjustPaymentForStrategy; - } - public boolean isRestrictPartsByMission() { return restrictPartsByMission; } @@ -5207,7 +5197,6 @@ public void writeToXml(final PrintWriter pw, int indent) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useStrategy", useStrategy); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "baseStrategyDeployment", baseStrategyDeployment); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "additionalStrategyDeployment", additionalStrategyDeployment); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "adjustPaymentForStrategy", adjustPaymentForStrategy); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "restrictPartsByMission", restrictPartsByMission); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "limitLanceWeight", limitLanceWeight); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "limitLanceNumUnits", limitLanceNumUnits); @@ -6237,8 +6226,6 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve retVal.baseStrategyDeployment = Integer.parseInt(wn2.getTextContent().trim()); } else if (wn2.getNodeName().equalsIgnoreCase("additionalStrategyDeployment")) { retVal.additionalStrategyDeployment = Integer.parseInt(wn2.getTextContent().trim()); - } else if (wn2.getNodeName().equalsIgnoreCase("adjustPaymentForStrategy")) { - retVal.adjustPaymentForStrategy = Boolean.parseBoolean(wn2.getTextContent().trim()); } else if (wn2.getNodeName().equalsIgnoreCase("restrictPartsByMission")) { retVal.restrictPartsByMission = Boolean.parseBoolean(wn2.getTextContent().trim()); } else if (wn2.getNodeName().equalsIgnoreCase("limitLanceWeight")) { diff --git a/MekHQ/src/mekhq/campaign/force/CombatTeam.java b/MekHQ/src/mekhq/campaign/force/CombatTeam.java index d1dd6a608f..aca540879a 100644 --- a/MekHQ/src/mekhq/campaign/force/CombatTeam.java +++ b/MekHQ/src/mekhq/campaign/force/CombatTeam.java @@ -25,6 +25,7 @@ import megamek.common.Entity; import megamek.common.EntityWeightClass; import megamek.common.Infantry; +import megamek.common.annotations.Nullable; import megamek.logging.MMLogger; import mekhq.MekHQ; import mekhq.campaign.Campaign; @@ -745,4 +746,20 @@ private static void recalculateSubForceStrategicStatus(Campaign campaign, recalculateSubForceStrategicStatus(campaign, campaign.getCombatTeamsTable(), force); } } + + /** + * Retrieves the force associated with the given campaign using the stored force ID. + * + *
+ * This method returns a {@link Force} object corresponding to the stored {@code forceId}, + * if it exists within the specified campaign. If no matching force is found, {@code null} + * is returned. + *
+ * + * @param campaign the campaign containing the forces to search for the specified {@code forceId} + * @return the {@link Force} object associated with the {@code forceId}, or {@code null} if not found + */ + public @Nullable Force getForce(Campaign campaign) { + return campaign.getForce(forceId); + } } diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java index 34cf7c4f4a..5247afa3a7 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java @@ -5,6 +5,7 @@ import megamek.common.enums.SkillLevel; import megamek.logging.MMLogger; import mekhq.campaign.Campaign; +import mekhq.campaign.CampaignOptions; import mekhq.campaign.market.enums.ContractMarketMethod; import mekhq.campaign.mission.AtBContract; import mekhq.campaign.mission.Contract; @@ -22,8 +23,6 @@ import java.io.PrintWriter; import java.util.*; -import static java.lang.Math.floor; -import static java.lang.Math.max; import static java.lang.Math.min; import static megamek.common.Compute.d6; import static megamek.common.enums.SkillLevel.REGULAR; @@ -42,6 +41,12 @@ public abstract class AbstractContractMarket { public static final int CLAUSE_TRANSPORT = 3; public static final int CLAUSE_NUM = 4; + /** + * The portion of combat teams we expect to be performing combat actions. + * This is one in 'x' where 'x' is the value set here. + */ + static final double COMBAT_FORCE_DIVIDER = 2; + protected List+ * This method determines the number of combat teams needed to deploy, taking into account factors such as: + *
+ * The variance factor is determined by applying a multiplier to the fixed formation size divisor. + * The multiplier varies based on the roll value: + *
+ * The reduction is calculated by dividing the available forces by a fixed factor of 3 + * and rounding down to the nearest whole number. This value is used in scenarios where + * variance adjustments are bypassed. + *
+ * + * @param availableForces the total number of forces available + * @return the bypass variance reduction as an integer + */ + private int calculateBypassVarianceReduction(int availableForces) { + return (int) Math.floor((double) availableForces / 3); } /** - * Determine the maximum number of lances the force can deploy. The result is based on the - * commander's Strategy skill and various campaign options. - * @param campaign - * @return the maximum number of lances that can be deployed on the contract. + * Calculates the maximum number of deployable combat teams based on the given campaign's options. + * + *+ * This method retrieves campaign options and calculates the total deployable combat teams using + * the base strategy deployment, additional strategy deployment, and the campaign's commander strategy. + *
+ * + * @param campaign the campaign object containing the necessary data to perform the calculation + * @return the total number of deployable combat teams */ - public int calculateMaxDeployedLances(Campaign campaign) { - return campaign.getCampaignOptions().getBaseStrategyDeployment() + - campaign.getCampaignOptions().getAdditionalStrategyDeployment() * - campaign.getCommanderStrategy(); + public int calculateMaxDeployableCombatTeams(Campaign campaign) { + CampaignOptions options = campaign.getCampaignOptions(); + int baseStrategyDeployment = options.getBaseStrategyDeployment(); + int additionalStrategyDeployment = options.getAdditionalStrategyDeployment(); + int commanderStrategy = campaign.getCommanderStrategy(); + + return baseStrategyDeployment + additionalStrategyDeployment * commanderStrategy; } protected SkillLevel getSkillRating(int roll) { diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java index 9dcac0d8bf..0e8d040760 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java @@ -298,7 +298,7 @@ private void checkForSubcontracts(Campaign campaign, AtBContract contract, int u contract.calculateLength(campaign.getCampaignOptions().isVariableContractLength()); setContractClauses(contract, unitRatingMod, campaign); - contract.setRequiredLances(calculateRequiredLances(campaign, contract, false)); + contract.setRequiredCombatTeams(calculateRequiredCombatTeams(campaign, contract, false)); contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel()); @@ -392,7 +392,7 @@ protected AtBContract generateAtBSubcontract(Campaign campaign, } contract.setTransportComp(100); - contract.setRequiredLances(calculateRequiredLances(campaign, contract, false)); + contract.setRequiredCombatTeams(calculateRequiredCombatTeams(campaign, contract, false)); contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel()); contract.calculateContract(campaign); @@ -434,7 +434,7 @@ private void addFollowup(Campaign campaign, followup.calculateLength(campaign.getCampaignOptions().isVariableContractLength()); setContractClauses(followup, campaign.getAtBUnitRatingMod(), campaign); - contract.setRequiredLances(calculateRequiredLances(campaign, contract, false)); + contract.setRequiredCombatTeams(calculateRequiredCombatTeams(campaign, contract, false)); contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); followup.setPartsAvailabilityLevel(followup.getContractType().calculatePartsAvailabilityLevel()); @@ -484,16 +484,27 @@ public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract multiplier *= 1.1; } - int baseRequiredLances = calculateRequiredLances(campaign, contract, true); - int requiredLances = contract.getRequiredLances(); + // Adjust pay based on the percentage of the players' forces required by the contract + int requiredCombatTeams = contract.getRequiredCombatTeams(); + double totalCombatTeams = campaign.getAllCombatTeams().size(); + totalCombatTeams /= COMBAT_FORCE_DIVIDER; - multiplier *= (double) requiredLances / baseRequiredLances; + if (totalCombatTeams > 0) { + multiplier *= (double) requiredCombatTeams / totalCombatTeams; + } + + // Adjust pay based on difficulty if FG3 is enabled + if (campaign.getCampaignOptions().isUseGenericBattleValue()) { + double skulls = contract.calculateContractDifficulty(campaign); + skulls -= 5; // 5 skulls (or 2.5) is equivalent to the player force, so no modifier. - int maxDeployedLances = calculateMaxDeployedLances(campaign); - if (requiredLances > maxDeployedLances && campaign.getCampaignOptions().isAdjustPaymentForStrategy()) { - multiplier *= (double) maxDeployedLances / (double) requiredLances; + if (skulls != 0) { + skulls *= 0.05; // each half-skull is a 5% pay change + multiplier *= (1 + skulls); + } } + return multiplier; } diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java index 062369afa8..fbdd73ae22 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java @@ -204,7 +204,7 @@ private Optional+ * This method iterates through all combat teams in the specified campaign, ignoring combat teams with + * the auxiliary role. For each valid combat team, it retrieves the associated force and evaluates + * all units within that force. The unit contribution to the total is determined based on its type: + *
+ * Units that aren’t associated with a valid combat team or can’t be fetched due to missing + * data are ignored. The final result is returned as an integer by flooring the calculated total. + *
+ * + * @param campaign the campaign containing the combat teams and units to evaluate + * @return the effective number of units as an integer + */ public static int getEffectiveNumUnits(Campaign campaign) { double numUnits = 0; - for (UUID uuid : campaign.getForces().getAllUnits(true)) { - if (null == campaign.getUnit(uuid)) { + for (CombatTeam combatTeam : campaign.getAllCombatTeams()) { + Force force = combatTeam.getForce(campaign); + + if (force == null) { continue; } - switch (campaign.getUnit(uuid).getEntity().getUnitType()) { - case MEK: - numUnits += 1; - break; - case UnitType.TANK: - case UnitType.VTOL: - case UnitType.NAVAL: - numUnits += campaign.getFaction().isClan() ? 0.5 : 1; - break; - case UnitType.CONV_FIGHTER: - case UnitType.AEROSPACEFIGHTER: - if (campaign.getCampaignOptions().isUseAero()) { - numUnits += campaign.getFaction().isClan() ? 0.5 : 1; - } - break; - case UnitType.PROTOMEK: - numUnits += 0.2; - break; - case UnitType.BATTLE_ARMOR: - case UnitType.INFANTRY: - default: - /* don't count */ + + for (UUID unitId : force.getAllUnits(true)) { + Entity entity = getEntityFromUnitId(campaign, unitId); + + if (entity == null) { + continue; + } + + numUnits += switch (entity.getUnitType()) { + case UnitType.TANK, UnitType.VTOL, UnitType.NAVAL, UnitType.CONV_FIGHTER, + UnitType.AEROSPACEFIGHTER + -> campaign.getFaction().isClan() ? 0.5 : 1; + case UnitType.PROTOMEK -> 0.2; + case UnitType.BATTLE_ARMOR, UnitType.INFANTRY -> 0; + default -> 1; // All other unit types + }; } } - return (int) numUnits; + + return (int) floor(numUnits); } public static boolean isMinorPower(final String factionCode) { @@ -1133,7 +1156,7 @@ protected int writeToXMLBegin(final PrintWriter pw, int indent) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, "enemyCamoFileName", getEnemyCamouflage().getFilename()); } MHQXMLUtility.writeSimpleXMLTag(pw, indent, "enemyColour", getEnemyColour().name()); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "requiredLances", getRequiredLances()); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "requiredCombatTeams", getRequiredCombatTeams()); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "moraleLevel", getMoraleLevel().name()); if (routEnd != null) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, "routEnd", routEnd); @@ -1211,8 +1234,10 @@ public void loadFieldsFromXmlNode(Node wn) throws ParseException { getEnemyCamouflage().setFilename(wn2.getTextContent().trim()); } else if (wn2.getTextContent().equalsIgnoreCase("enemyColour")) { setEnemyColour(PlayerColour.parseFromString(wn2.getTextContent().trim())); - } else if (wn2.getNodeName().equalsIgnoreCase("requiredLances")) { - requiredLances = Integer.parseInt(wn2.getTextContent()); + // <50.03 compatibility handler + } else if (wn2.getNodeName().equalsIgnoreCase("requiredLances") + || wn2.getNodeName().equalsIgnoreCase("requiredCombatTeams")) { + requiredCombatTeams = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("moraleLevel")) { setMoraleLevel(AtBMoraleLevel.parseFromString(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("routEnd")) { @@ -1435,12 +1460,12 @@ public void setEnemyColour(PlayerColour enemyColour) { this.enemyColour = Objects.requireNonNull(enemyColour); } - public int getRequiredLances() { - return requiredLances; + public int getRequiredCombatTeams() { + return requiredCombatTeams; } - public void setRequiredLances(int required) { - requiredLances = required; + public void setRequiredCombatTeams(int required) { + requiredCombatTeams = required; } public int getPartsAvailabilityLevel() { @@ -1605,7 +1630,7 @@ public AtBContract(Contract c, Campaign campaign) { enemyCode = "REB"; } - requiredLances = calculateRequiredLances(campaign); + requiredCombatTeams = calculateRequiredLances(campaign); setPartsAvailabilityLevel(getContractType().calculatePartsAvailabilityLevel()); diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java index 14adb1e019..561634fe0b 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java @@ -394,7 +394,7 @@ static int calculateTargetCargoTonnage(Campaign campaign, AtBContract contract) // Next, we determine the tonnage cap. This is the maximum tonnage the employer is willing to support. final int INDIVIDUAL_TONNAGE_ALLOWANCE = 80; // This is how many tons the employer will budget per unit final int formationSize = getStandardForceSize(campaign.getFaction()); - final int tonnageCap = contract.getRequiredLances() * formationSize * INDIVIDUAL_TONNAGE_ALLOWANCE; + final int tonnageCap = contract.getRequiredCombatTeams() * formationSize * INDIVIDUAL_TONNAGE_ALLOWANCE; // Then we determine the size of each individual 'drop'. This uses the lowest of // unitTonnage and tonnageCap and divides that by 100 diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/StarLeagueCache.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/StarLeagueCache.java index ead77fe370..370298bd0b 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/StarLeagueCache.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/StarLeagueCache.java @@ -127,7 +127,7 @@ public boolean didGenerationFail() { private void processUnits() { int intactUnitCount = 0; - for (int lance = 0; lance < contract.getRequiredLances(); lance++) { + for (int lance = 0; lance < contract.getRequiredCombatTeams(); lance++) { // This will generate a number between 1 and 4 with an average roll of 3 intactUnitCount += Compute.randomInt(3) + Compute.randomInt(3); } diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java b/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java index aa7435e082..232ef47265 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java @@ -75,7 +75,7 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai // scenarios // when objective is allied/hostile facility, place those facilities - int maximumTrackIndex = Math.max(0, contract.getRequiredLances() / NUM_LANCES_PER_TRACK); + int maximumTrackIndex = Math.max(0, contract.getRequiredCombatTeams() / NUM_LANCES_PER_TRACK); int planetaryTemperature = campaign.getLocation().getPlanet().getTemperature(campaign.getLocalDate()); for (int x = 0; x < maximumTrackIndex; x++) { @@ -93,7 +93,7 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai // a campaign will have X tracks going at a time, where // X = # required lances / 3, rounded up. The last track will have fewer // required lances. - int oddLanceCount = contract.getRequiredLances() % NUM_LANCES_PER_TRACK; + int oddLanceCount = contract.getRequiredCombatTeams() % NUM_LANCES_PER_TRACK; if (oddLanceCount > 0) { int scenarioOdds = contractDefinition.getScenarioOdds() .get(Compute.randomInt(contractDefinition.getScenarioOdds().size())); @@ -109,7 +109,7 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai // now seed the tracks with objectives and facilities for (ObjectiveParameters objectiveParams : contractDefinition.getObjectiveParameters()) { int objectiveCount = objectiveParams.objectiveCount > 0 ? (int) objectiveParams.objectiveCount - : (int) Math.max(1, -objectiveParams.objectiveCount * contract.getRequiredLances()); + : (int) Math.max(1, -objectiveParams.objectiveCount * contract.getRequiredCombatTeams()); List