Skip to content

Commit 9acbd11

Browse files
authored
Merge pull request #7933 from psikomonkie/pr/intensity-variance
PR: Adjust Intensity Based on Variance
2 parents 287ce20 + 8891d02 commit 9acbd11

File tree

9 files changed

+371
-89
lines changed

9 files changed

+371
-89
lines changed

MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java

Lines changed: 56 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
*/
3333
package mekhq.campaign.market.contractMarket;
3434

35+
import static java.lang.Math.max;
3536
import static java.lang.Math.min;
3637
import static megamek.common.compute.Compute.d6;
3738
import static megamek.common.enums.SkillLevel.REGULAR;
@@ -54,11 +55,14 @@
5455
import megamek.logging.MMLogger;
5556
import mekhq.campaign.Campaign;
5657
import mekhq.campaign.campaignOptions.CampaignOptions;
58+
import mekhq.campaign.force.CombatTeam;
59+
import mekhq.campaign.force.Force;
5760
import mekhq.campaign.market.enums.ContractMarketMethod;
5861
import mekhq.campaign.mission.AtBContract;
5962
import mekhq.campaign.mission.Contract;
6063
import mekhq.campaign.mission.Mission;
6164
import mekhq.campaign.mission.enums.AtBContractType;
65+
import mekhq.campaign.mission.enums.CombatRole;
6266
import mekhq.campaign.mission.enums.ContractCommandRights;
6367
import mekhq.campaign.mission.utilities.ContractUtilities;
6468
import mekhq.campaign.rating.IUnitRating;
@@ -86,12 +90,6 @@ public abstract class AbstractContractMarket {
8690
private static final int MERCENARY_THRESHOLD_LIAISON = 12;
8791
private static final int NON_MERCENARY_THRESHOLD = 12;
8892

89-
/**
90-
* The portion of combat teams we expect to be performing combat actions. This is one in 'x' where 'x' is the value
91-
* set here.
92-
*/
93-
static final double BASE_VARIANCE_FACTOR = 0.7;
94-
9593

9694
protected List<Contract> contracts = new ArrayList<>();
9795
protected int lastId = 0;
@@ -260,10 +258,12 @@ protected void updateReport(Campaign campaign) {
260258
* @param campaign the campaign containing relevant options and faction information
261259
* @param contract the contract that specifies details such as subcontract status
262260
* @param bypassVariance a flag indicating whether variance adjustments should be bypassed
261+
* @param varianceFactor the degree of variance to apply to required combat elements
263262
*
264263
* @return the calculated number of required units in combat teams, ensuring it meets game rules and constraints
265264
*/
266-
public int calculateRequiredCombatElements(Campaign campaign, AtBContract contract, boolean bypassVariance) {
265+
public int calculateRequiredCombatElements(Campaign campaign, AtBContract contract, boolean bypassVariance,
266+
double varianceFactor) {
267267
// Return 1 combat team if the contract is a subcontract
268268
if (contract.isSubcontract()) {
269269
return 1;
@@ -274,13 +274,9 @@ public int calculateRequiredCombatElements(Campaign campaign, AtBContract contra
274274

275275
// If bypassing variance, apply flat reduction (reduce force by 1/3)
276276
if (bypassVariance) {
277-
return Math.max(effectiveForces - calculateBypassVarianceReduction(effectiveForces), 1);
277+
return max(effectiveForces - calculateBypassVarianceReduction(effectiveForces), 1);
278278
}
279279

280-
// Apply variance based on a die roll
281-
int varianceRoll = d6(2);
282-
double varianceFactor = calculateVarianceFactor(varianceRoll);
283-
284280
// Adjust available forces based on variance, ensuring minimum clamping
285281
int adjustedForces = (int) Math.floor((double) effectiveForces * varianceFactor);
286282

@@ -293,43 +289,60 @@ public int calculateRequiredCombatElements(Campaign campaign, AtBContract contra
293289
}
294290

295291
/**
296-
* Calculates the variance factor based on the given roll value and a fixed formation size divisor.
292+
* Calculates the required number of combat teams (intensity) for a contract based on campaign options, contract
293+
* details, and variance factors.
297294
*
298-
* <p>
299-
* The variance factor is determined by applying a multiplier to the fixed formation size divisor. The multiplier
300-
* varies based on the roll value:
295+
* <p>This method determines the number of combat elements needed to deploy, taking into account factors such
296+
* as:</p>
301297
* <ul>
302-
* <li><b>Roll 2:</b> Multiplier is 0.575.</li>
303-
* <li><b>Roll 3:</b> Multiplier is 0.6.</li>
304-
* <li><b>Roll 4:</b> Multiplier is 0.625</li>
305-
* <li><b>Roll 5:</b> Multiplier is 0.65.</li>
306-
* <li><b>Roll 6:</b> Multiplier is 0.675.</li>
307-
* <li><b>Roll 7:</b> Multiplier is 0.7.</li>
308-
* <li><b>Roll 7:</b> Multiplier is 0.725.</li>
309-
* <li><b>Roll 7:</b> Multiplier is 0.75.</li>
310-
* <li><b>Roll 7:</b> Multiplier is 0.775.</li>
311-
* <li><b>Roll 7:</b> Multiplier is 0.8.</li>
312-
* <li><b>Roll 7:</b> Multiplier is 0.825.</li>
298+
* <li>Whether the contract is a subcontract (returns 1 as a base case).</li>
299+
* <li>The effective unit forces.</li>
300+
* <li>Whether variance bypass is enabled, applying a flat reduction to available forces.</li>
301+
* <li>Variance adjustments applied through a die roll, affecting the availability of forces.</li>
313302
* </ul>
314303
*
315-
* @param roll the roll value used to determine the multiplier
304+
* <p>The method ensures values are clamped to maintain a minimum deployment of at least 1 combat element while
305+
* not exceeding the maximum deployable combat elements.</p>
306+
*
307+
* @param campaign the campaign containing relevant options and faction information
308+
* @param contract the contract that specifies details such as subcontract status
309+
* @param bypassVariance a flag indicating whether variance adjustments should be bypassed
310+
* @param varianceFactor the degree of variance to apply to required combat elements
311+
*
312+
* @return the calculated number of required units in combat teams, ensuring it meets game rules and constraints
316313
*
317-
* @return the calculated variance factor as a double
314+
* @since 0.50.10
318315
*/
319-
private double calculateVarianceFactor(int roll) {
320-
return switch (roll) {
321-
case 2 -> BASE_VARIANCE_FACTOR - 0.125;
322-
case 3 -> BASE_VARIANCE_FACTOR - 0.1;
323-
case 4 -> BASE_VARIANCE_FACTOR - 0.075;
324-
case 5 -> BASE_VARIANCE_FACTOR - 0.05;
325-
case 6 -> BASE_VARIANCE_FACTOR - 0.025;
326-
case 8 -> BASE_VARIANCE_FACTOR + 0.025;
327-
case 9 -> BASE_VARIANCE_FACTOR + 0.05;
328-
case 10 -> BASE_VARIANCE_FACTOR + 0.075;
329-
case 11 -> BASE_VARIANCE_FACTOR + 0.1;
330-
case 12 -> BASE_VARIANCE_FACTOR + 0.125;
331-
default -> BASE_VARIANCE_FACTOR; // 7
332-
};
316+
public int calculateRequiredCombatTeams(Campaign campaign, AtBContract contract, boolean bypassVariance,
317+
double varianceFactor) {
318+
// Return 1 combat team if the contract is a subcontract
319+
if (contract.isSubcontract()) {
320+
return 1;
321+
}
322+
323+
// Calculate base formation size and effective unit force
324+
int effectCombatTeams = 0;
325+
for (Map.Entry<Integer, CombatTeam> combatTeam : campaign.getCombatTeamsAsMap().entrySet()) {
326+
Force force = campaign.getForce(combatTeam.getKey());
327+
if (force != null) {
328+
CombatRole combatRoleInMemory = force.getCombatRoleInMemory();
329+
if (combatRoleInMemory != CombatRole.TRAINING) {
330+
effectCombatTeams++;
331+
}
332+
}
333+
}
334+
335+
// If bypassing variance, apply flat reduction (reduce force by 1/3)
336+
if (bypassVariance) {
337+
return max(effectCombatTeams - calculateBypassVarianceReduction(effectCombatTeams), 1);
338+
}
339+
340+
// Adjust available forces based on variance, ensuring minimum clamping
341+
int adjustedCombatTeams = (int) Math.floor((double) effectCombatTeams * varianceFactor);
342+
adjustedCombatTeams = max(adjustedCombatTeams, 1);
343+
344+
// Return the clamped value, ensuring it does not exceed max-deployable forces
345+
return Math.min(adjustedCombatTeams, effectCombatTeams);
333346
}
334347

335348
/**

MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,10 @@ private static void checkForEmployerOverride(LocalDate today, AtBContract contra
477477
contract.calculateLength(campaign.getCampaignOptions().isVariableContractLength());
478478
setContractClauses(contract, unitRatingMod, campaign);
479479

480+
double varianceFactor = ContractUtilities.calculateVarianceFactor();
480481
contract.setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,
481-
contract.getContractType().isCadreDuty()));
482-
contract.setRequiredCombatElements(calculateRequiredCombatElements(campaign, contract, false));
482+
contract.getContractType().isCadreDuty(), false, varianceFactor));
483+
contract.setRequiredCombatElements(calculateRequiredCombatElements(campaign, contract, false, varianceFactor));
483484
contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));
484485

485486
contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel());
@@ -577,9 +578,11 @@ protected AtBContract generateAtBSubcontract(Campaign campaign, AtBContract pare
577578
}
578579
contract.setTransportComp(100);
579580

581+
double varianceFactor = ContractUtilities.calculateVarianceFactor();
580582
contract.setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,
581-
contract.getContractType().isCadreDuty()));
582-
contract.setRequiredCombatElements(calculateRequiredCombatElements(campaign, contract, false));
583+
contract.getContractType().isCadreDuty(), false, varianceFactor));
584+
contract.setRequiredCombatElements(calculateRequiredCombatElements(campaign, contract, false, varianceFactor));
585+
583586
contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));
584587
contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel());
585588
contract.calculateContract(campaign);
@@ -651,9 +654,12 @@ private void addFollowup(Campaign campaign, AtBContract contract) {
651654
followup.setAllyQuality(contract.getAllyQuality());
652655
followup.calculateLength(campaign.getCampaignOptions().isVariableContractLength());
653656
setContractClauses(followup, campaign.getAtBUnitRatingMod(), campaign);
657+
658+
double varianceFactor = ContractUtilities.calculateVarianceFactor();
654659
followup.setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,
655-
contract.getContractType().isCadreDuty()));
656-
contract.setRequiredCombatElements(calculateRequiredCombatElements(campaign, contract, false));
660+
followup.getContractType().isCadreDuty(), false, varianceFactor));
661+
followup.setRequiredCombatElements(calculateRequiredCombatElements(campaign, followup, false, varianceFactor));
662+
657663
followup.setMultiplier(calculatePaymentMultiplier(campaign, followup));
658664
followup.setPartsAvailabilityLevel(followup.getContractType().calculatePartsAvailabilityLevel());
659665
followup.initContractDetails(campaign);

MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,10 @@ private Optional<AtBContract> generateContract(Campaign campaign, ReputationCont
281281
// Step 6: Determine the initial contract clauses
282282
setContractClauses(contract, contractTerms);
283283
// Step 7: Determine the number of required lances (Not CamOps RAW)
284+
double varianceFactor = ContractUtilities.calculateVarianceFactor();
284285
contract.setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,
285-
contract.getContractType().isCadreDuty()));
286-
contract.setRequiredCombatElements(calculateRequiredCombatElements(campaign, contract, false));
286+
contract.getContractType().isCadreDuty(), false, varianceFactor));
287+
contract.setRequiredCombatElements(calculateRequiredCombatElements(campaign, contract, false, varianceFactor));
287288
// Step 8: Calculate the payment
288289
contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));
289290
// Step 9: Determine parts availability

MekHQ/src/mekhq/campaign/mission/AtBContract.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,13 +365,13 @@ public void calculateLength(final boolean variable) {
365365
*
366366
* @return The number of lances required.
367367
*
368-
* @deprecated use {@link ContractUtilities#calculateBaseNumberOfRequiredLances(Campaign, boolean)}
368+
* @deprecated use {@link ContractUtilities#calculateBaseNumberOfRequiredLances(Campaign, boolean, boolean, double)}
369369
* <p>
370370
* Calculates the number of lances required for this contract, based on [campaign].
371371
*/
372372
@Deprecated(since = "0.50.07", forRemoval = true)
373373
public static int calculateBaseNumberOfRequiredLances(Campaign campaign) {
374-
return ContractUtilities.calculateBaseNumberOfRequiredLances(campaign, false);
374+
return ContractUtilities.calculateBaseNumberOfRequiredLances(campaign, false, true, 1.0);
375375
}
376376

377377
/**
@@ -1567,7 +1567,7 @@ public AtBContract(Contract contract, Campaign campaign) {
15671567
}
15681568

15691569
setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,
1570-
contractType.isCadreDuty()));
1570+
contractType.isCadreDuty(), true, 1.0));
15711571
setRequiredCombatElements(ContractUtilities.calculateBaseNumberOfUnitsRequiredInCombatTeams(campaign));
15721572

15731573
setPartsAvailabilityLevel(getContractType().calculatePartsAvailabilityLevel());

MekHQ/src/mekhq/campaign/mission/utilities/ContractUtilities.java

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333

3434
package mekhq.campaign.mission.utilities;
3535

36+
import static java.lang.Math.ceil;
3637
import static java.lang.Math.floor;
3738
import static java.lang.Math.max;
39+
import static megamek.common.compute.Compute.d6;
3840
import static mekhq.campaign.force.ForceType.STANDARD;
3941

4042
import mekhq.campaign.Campaign;
@@ -43,16 +45,24 @@
4345
import mekhq.campaign.mission.enums.CombatRole;
4446

4547
public class ContractUtilities {
48+
/**
49+
* The portion of combat teams we expect to be performing combat actions. This is one in 'x' where 'x' is the value
50+
* set here.
51+
*/
52+
static final double BASE_VARIANCE_FACTOR = 0.7;
53+
4654
/**
4755
* Calculates the number of lances used for this contract, based on [campaign].
4856
*
49-
* @param campaign The campaign to reference.
50-
* @param isCadreDuty {@code true} if {@link CombatRole#CADRE} should be considered a combat role
57+
* @param campaign The campaign to reference.
58+
* @param isCadreDuty {@code true} if {@link CombatRole#CADRE} should be considered a combat role
59+
* @param bypassVariance a flag indicating whether variance adjustments should be bypassed
60+
* @param varianceFactor the degree of variance to apply to required combat elements
5161
*
5262
* @return The number of lances required.
5363
*/
54-
public static int calculateBaseNumberOfRequiredLances(Campaign campaign, boolean isCadreDuty) {
55-
64+
public static int calculateBaseNumberOfRequiredLances(Campaign campaign, boolean isCadreDuty,
65+
boolean bypassVariance, double varianceFactor) {
5666
int combatForceCount = 0;
5767
for (CombatTeam combatTeam : campaign.getCombatTeamsAsList()) {
5868
if (0 >= combatTeam.getSize(campaign)) { // Don't count empty combat teams (or warship-only)
@@ -71,7 +81,11 @@ public static int calculateBaseNumberOfRequiredLances(Campaign campaign, boolean
7181
}
7282
}
7383

74-
return max(combatForceCount, 1);
84+
if (bypassVariance) {
85+
return max(combatForceCount, 1);
86+
} else {
87+
return (int) ceil(max(combatForceCount * varianceFactor, 1));
88+
}
7589
}
7690

7791
/**
@@ -120,4 +134,43 @@ public static int getEffectiveNumUnits(Campaign campaign) {
120134

121135
return (int) floor(numUnits);
122136
}
137+
138+
/**
139+
* Calculates the variance factor based on the given roll value and a fixed formation size divisor.
140+
*
141+
* <p>
142+
* The variance factor is determined by applying a multiplier to the fixed formation size divisor. The multiplier
143+
* varies based on the roll value:
144+
* <ul>
145+
* <li><b>Roll 2:</b> Multiplier is 0.575.</li>
146+
* <li><b>Roll 3:</b> Multiplier is 0.6.</li>
147+
* <li><b>Roll 4:</b> Multiplier is 0.625</li>
148+
* <li><b>Roll 5:</b> Multiplier is 0.65.</li>
149+
* <li><b>Roll 6:</b> Multiplier is 0.675.</li>
150+
* <li><b>Roll 7:</b> Multiplier is 0.7.</li>
151+
* <li><b>Roll 8:</b> Multiplier is 0.725.</li>
152+
* <li><b>Roll 9:</b> Multiplier is 0.75.</li>
153+
* <li><b>Roll 10:</b> Multiplier is 0.775.</li>
154+
* <li><b>Roll 11:</b> Multiplier is 0.8.</li>
155+
* <li><b>Roll 12:</b> Multiplier is 0.825.</li>
156+
* </ul>
157+
*
158+
* @return the calculated variance factor as a double
159+
*/
160+
public static double calculateVarianceFactor() {
161+
int roll = d6(2);
162+
return switch (roll) {
163+
case 2 -> BASE_VARIANCE_FACTOR - 0.125;
164+
case 3 -> BASE_VARIANCE_FACTOR - 0.1;
165+
case 4 -> BASE_VARIANCE_FACTOR - 0.075;
166+
case 5 -> BASE_VARIANCE_FACTOR - 0.05;
167+
case 6 -> BASE_VARIANCE_FACTOR - 0.025;
168+
case 8 -> BASE_VARIANCE_FACTOR + 0.025;
169+
case 9 -> BASE_VARIANCE_FACTOR + 0.05;
170+
case 10 -> BASE_VARIANCE_FACTOR + 0.075;
171+
case 11 -> BASE_VARIANCE_FACTOR + 0.1;
172+
case 12 -> BASE_VARIANCE_FACTOR + 0.125;
173+
default -> BASE_VARIANCE_FACTOR; // 0.7
174+
};
175+
}
123176
}

MekHQ/src/mekhq/gui/dialog/NewAtBContractDialog.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -568,14 +568,14 @@ protected void btnOKActionPerformed(ActionEvent evt) {
568568
contract.setDesc(txtDesc.getText());
569569
contract.setCommandRights(choiceCommand.getSelectedItem());
570570

571-
contract.setRequiredCombatTeams(ContractUtilities.calculateBaseNumberOfRequiredLances(campaign,
572-
contract.getContractType().isCadreDuty()));
573571

574572
AbstractContractMarket contractMarket = campaign.getContractMarket();
575573
if (contractMarket != null) {
576-
contract.setRequiredCombatElements(contractMarket.calculateRequiredCombatElements(campaign,
577-
contract,
578-
false));
574+
double varianceFactor = ContractUtilities.calculateVarianceFactor();
575+
contract.setRequiredCombatTeams(contractMarket.calculateRequiredCombatTeams(campaign, contract, false,
576+
varianceFactor));
577+
contract.setRequiredCombatElements(contractMarket.calculateRequiredCombatElements(campaign, contract,
578+
false, varianceFactor));
579579
} else {
580580
contract.setRequiredCombatElements(0); // This shouldn't happen, but let's not crash if it does
581581
}

0 commit comments

Comments
 (0)