Skip to content

Commit

Permalink
Merge pull request #5664 from IllianiCBT/requiredForceFix
Browse files Browse the repository at this point in the history
Updated & Fixed Required Combat Team Calculations; Reworked Combat Team Requirement Contract Pay Modifiers; Added Contract Difficulty Modifier
  • Loading branch information
HammerGS authored Jan 15, 2025
2 parents 92a314a + 073c9d1 commit 4d9c830
Show file tree
Hide file tree
Showing 16 changed files with 226 additions and 117 deletions.
4 changes: 2 additions & 2 deletions MekHQ/src/mekhq/campaign/Campaign.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
13 changes: 0 additions & 13 deletions MekHQ/src/mekhq/campaign/CampaignOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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")) {
Expand Down
17 changes: 17 additions & 0 deletions MekHQ/src/mekhq/campaign/force/CombatTeam.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
* <p>
* 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.
* </p>
*
* @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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<Contract> contracts = new ArrayList<>();
protected int lastId = 0;
Expand Down Expand Up @@ -182,64 +187,130 @@ protected void updateReport(Campaign campaign) {
}

/**
* Determines the number of required lances to be deployed for a contract. For Mercenary subcontracts
* this defaults to 1; otherwise, the number is based on the number of combat units in the
* campaign. Modified by a 2d6 roll if {@code bypassVariance} is {@code false}.
* Calculates the required number of combat teams for a contract based on campaign options,
* contract details, and variance factors.
*
* @param campaign the current campaign
* @param contract the relevant contract
* @param bypassVariance if {@code true} requirements will not be semi-randomized.
* @return The number of lances required to be deployed.
* <p>
* This method determines the number of combat teams needed to deploy, taking into account factors such as:
* <ul>
* <li>Whether the contract is a subcontract (returns 1 as a base case).</li>
* <li>The base formation sizes (lance-level and company-level) and the effective unit forces.</li>
* <li>The maximum deployable combat teams, adjusted based on campaign strategy options.</li>
* <li>Whether variance bypass is enabled, applying a flat reduction to available forces.</li>
* <li>Variance adjustments applied through a dice roll, affecting the availability of forces.</li>
* </ul>
* The method ensures values are clamped to maintain a minimum deployment of at least 1 combat
* team while not exceeding the maximum deployable combat teams.
*
* @param campaign the campaign containing relevant options and faction information
* @param contract the contract that specifies details such as subcontract status
* @param bypassVariance a flag indicating whether variance adjustments should be bypassed
* @return the calculated number of required combat teams, ensuring it meets game rules and constraints
*/
public int calculateRequiredLances(Campaign campaign, AtBContract contract, boolean bypassVariance) {
public int calculateRequiredCombatTeams(Campaign campaign, AtBContract contract, boolean bypassVariance) {
// Return 1 combat team if the contract is a subcontract
if (contract.isSubcontract()) {
return 1;
}

int formationSize = getStandardForceSize(campaign.getFaction());
int availableForces = max(getEffectiveNumUnits(campaign) / formationSize, 1);
int maxDeployedLances = availableForces;
// Calculate base formation size and effective unit force
Faction faction = campaign.getFaction();
int lanceLevelFormationSize = getStandardForceSize(faction);

int effectiveForces = Math.max(getEffectiveNumUnits(campaign) / lanceLevelFormationSize, 1);

// Calculate maximum deployed forces based on strategy options
int maxDeployableCombatTeams = effectiveForces;
if (campaign.getCampaignOptions().isUseStrategy()) {
maxDeployedLances = max(calculateMaxDeployedLances(campaign), 1);
maxDeployableCombatTeams = Math.max(calculateMaxDeployableCombatTeams(campaign), 1);
}

availableForces = min(availableForces, maxDeployedLances);
// Clamp available forces to the max deployable limit
int availableForces = Math.min(effectiveForces, maxDeployableCombatTeams);

// If we're bypassing variance, we can early exit here
// If bypassing variance, apply flat reduction (reduce force by 1/3)
if (bypassVariance) {
availableForces -= (int) floor((double) availableForces / 3);
return Math.max(availableForces - calculateBypassVarianceReduction(availableForces), 1);
}

// Apply variance based on a die roll
int varianceRoll = d6(2);
double varianceFactor = calculateVarianceFactor(varianceRoll);

return max(availableForces, 1);
// Adjust available forces based on variance, ensuring minimum clamping
int adjustedForces = availableForces - (int) Math.floor((double) availableForces / varianceFactor);

if (adjustedForces < 1) {
adjustedForces = 1;
}

// Otherwise, we roll to determine the amount we divide availableForces by
int roll = d6(2);
double varianceFactor = switch (roll) {
case 2 -> 4.5;
case 3 -> 4;
case 4 -> 3.5;
case 10 -> 2.5;
case 11 -> 2;
case 12 -> 1.5;
default -> 3;
};
// Return the clamped value, ensuring it does not exceed max-deployable forces
return Math.min(adjustedForces, maxDeployableCombatTeams);
}

availableForces -= (int) floor((double) availableForces / varianceFactor);
/**
* Calculates the variance factor based on the given roll value and a fixed formation size divisor.
*
* <p>
* The variance factor is determined by applying a multiplier to the fixed formation size divisor.
* The multiplier varies based on the roll value:
* <ul>
* <li><b>Roll 2:</b> Multiplier is 0.75.</li>
* <li><b>Roll 3:</b> Multiplier is 0.5.</li>
* <li><b>Roll 4:</b> Multiplier is 0.25.</li>
* <li><b>Rolls 10, 11, 12:</b> Multipliers are 1.25, 1.5, and 1.75 respectively.</li>
* <li><b>Rolls 5-9:</b> Default multiplier is 1.0 (no change).</li>
* </ul>
*
* @param roll the roll value used to determine the multiplier
* @return the calculated variance factor as a double
*/
private double calculateVarianceFactor(int roll) {
return switch (roll) {
case 2 -> COMBAT_FORCE_DIVIDER * 0.25;
case 3 -> COMBAT_FORCE_DIVIDER * 0.5;
case 4 -> COMBAT_FORCE_DIVIDER * 0.75;
case 10 -> COMBAT_FORCE_DIVIDER * 1.25;
case 11 -> COMBAT_FORCE_DIVIDER * 1.5;
case 12 -> COMBAT_FORCE_DIVIDER * 1.75;
default -> COMBAT_FORCE_DIVIDER; // 5-9
};
}

return max(availableForces, 1);
/**
* Calculates the bypass variance reduction based on the available forces.
*
* <p>
* 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.
* </p>
*
* @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.
*
* <p>
* 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.
* </p>
*
* @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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ private Optional<AtBContract> generateContract(Campaign campaign, ReputationCont
// Step 6: Determine the initial contract clauses
setContractClauses(contract, contractTerms);
// Step 7: Determine the number of required lances (Not CamOps RAW)
contract.setRequiredLances(calculateRequiredLances(campaign, contract, false));
contract.setRequiredCombatTeams(calculateRequiredCombatTeams(campaign, contract, false));
// Step 8: Calculate the payment
contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));
// Step 9: Determine parts availability
Expand Down
Loading

0 comments on commit 4d9c830

Please sign in to comment.