Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Centralized Formation Size Logic & Adjusted Contract Required Lance Count #5280

Merged
merged 3 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ lblTransport.text=Transport Terms:
lblSalvageRights.text=Salvage Rights:
lblStraightSupport.text=Straight Support:
lblBattleLossComp.text=Battle Loss Compensation:
lblRequiredLances.text=Required Lances:
lblRequiredLances.text=Required Combat Forces:

lblRenegotiate.text=Renegotiate

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ lblAllyBotName.text=Ally Bot Name:
lblEnemyBotName.text=Enemy Bot Name:
lblAllyCamo.text=Ally Camo
lblEnemyCamo.text=Enemy Camo
lblRequiredLances.text=Required Lances:
lblRequiredLances.text=Required Combat Forces:
lblEnemyMorale.text=Enemy Morale:
lblContractScoreArbitraryModifier.text=Contract Score Modifier:
lblBasePay.text=Base Pay:
Expand Down
52 changes: 42 additions & 10 deletions MekHQ/src/mekhq/campaign/force/StrategicFormation.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import static megamek.common.EntityWeightClass.WEIGHT_ULTRA_LIGHT;
import static mekhq.campaign.force.Force.STRATEGIC_FORMATION_OVERRIDE_NONE;
import static mekhq.campaign.force.Force.STRATEGIC_FORMATION_OVERRIDE_TRUE;
import static mekhq.campaign.force.FormationLevel.LANCE;

/**
* Used by Against the Bot & StratCon to track additional information about each force
Expand All @@ -64,9 +65,9 @@
public class StrategicFormation {
private static final MMLogger logger = MMLogger.create(StrategicFormation.class);

public static final int STR_IS = 4;
public static final int STR_CLAN = 5;
public static final int STR_CS = 6;
public static final int LANCE_SIZE = 4;
public static final int STAR_SIZE = 5;
public static final int LEVEL_II_SIZE = 6;

public static final long ETYPE_GROUND = ETYPE_MEK |
ETYPE_TANK | Entity.ETYPE_INFANTRY | ETYPE_PROTOMEK;
Expand All @@ -81,20 +82,51 @@ public class StrategicFormation {

/**
* Determines the standard size for a given faction. The size varies depending on whether the
* faction is a Clan, ComStar/WoB, or others (Inner Sphere).
* faction is a Clan, ComStar/WoB, or others (Inner Sphere). This overloaded method defaults to
* Lance/Star/Level II
*
* @param faction The {@link Faction} object for which the standard force size is to be calculated.
* @return The standard force size for the given faction. It returns {@code STR_CLAN} if the
* faction is a Clan, {@code STR_CS} if the faction is ComStar or WoB, and {@code STR_IS} otherwise.
* @return The standard force size, at the provided formation level, for the provided faction
*/
public static int getStandardForceSize(Faction faction) {
if (faction.isClan()) {
return STR_CLAN;
return getStandardForceSize(faction, LANCE.getDepth());
}

/**
* Determines the standard size for a given faction. The size varies depending on whether the
* faction is a Clan, ComStar/WoB, or others (Inner Sphere).
*
* @param faction The {@link Faction} object for which the standard force size is to be calculated.
* @param formationLevelDepth The {@link FormationLevel} {@code Depth} from which the standard
* force size is to be calculated.
* @return The standard force size, at the provided formation level, for the provided faction
*/
public static int getStandardForceSize(Faction faction, int formationLevelDepth) {
int formationSize;
if (faction.isClan() || faction.isMarianHegemony()) {
formationSize = STAR_SIZE;
} else if (faction.isComStarOrWoB()) {
return STR_CS;
formationSize = LEVEL_II_SIZE;
} else {
return STR_IS;
formationSize = LANCE_SIZE;
}

if (formationLevelDepth == LANCE.getDepth()) {
return formationSize;
}

formationLevelDepth++; // Lance is depth 0, so we need to add +1 to get the number of iterations

for (int i = 0; i < formationLevelDepth; i++) {

if (faction.isComStarOrWoB()) {
formationSize *= 6;
} else {
formationSize *= 3;
}
}

return formationSize;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import megamek.Version;
import megamek.codeUtilities.MathUtility;
import megamek.common.Compute;
import megamek.common.enums.SkillLevel;
import megamek.logging.MMLogger;
import mekhq.campaign.Campaign;
Expand All @@ -22,6 +21,13 @@
import java.io.PrintWriter;
import java.util.*;

import static java.lang.Math.floor;
import static java.lang.Math.max;
import static java.lang.Math.round;
import static megamek.common.Compute.d6;
import static mekhq.campaign.force.StrategicFormation.getStandardForceSize;
import static mekhq.campaign.mission.AtBContract.getEffectiveNumUnits;

/**
* Abstract base class for various Contract Market types in AtB/Stratcon. Responsible for generation
* and initialization of AtBContracts.
Expand Down Expand Up @@ -174,18 +180,43 @@ 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.
* @param campaign
* @param contract
* 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}.
* @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.
*/
public int calculateRequiredLances(Campaign campaign, AtBContract contract) {
int maxDeployedLances = calculateMaxDeployedLances(campaign);
public int calculateRequiredLances(Campaign campaign, AtBContract contract, boolean bypassVariance) {
int maxDeployedLances = max(calculateMaxDeployedLances(campaign), 1);
if (contract.isSubcontract()) {
return 1;
} else {
int requiredLances = Math.max(AtBContract.getEffectiveNumUnits(campaign) / 6, 1);
return Math.min(requiredLances, maxDeployedLances);
int formationSize = getStandardForceSize(campaign.getFaction());
int availableForces = max(getEffectiveNumUnits(campaign) / formationSize, 1);

// We allow for one reserve force per 3 depth 0 forces (lances, etc)
availableForces -= max((int) floor((double) availableForces / 3), 1);

if (!bypassVariance) {
int roll = d6(2);

if (roll == 2) {
availableForces = (int) round((double) availableForces * 0.25);
} else if (roll == 3) {
availableForces = (int) round((double) availableForces * 0.5);
} else if (roll < 5) {
availableForces = (int) round((double) availableForces * 0.75);
} else if (roll == 12) {
availableForces = (int) round((double) availableForces * 1.75);
} else if (roll == 11) {
availableForces = (int) round((double) availableForces * 1.5);
} else if (roll > 9) {
availableForces = (int) round((double) availableForces * 1.25);
}
}

return MathUtility.clamp(availableForces, 1, maxDeployedLances);
}
}

Expand Down Expand Up @@ -228,7 +259,7 @@ protected int getQualityRating(int roll) {
}

protected void rollCommandClause(final Contract contract, final int modifier) {
final int roll = Compute.d6(2) + modifier;
final int roll = d6(2) + modifier;
if (roll < 3) {
contract.setCommandRights(ContractCommandRights.INTEGRATED);
} else if (roll < 8) {
Expand All @@ -242,14 +273,14 @@ protected void rollCommandClause(final Contract contract, final int modifier) {

protected void rollSalvageClause(AtBContract contract, int mod, int contractMaxSalvagePercentage) {
contract.setSalvageExchange(false);
int roll = Math.min(Compute.d6(2) + mod, 13);
int roll = Math.min(d6(2) + mod, 13);
if (roll < 2) {
contract.setSalvagePct(0);
} else if (roll < 4) {
contract.setSalvageExchange(true);
int r;
do {
r = Compute.d6(2);
r = d6(2);
} while (r < 4);
contract.setSalvagePct(Math.min((r - 3) * 10, contractMaxSalvagePercentage));
} else {
Expand All @@ -258,7 +289,7 @@ protected void rollSalvageClause(AtBContract contract, int mod, int contractMaxS
}

protected void rollSupportClause(AtBContract contract, int mod) {
int roll = Compute.d6(2) + mod;
int roll = d6(2) + mod;
contract.setStraightSupport(0);
contract.setBattleLossComp(0);
if (roll < 3) {
Expand All @@ -273,7 +304,7 @@ protected void rollSupportClause(AtBContract contract, int mod) {
}

protected void rollTransportClause(AtBContract contract, int mod) {
int roll = Compute.d6(2) + mod;
int roll = d6(2) + mod;
if (roll < 2) {
contract.setTransportComp(0);
} else if (roll < 6) {
Expand All @@ -299,7 +330,7 @@ protected AtBContractType findMissionType(int unitRatingMod, boolean majorPower)
AtBContractType.SECURITY_DUTY, AtBContractType.OBJECTIVE_RAID, AtBContractType.GARRISON_DUTY,
AtBContractType.CADRE_DUTY, AtBContractType.DIVERSIONARY_RAID }
};
int roll = MathUtility.clamp(Compute.d6(2) + unitRatingMod - IUnitRating.DRAGOON_C, 2, 12);
int roll = MathUtility.clamp(d6(2) + unitRatingMod - IUnitRating.DRAGOON_C, 2, 12);
return table[majorPower ? 0 : 1][roll - 2];
}

Expand All @@ -316,7 +347,7 @@ protected void setEnemyCode(AtBContract contract) {

protected void setAttacker(AtBContract contract) {
boolean isAttacker = !contract.getContractType().isGarrisonType()
|| (contract.getContractType().isReliefDuty() && (Compute.d6() < 4))
|| (contract.getContractType().isReliefDuty() && (d6() < 4))
|| contract.getEnemy().isRebel();
contract.setAttacker(isAttacker);
}
Expand Down Expand Up @@ -372,12 +403,12 @@ protected void setAllyRating(AtBContract contract, int year) {
// facing front-line units
mod += 1;
}
contract.setAllySkill(getSkillRating(Compute.d6(2) + mod));
contract.setAllySkill(getSkillRating(d6(2) + mod));
if (year > 2950 && year < 3039 &&
!Factions.getInstance().getFaction(contract.getEmployerCode()).isClan()) {
mod -= 1;
}
contract.setAllyQuality(getQualityRating(Compute.d6(2) + mod));
contract.setAllyQuality(getQualityRating(d6(2) + mod));
}

protected void setEnemyRating(AtBContract contract, int year) {
Expand All @@ -397,12 +428,12 @@ protected void setEnemyRating(AtBContract contract, int year) {
if (Factions.getInstance().getFaction(contract.getEmployerCode()).isClan()) {
mod += contract.isAttacker() ? 2 : 4;
}
contract.setEnemySkill(getSkillRating(Compute.d6(2) + mod));
contract.setEnemySkill(getSkillRating(d6(2) + mod));
if (year > 2950 && year < 3039 &&
!Factions.getInstance().getFaction(contract.getEnemyCode()).isClan()) {
mod -= 1;
}
contract.setEnemyQuality(getQualityRating(Compute.d6(2) + mod));
contract.setEnemyQuality(getQualityRating(d6(2) + mod));
}

public void writeToXML(final PrintWriter pw, int indent) {
Expand Down Expand Up @@ -475,8 +506,7 @@ public static AbstractContractMarket generateInstanceFromXML(Node wn, Campaign c

// Restore any parent contract references
for (Contract contract : retVal.contracts) {
if (contract instanceof AtBContract) {
final AtBContract atbContract = (AtBContract) contract;
if (contract instanceof AtBContract atbContract) {
atbContract.restore(c);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@
*/
package mekhq.campaign.market.contractMarket;

import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Set;

import megamek.common.Compute;
import megamek.common.annotations.Nullable;
import megamek.common.enums.SkillLevel;
Expand All @@ -39,11 +35,11 @@
import mekhq.campaign.personnel.SkillType;
import mekhq.campaign.personnel.enums.PersonnelRole;
import mekhq.campaign.rating.IUnitRating;
import mekhq.campaign.universe.Faction;
import mekhq.campaign.universe.Factions;
import mekhq.campaign.universe.PlanetarySystem;
import mekhq.campaign.universe.RandomFactionGenerator;
import mekhq.campaign.universe.Systems;
import mekhq.campaign.universe.*;

import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Set;

/**
* Contract offers that are generated monthly under AtB rules.
Expand Down Expand Up @@ -302,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));
contract.setRequiredLances(calculateRequiredLances(campaign, contract, false));
contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));

contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel());
Expand Down Expand Up @@ -396,7 +392,7 @@ protected AtBContract generateAtBSubcontract(Campaign campaign,
}
contract.setTransportComp(100);

contract.setRequiredLances(calculateRequiredLances(campaign, contract));
contract.setRequiredLances(calculateRequiredLances(campaign, contract, false));
contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));
contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel());
contract.calculateContract(campaign);
Expand Down Expand Up @@ -438,7 +434,7 @@ private void addFollowup(Campaign campaign,
followup.calculateLength(campaign.getCampaignOptions().isVariableContractLength());
setContractClauses(followup, campaign.getAtBUnitRatingMod(), campaign);

contract.setRequiredLances(calculateRequiredLances(campaign, contract));
contract.setRequiredLances(calculateRequiredLances(campaign, contract, false));
contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));

followup.setPartsAvailabilityLevel(followup.getContractType().calculatePartsAvailabilityLevel());
Expand Down Expand Up @@ -488,7 +484,11 @@ public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract
multiplier *= 1.1;
}

int requiredLances = calculateRequiredLances(campaign, contract);
int baseRequiredLances = calculateRequiredLances(campaign, contract, true);
int requiredLances = contract.getRequiredLances();

multiplier *= (double) requiredLances / baseRequiredLances;

int maxDeployedLances = calculateMaxDeployedLances(campaign);
if (requiredLances > maxDeployedLances && campaign.getCampaignOptions().isAdjustPaymentForStrategy()) {
multiplier *= (double) maxDeployedLances / (double) requiredLances;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
*/
public class CamOpsContractMarket extends AbstractContractMarket {
private static final MMLogger logger = MMLogger.create(CamOpsContractMarket.class);
private static int BASE_NEGOTIATION_TARGET = 8;
private static int EMPLOYER_NEGOTIATION_SKILL_LEVEL = 5;
private static final int BASE_NEGOTIATION_TARGET = 8;
private static final int EMPLOYER_NEGOTIATION_SKILL_LEVEL = 5;

public CamOpsContractMarket() {
super(ContractMarketMethod.CAM_OPS);
Expand Down 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));
contract.setRequiredLances(calculateRequiredLances(campaign, contract, false));
// Step 8: Calculate the payment
contract.setMultiplier(calculatePaymentMultiplier(campaign, contract));
// Step 9: Determine parts availability
Expand Down
Loading
Loading