getLargeCraftAndWarShips() {
return getHangar().getUnits().stream()
.filter(unit -> (unit.getEntity().isLargeCraft()) || (unit.getEntity().isWarShip()))
@@ -1688,12 +1709,9 @@ public void checkBloodnameAdd(Person person, boolean ignoreDice) {
break;
}
}
- // Higher rated units are more likely to have Bloodnamed
+ // Higher-rated units are more likely to have Bloodnamed
if (getCampaignOptions().getUnitRatingMethod().isEnabled()) {
- IUnitRating rating = getUnitRating();
- bloodnameTarget += IUnitRating.DRAGOON_C - (getCampaignOptions().getUnitRatingMethod().equals(
- UnitRatingMethod.FLD_MAN_MERCS_REV)
- ? rating.getUnitRatingAsInteger() : rating.getModifier());
+ bloodnameTarget += IUnitRating.DRAGOON_C - getUnitRatingMod();
}
// Reavings diminish the number of available Bloodrights in later eras
@@ -3401,8 +3419,10 @@ && getCampaignOptions().getRandomDependentMethod().isAgainstTheBot()
/*
* First of the month; roll Morale.
*/
- IUnitRating rating = getUnitRating();
- rating.reInitialize();
+ if (campaignOptions.getUnitRatingMethod().isFMMR()) {
+ IUnitRating rating = getUnitRating();
+ rating.reInitialize();
+ }
for (AtBContract contract : getActiveAtBContracts()) {
contract.checkMorale(getLocalDate(), getUnitRatingMod());
@@ -3731,6 +3751,10 @@ public boolean newDay() {
processFatigueNewDay();
+ if (campaignOptions.getUnitRatingMethod().isCampaignOperations()) {
+ updateCrimeRating();
+ }
+
if (campaignOptions.isUseEducationModule()) {
processEducationNewDay();
}
@@ -3770,6 +3794,35 @@ public boolean newDay() {
return true;
}
+ /**
+ * Updates the campaign's crime rating based on specific conditions.
+ */
+ private void updateCrimeRating() {
+ if (faction.isPirate()) {
+ dateOfLastCrime = currentDay;
+ crimePirateModifier = -100;
+ }
+
+ if (currentDay.getDayOfMonth() == 1) {
+ if (dateOfLastCrime != null) {
+ long yearsBetween = ChronoUnit.YEARS.between(currentDay, dateOfLastCrime);
+
+ int remainingCrimeChange = 2;
+
+ if (yearsBetween >= 1) {
+ if (crimePirateModifier < 0) {
+ remainingCrimeChange = Math.max(0, 2 + crimePirateModifier);
+ changeCrimePirateModifier(2); // this is the amount of change specified by CamOps
+ }
+
+ if (crimeRating < 0 && remainingCrimeChange > 0) {
+ changeCrimeRating(remainingCrimeChange);
+ }
+ }
+ }
+ }
+ }
+
/**
* This method iterates through the list of personnel and deletes the records of those who have
* departed the unit and who match additional checks.
@@ -4359,6 +4412,75 @@ public void setRetainerEmployerCode(String code) {
retainerEmployerCode = code;
}
+ public LocalDate getRetainerStartDate() {
+ return retainerStartDate;
+ }
+
+ public void setRetainerStartDate(LocalDate retainerStartDate) {
+ this.retainerStartDate = retainerStartDate;
+ }
+
+ public int getRawCrimeRating() {
+ return crimeRating;
+ }
+
+ public void setCrimeRating(int crimeRating) {
+ this.crimeRating = crimeRating;
+ }
+
+ /**
+ * Updates the crime rating by the specified change.
+ * If improving crime rating, use a positive number, otherwise negative
+ *
+ * @param change the change to be applied to the crime rating
+ */
+ public void changeCrimeRating(int change) {
+ this.crimeRating = Math.min(0, crimeRating + change);
+ }
+
+ public int getCrimePirateModifier() {
+ return crimePirateModifier;
+ }
+
+ public void setCrimePirateModifier(int crimePirateModifier) {
+ this.crimePirateModifier = crimePirateModifier;
+ }
+ /**
+ * Updates the crime pirate modifier by the specified change.
+ * If improving the modifier, use a positive number, otherwise negative
+ *
+ * @param change the change to be applied to the crime modifier
+ */
+ public void changeCrimePirateModifier(int change) {
+ this.crimePirateModifier = Math.min(0, crimePirateModifier + change);
+ }
+
+ /**
+ * Calculates the adjusted crime rating by adding the crime rating
+ * with the pirate modifier.
+ *
+ * @return The adjusted crime rating.
+ */
+ public int getAdjustedCrimeRating() {
+ return crimeRating + crimePirateModifier;
+ }
+
+ public LocalDate getDateOfLastCrime() {
+ return dateOfLastCrime;
+ }
+
+ public void setDateOfLastCrime(LocalDate dateOfLastCrime) {
+ this.dateOfLastCrime = dateOfLastCrime;
+ }
+
+ public ReputationController getReputation() {
+ return reputation;
+ }
+
+ public void setReputation(ReputationController reputation) {
+ this.reputation = reputation;
+ }
+
private void addInMemoryLogHistory(LogEntry le) {
if (!inMemoryLogHistory.isEmpty()) {
while (ChronoUnit.DAYS.between(inMemoryLogHistory.get(0).getDate(), le.getDate()) > MHQConstants.MAX_HISTORICAL_LOG_DAYS) {
@@ -4513,7 +4635,27 @@ public void writeToXML(final PrintWriter pw) {
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "faction", getFaction().getShortName());
if (retainerEmployerCode != null) {
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "retainerEmployerCode", retainerEmployerCode);
+
+ if (retainerStartDate == null) {
+ // this handles <50.0 campaigns
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "retainerStartDate", currentDay);
+ } else {
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "retainerStartDate", retainerStartDate);
+ }
}
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "crimeRating", crimeRating);
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "crimePirateModifier", crimePirateModifier);
+
+ // this handles <50.0 campaigns
+ if (dateOfLastCrime != null) {
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "dateOfLastCrime", dateOfLastCrime);
+ } else if (getAdjustedCrimeRating() < 0) {
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "dateOfLastCrime", currentDay);
+ }
+
+ MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "reputation");
+ reputation.writeReputationToXML(pw, indent);
+ MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "reputation");
// this handles campaigns that predate 49.20
if (campaignStartDate == null) {
@@ -6054,33 +6196,49 @@ private synchronized void checkDuplicateNamesDuringDelete(Entity entity) {
});
}
+ /**
+ * Returns the text representation of the unit rating based on the selected unit rating method.
+ * If the unit rating method is FMMR, the unit rating value is returned.
+ * If the unit rating method is Campaign Operations, the reputation rating and unit rating modification are combined and returned.
+ * If the unit rating method is neither FMMR nor Campaign Operations, "N/A" is returned.
+ *
+ * @return The text representation of the unit rating
+ */
public String getUnitRatingText() {
- return getUnitRating().getUnitRating();
+ UnitRatingMethod unitRatingMethod = campaignOptions.getUnitRatingMethod();
+
+ if (unitRatingMethod.isFMMR()) {
+ return getUnitRating().getUnitRating();
+ } else if (unitRatingMethod.isCampaignOperations()) {
+ int reputationRating = reputation.getReputationRating();
+ int unitRatingMod = getUnitRatingMod();
+
+ return String.format("%d (%+d)", reputationRating, unitRatingMod);
+ } else {
+ return "N/A";
+ }
}
/**
- * Against the Bot Calculates and returns dragoon rating if that is the chosen
- * method; for IOps method, returns unit reputation / 10. If the player chooses
- * not to use unit rating at all, use a default value of C. Note that the AtB
- * system is designed for use with FMMerc dragoon rating, and use of the IOps
- * Beta system may have unsatisfactory results, but we follow the options set by
- * the user here.
+ * Retrieves the unit rating modifier based on campaign options.
+ * If the unit rating method is not enabled, it returns the default value of IUnitRating.DRAGOON_C.
+ * If the unit rating method uses FMMR, it returns the unit rating as an integer.
+ * Otherwise, it calculates the modifier using the getAtBModifier method.
+ *
+ * @return The unit rating modifier based on the campaign options.
*/
public int getUnitRatingMod() {
if (!getCampaignOptions().getUnitRatingMethod().isEnabled()) {
return IUnitRating.DRAGOON_C;
}
- IUnitRating rating = getUnitRating();
- return getCampaignOptions().getUnitRatingMethod().isFMMR() ? rating.getUnitRatingAsInteger()
- : (int) MathUtility.clamp((rating.getModifier() / campaignOptions.getAtbCamOpsDivision()), IUnitRating.DRAGOON_F, IUnitRating.DRAGOON_ASTAR);
+
+ return getCampaignOptions().getUnitRatingMethod().isFMMR() ?
+ getUnitRating().getUnitRatingAsInteger() : reputation.getAtbModifier();
}
- /**
- * This is a better method for pairing AtB with IOpts with regards to Prisoner Capture
- */
+ @Deprecated
public int getUnitRatingAsInteger() {
- return getCampaignOptions().getUnitRatingMethod().isEnabled()
- ? getUnitRating().getUnitRatingAsInteger() : IUnitRating.DRAGOON_C;
+ return getUnitRatingMod();
}
public RandomSkillPreferences getRandomSkillPreferences() {
@@ -7305,6 +7463,7 @@ public void setUnitRating(IUnitRating rating) {
* Returns the type of rating method as selected in the Campaign Options dialog.
* Lazy-loaded for performance. Default is CampaignOpsReputation
*/
+ @Deprecated
public IUnitRating getUnitRating() {
// if we switched unit rating methods,
if (unitRating != null && (unitRating.getUnitRatingMethod() != getCampaignOptions().getUnitRatingMethod())) {
@@ -7316,8 +7475,6 @@ public IUnitRating getUnitRating() {
if (UnitRatingMethod.FLD_MAN_MERCS_REV.equals(method)) {
unitRating = new FieldManualMercRevDragoonsRating(this);
- } else {
- unitRating = new CampaignOpsReputation(this);
}
}
diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java
index fab77be4cb..ab0777c57e 100644
--- a/MekHQ/src/mekhq/campaign/CampaignOptions.java
+++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java
@@ -274,8 +274,11 @@ public static String getTransitUnitName(final int unit) {
//region Life Paths Tab
// Personnel Randomization
private boolean useDylansRandomXP; // Unofficial
+
+ // Random Histories
private RandomOriginOptions randomOriginOptions;
private boolean useRandomPersonalities;
+ private boolean useRandomPersonalityReputation;
// Retirement
private boolean useRandomRetirement;
@@ -830,8 +833,11 @@ public CampaignOptions() {
//region Life Paths Tab
// Personnel Randomization
setUseDylansRandomXP(false);
+
+ // Random Histories
setRandomOriginOptions(new RandomOriginOptions(true));
setUseRandomPersonalities(false);
+ setUseRandomPersonalityReputation(true);
// Family
setFamilyDisplayLevel(FamilialRelationshipDisplayLevel.SPOUSE);
@@ -1864,6 +1870,14 @@ public boolean isUseRandomPersonalities() {
public void setUseRandomPersonalities(final boolean useRandomPersonalities) {
this.useRandomPersonalities = useRandomPersonalities;
}
+
+ public boolean isUseRandomPersonalityReputation() {
+ return useRandomPersonalityReputation;
+ }
+
+ public void setUseRandomPersonalityReputation(final boolean useRandomPersonalityReputation) {
+ this.useRandomPersonalityReputation = useRandomPersonalityReputation;
+ }
//endregion Personnel Randomization
//region Retirement
@@ -4712,6 +4726,7 @@ public void writeToXml(final PrintWriter pw, int indent) {
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useDylansRandomXP", isUseDylansRandomXP());
getRandomOriginOptions().writeToXML(pw, indent);
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useRandomPersonalities", isUseRandomPersonalities());
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useRandomPersonalityReputation", isUseRandomPersonalityReputation());
//endregion Personnel Randomization
//region Retirement
@@ -5466,6 +5481,8 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve
retVal.setRandomOriginOptions(randomOriginOptions);
} else if (wn2.getNodeName().equalsIgnoreCase("useRandomPersonalities")) {
retVal.setUseRandomPersonalities(Boolean.parseBoolean(wn2.getTextContent().trim()));
+ } else if (wn2.getNodeName().equalsIgnoreCase("useRandomPersonalityReputation")) {
+ retVal.setUseRandomPersonalityReputation(Boolean.parseBoolean(wn2.getTextContent().trim()));
//endregion Personnel Randomization
//region Family
diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java
index c210b5b30a..ab0a97a9ca 100644
--- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java
+++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java
@@ -56,6 +56,7 @@
import mekhq.campaign.personnel.ranks.RankSystem;
import mekhq.campaign.personnel.ranks.RankValidator;
import mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker;
+import mekhq.campaign.rating.CamOpsReputation.ReputationController;
import mekhq.campaign.storyarc.StoryArc;
import mekhq.campaign.unit.Unit;
import mekhq.campaign.unit.cleanup.EquipmentUnscrambler;
@@ -694,6 +695,16 @@ private static void processInfoNode(Campaign retVal, Node wni, Version version)
retVal.setFactionCode(wn.getTextContent());
} else if (xn.equalsIgnoreCase("retainerEmployerCode")) {
retVal.setRetainerEmployerCode(wn.getTextContent());
+ } else if (xn.equalsIgnoreCase("retainerStartDate")) {
+ retVal.setRetainerStartDate(LocalDate.parse(wn.getTextContent()));
+ } else if (xn.equalsIgnoreCase("crimeRating")) {
+ retVal.setCrimeRating(Integer.parseInt(wn.getTextContent()));
+ } else if (xn.equalsIgnoreCase("crimePirateModifier")) {
+ retVal.setCrimePirateModifier(Integer.parseInt(wn.getTextContent()));
+ } else if (xn.equalsIgnoreCase("dateOfLastCrime")) {
+ retVal.setDateOfLastCrime(LocalDate.parse(wn.getTextContent()));
+ } else if (xn.equalsIgnoreCase("reputation")) {
+ retVal.setReputation(new ReputationController().generateInstanceFromXML(wn));
} else if (xn.equalsIgnoreCase("rankSystem")) {
if (!wn.hasChildNodes()) { // we need there to be child nodes to parse from
continue;
diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java
index 29ce00e7f0..11f600d817 100644
--- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java
+++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java
@@ -54,6 +54,7 @@
import mekhq.campaign.personnel.SkillType;
import mekhq.campaign.personnel.enums.Phenotype;
import mekhq.campaign.rating.IUnitRating;
+import mekhq.campaign.rating.UnitRatingMethod;
import mekhq.campaign.stratcon.StratconBiomeManifest;
import mekhq.campaign.stratcon.StratconContractInitializer;
import mekhq.campaign.unit.Unit;
@@ -713,13 +714,14 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac
// For BV-scaled forces, check whether to stop generating after each formation is
// generated.
if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal()) {
-
// Check random number vs. percentage of the BV budget already generated, with the
// percentage chosen based on unit rating
int roll = Compute.randomInt(100);
double rollTarget = ((double) forceBV / forceBVBudget) * 100;
- stopGenerating = rollTarget > minimumBVPercentage[campaign.getUnitRating().getUnitRatingAsInteger()] &&
- roll < rollTarget;
+
+ int unitRating = getUnitRating(campaign);
+
+ stopGenerating = rollTarget > minimumBVPercentage[unitRating] && roll < rollTarget;
} else {
// For generation methods other than scaled BV, compare to the overall budget
stopGenerating = generatedEntities.size() >= forceUnitBudget;
@@ -763,6 +765,25 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac
return generatedLanceCount;
}
+ /**
+ * Retrieves the unit rating from the given campaign.
+ *
+ * @param campaign the campaign from which the unit rating is to be retrieved
+ * @return the unit rating value as an integer
+ */
+ private static int getUnitRating(Campaign campaign) {
+ final CampaignOptions campaignOptions = campaign.getCampaignOptions();
+ final UnitRatingMethod unitRatingMethod = campaignOptions.getUnitRatingMethod();
+
+ int unitRating = IUnitRating.DRAGOON_C;
+ if (unitRatingMethod.isFMMR()) {
+ unitRating = campaign.getUnitRating().getUnitRatingAsInteger();
+ } else if (unitRatingMethod.isCampaignOperations()) {
+ unitRating = campaign.getReputation().getAtbModifier();
+ }
+ return unitRating;
+ }
+
/**
* Generates the indicated number of civilian entities.
*
diff --git a/MekHQ/src/mekhq/campaign/personnel/Person.java b/MekHQ/src/mekhq/campaign/personnel/Person.java
index 9fbe04bfe8..3e6286a2c0 100644
--- a/MekHQ/src/mekhq/campaign/personnel/Person.java
+++ b/MekHQ/src/mekhq/campaign/personnel/Person.java
@@ -3657,16 +3657,31 @@ public Skill getBestTechSkill() {
}
public boolean isTech() {
- //type must be correct and you must be more than ultra-green in the skill
- boolean isMechTech = hasSkill(SkillType.S_TECH_MECH) && getSkill(SkillType.S_TECH_MECH).getExperienceLevel() > SkillType.EXP_ULTRA_GREEN;
- boolean isAeroTech = hasSkill(SkillType.S_TECH_AERO) && getSkill(SkillType.S_TECH_AERO).getExperienceLevel() > SkillType.EXP_ULTRA_GREEN;
- boolean isMechanic = hasSkill(SkillType.S_TECH_MECHANIC) && getSkill(SkillType.S_TECH_MECHANIC).getExperienceLevel() > SkillType.EXP_ULTRA_GREEN;
- boolean isBATech = hasSkill(SkillType.S_TECH_BA) && getSkill(SkillType.S_TECH_BA).getExperienceLevel() > SkillType.EXP_ULTRA_GREEN;
- // At some point we may want to re-write things to include this
- /*boolean isEngineer = hasSkill(SkillType.S_TECH_VESSEL) && getSkill(SkillType.S_TECH_VESSEL).getExperienceLevel() > SkillType.EXP_ULTRA_GREEN
- && campaign.getUnit(getUnitId()).getEngineer() != null
- && campaign.getUnit(getUnitId()).getEngineer().equals(this);*/
- return (getPrimaryRole().isTech() || getSecondaryRole().isTechSecondary()) && (isMechTech || isAeroTech || isMechanic || isBATech);
+ return isTechMech() || isTechAero() || isTechMechanic() || isTechBA();
+ }
+
+ public boolean isTechMech() {
+ boolean hasSkill = hasSkill(SkillType.S_TECH_MECH) && getSkill(SkillType.S_TECH_MECH).getExperienceLevel() > SkillType.EXP_ULTRA_GREEN;
+
+ return hasSkill && (getPrimaryRole().isMechTech() || getSecondaryRole().isMechTech());
+ }
+
+ public boolean isTechAero() {
+ boolean hasSkill = hasSkill(SkillType.S_TECH_AERO) && getSkill(SkillType.S_TECH_AERO).getExperienceLevel() > SkillType.EXP_ULTRA_GREEN;
+
+ return hasSkill && (getPrimaryRole().isAeroTech() || getSecondaryRole().isAeroTech());
+ }
+
+ public boolean isTechMechanic() {
+ boolean hasSkill = hasSkill(SkillType.S_TECH_MECHANIC) && getSkill(SkillType.S_TECH_MECHANIC).getExperienceLevel() > SkillType.EXP_ULTRA_GREEN;
+
+ return hasSkill && (getPrimaryRole().isMechanic() || getSecondaryRole().isMechanic());
+ }
+
+ public boolean isTechBA() {
+ boolean hasSkill = hasSkill(SkillType.S_TECH_BA) && getSkill(SkillType.S_TECH_BA).getExperienceLevel() > SkillType.EXP_ULTRA_GREEN;
+
+ return hasSkill && (getPrimaryRole().isBATech() || getSecondaryRole().isBATech());
}
public boolean isAdministrator() {
diff --git a/MekHQ/src/mekhq/campaign/personnel/Skill.java b/MekHQ/src/mekhq/campaign/personnel/Skill.java
index 8721c3659e..6fb059bcd7 100644
--- a/MekHQ/src/mekhq/campaign/personnel/Skill.java
+++ b/MekHQ/src/mekhq/campaign/personnel/Skill.java
@@ -20,7 +20,6 @@
*/
package mekhq.campaign.personnel;
-import megamek.Version;
import megamek.common.Compute;
import megamek.common.enums.SkillLevel;
import mekhq.utilities.MHQXMLUtility;
@@ -34,7 +33,7 @@
* As ov v0.1.9, we will be tracking a group of skills on the person. These skills will define
* personnel rather than subtypes wrapped around pilots and teams. This will allow for considerably
* more flexibility in the kinds of personnel available.
- *
+ *
* Four important characteristics will determine how each skill works
* level - this is the level of the skill. By default this will go from 0 to 10, but the max will
* be customizable. These won't necessarily correspond to named levels (e.g. Green, Elite)
@@ -142,6 +141,15 @@ public int getFinalSkillValue() {
}
}
+ /**
+ * Calculates the total skill value by summing the level and bonus.
+ *
+ * @return The total skill value.
+ */
+ public int getTotalSkillLevel() {
+ return level + bonus;
+ }
+
public void improve() {
if (level >= SkillType.NUM_LEVELS - 1) {
// Can't improve past the max
diff --git a/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/AverageExperienceRating.java b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/AverageExperienceRating.java
new file mode 100644
index 0000000000..e5452d327a
--- /dev/null
+++ b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/AverageExperienceRating.java
@@ -0,0 +1,230 @@
+package mekhq.campaign.rating.CamOpsReputation;
+
+import megamek.codeUtilities.MathUtility;
+import megamek.common.*;
+import megamek.common.enums.SkillLevel;
+import megamek.logging.MMLogger;
+import mekhq.campaign.Campaign;
+import mekhq.campaign.personnel.Person;
+import mekhq.campaign.personnel.SkillType;
+import mekhq.campaign.unit.Unit;
+
+import java.util.function.Consumer;
+
+public class AverageExperienceRating {
+ private static final MMLogger logger = MMLogger.create(AverageExperienceRating.class);
+
+ /**
+ * Calculates the skill level based on the average experience rating of a campaign.
+ *
+ * @param campaign the campaign to calculate the average experience rating from
+ * @param log whether to log the calculation in mekhq.log
+ * @return the skill level based on the average experience rating
+ * @throws IllegalStateException if the experience score is not within the expected range
+ */
+ protected static SkillLevel getSkillLevel(Campaign campaign, boolean log) {
+ // values below 0 are treated as 'Legendary',
+ // values above 7 are treated as 'wet behind the ears' which we call 'None'
+ int experienceScore = MathUtility.clamp(
+ calculateAverageExperienceRating(campaign, log),
+ 0,
+ 7
+ );
+
+ return switch (experienceScore) {
+ case 7 -> SkillLevel.NONE;
+ case 6 -> SkillLevel.ULTRA_GREEN;
+ case 5 -> SkillLevel.GREEN;
+ case 4 -> SkillLevel.REGULAR;
+ case 3 -> SkillLevel.VETERAN;
+ case 2 -> SkillLevel.ELITE;
+ case 1 -> SkillLevel.HEROIC;
+ case 0 -> SkillLevel.LEGENDARY;
+ default -> throw new IllegalStateException(
+ "Unexpected value in mekhq/campaign/rating/CamOpsRatingV2/AverageExperienceRating.java/getSkillLevel: "
+ + experienceScore
+ );
+ };
+ }
+
+ /**
+ * Retrieves the reputation modifier.
+ *
+ * @param averageSkillLevel the average skill level to calculate the reputation modifier for
+ * @return the reputation modifier for the camera operator
+ */
+ protected static int getReputationModifier(SkillLevel averageSkillLevel) {
+ int modifier = switch(averageSkillLevel) {
+ case NONE, ULTRA_GREEN, GREEN -> 5;
+ case REGULAR -> 10;
+ case VETERAN -> 20;
+ case ELITE, HEROIC, LEGENDARY -> 40;
+ };
+
+ logger.debug("Reputation Rating = {}, +{}",
+ averageSkillLevel.toString(),
+ modifier);
+
+ return modifier;
+ }
+
+ /**
+ * Calculates a modifier for Against the Bot's various systems, based on the average skill level.
+ *
+ * @param campaign the campaign from which to calculate the ATB modifier
+ * @return the ATB modifier as an integer value
+ */
+ public static int getAtBModifier(Campaign campaign) {
+ SkillLevel averageSkillLevel = getSkillLevel(campaign, false);
+
+ return switch (averageSkillLevel) {
+ case NONE, ULTRA_GREEN -> 0;
+ case GREEN -> 1;
+ case REGULAR -> 2;
+ case VETERAN -> 3;
+ case ELITE -> 4;
+ case HEROIC, LEGENDARY -> 5;
+ };
+ }
+
+ /**
+ * Calculates the average experience rating of combat personnel in the given campaign.
+ *
+ * @param campaign the campaign to calculate the average experience rating for
+ * @param log whether to log the calculation to mekhq.log
+ * @return the average experience rating of personnel in the campaign
+ */
+ private static int calculateAverageExperienceRating(Campaign campaign, boolean log) {
+ int personnelCount = 0;
+ double totalExperience = 0.0;
+
+ for (Person person : campaign.getActivePersonnel()) {
+ Unit unit = person.getUnit();
+
+ // if the person does not belong to a unit, then skip this person
+ if (unit == null) {
+ continue;
+ }
+
+ Entity entity = unit.getEntity();
+ // if the unit's entity is a JumpShip, then it is not considered a combatant.
+ if (entity instanceof Jumpship) {
+ continue;
+ }
+
+ // if both primary and secondary roles are support roles, skip this person
+ // as they are also not considered combat personnel
+ if (person.getPrimaryRole().isSupport() && person.getSecondaryRole().isSupport()) {
+ continue;
+ }
+
+ Crew crew = entity.getCrew();
+
+ // Experience calculation varies depending on the type of entity
+ if (entity instanceof Infantry) {
+ // we only want to parse infantry units once, as CamOps treats them as an individual entity
+ if (!unit.isCommander(person)) {
+ continue;
+ }
+
+ // For Infantry, average experience is calculated using a different method.
+ totalExperience += calculateInfantryExperience((Infantry) entity, crew); // add the average experience to the total
+ personnelCount++;
+ } else if (entity instanceof Protomech) {
+ // ProtoMech entities only use gunnery for calculation
+ if (person.hasSkill(SkillType.S_GUN_PROTO)) {
+ totalExperience += person.getSkill(SkillType.S_GUN_PROTO).getTotalSkillLevel();
+ }
+
+ personnelCount ++;
+ } else {
+ // For regular entities, another method calculates the average experience
+ if (unit.isGunner(person) || unit.isDriver(person)) {
+ totalExperience += calculateRegularExperience(person, entity, unit);
+ personnelCount ++;
+ }
+ }
+ }
+
+ if (personnelCount == 0) {
+ return 7;
+ }
+
+ // Calculate the average experience rating across all personnel. If there are no personnel, return 0
+ double rawAverage = personnelCount > 0 ? (totalExperience / personnelCount) : 0;
+
+ // CamOps wants us to round down from 0.5 and up from >0.5, so we need to do an extra step here
+ double fractionalPart = rawAverage - Math.floor(rawAverage);
+
+ int averageExperienceRating = (int) (fractionalPart > 0.5 ? Math.ceil(rawAverage) : Math.floor(rawAverage));
+
+ // Log the details of the calculation to aid debugging,
+ // and so the user can easily see if there is a mistake
+ if (log) {
+ logger.debug("Average Experience Rating: {} / {} = {}",
+ totalExperience,
+ personnelCount,
+ averageExperienceRating);
+ }
+
+ // Return the average experience rating
+ return averageExperienceRating;
+ }
+
+ /**
+ * Calculates the average experience of an Infantry entity's crew.
+ *
+ * @param infantry The Infantry entity, which also includes some crew details.
+ * @param crew The unit crew.
+ * @return The average experience of the Infantry crew.
+ */
+ private static double calculateInfantryExperience(Infantry infantry, Crew crew) {
+ // Average of gunnery and antiMek skill
+ int gunnery = crew.getGunnery();
+ int antiMek = infantry.getAntiMekSkill();
+
+ return (double) (gunnery + antiMek) / 2;
+ }
+
+ /**
+ * Calculates the average experience of a (non-Infantry, non-ProtoMech) crew.
+ *
+ * @param person The person in the crew.
+ * @param entity The entity associated with the crew.
+ * @param unit The unit the crew belongs to.
+ * @return The average experience of the crew.
+ */
+ private static double calculateRegularExperience(Person person, Entity entity, Unit unit) {
+ /*
+ * skillData[0] represents sumOfSkillLevels
+ * skillData[1] represents skillCount
+ */
+ int[] skillData = new int[2];
+
+ Consumer skillHandler = skillName -> {
+ if (person.hasSkill(skillName)) {
+ skillData[0] += person.getSkill(skillName).getTotalSkillLevel();
+ skillData[1]++;
+ }
+ };
+
+ if (unit.isDriver(person)) {
+ skillHandler.accept(SkillType.getDrivingSkillFor(entity));
+ }
+
+ if (unit.isGunner(person)) {
+ skillHandler.accept(SkillType.getGunnerySkillFor(entity));
+ }
+
+ if (skillData[1] != 0) {
+ if (person.getPrimaryRole().isVehicleCrew() &&
+ (person.getSecondaryRole().isCivilian() || person.getSecondaryRole().isVehicleCrew())) {
+ if (person.hasSkill(SkillType.S_TECH_MECHANIC)) {
+ return person.getSkill(SkillType.S_TECH_MECHANIC).getTotalSkillLevel();
+ }
+ }
+ }
+
+ return (skillData[0] > 0) ? (double) skillData[0] / skillData[1] : 0.0;
+ }
+}
diff --git a/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/CombatRecordRating.java b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/CombatRecordRating.java
new file mode 100644
index 0000000000..cb049fc28b
--- /dev/null
+++ b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/CombatRecordRating.java
@@ -0,0 +1,86 @@
+package mekhq.campaign.rating.CamOpsReputation;
+
+import megamek.logging.MMLogger;
+import mekhq.campaign.Campaign;
+import mekhq.campaign.mission.Mission;
+import mekhq.campaign.mission.enums.MissionStatus;
+
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class CombatRecordRating {
+ private static final MMLogger logger = MMLogger.create(CombatRecordRating.class);
+
+ /**
+ * Calculates the combat record rating for the provided campaign.
+ *
+ * @param campaign the campaign for which to calculate the combat record rating
+ * @return a map containing the combat record ratings:
+ * - "partialSuccesses": the number of missions with status "PARTIAL"
+ * - "successes": the number of missions with status "SUCCESS"
+ * - "failures": the number of missions with status "FAILED"
+ * - "contractsBreached": the number of missions with status "BREACH"
+ * - "retainerDuration": the duration of the campaign's retainer (in years),
+ * zero if there is no retainer
+ * - "total": the total combat record rating calculated using the formula:
+ * (successes * 5) - (failures * 10) - (contractBreaches * 25)
+ */
+ protected static Map calculateCombatRecordRating(Campaign campaign) {
+ Map combatRecord = new HashMap<>();
+
+ // If the faction is pirate, set all values to zero and return the map immediately,
+ // CamOps says pirates don't track combat record rating, but we still want these values for use elsewhere
+ if (campaign.getFaction().isPirate()) {
+ combatRecord.put("partialSuccesses", 0);
+ combatRecord.put("successes", 0);
+ combatRecord.put("failures", 0);
+ combatRecord.put("contractsBreached", 0);
+ combatRecord.put("retainerDuration", 0);
+ combatRecord.put("total", 0);
+ return combatRecord;
+ }
+
+ // Construct a map with mission statuses and their counts
+ Map missionCountsByStatus = campaign.getCompletedMissions().stream()
+ .filter(mission -> mission.getStatus() != MissionStatus.ACTIVE)
+ .collect(Collectors.groupingBy(Mission::getStatus, Collectors.counting()));
+
+ // Assign mission counts to each category
+ int successes = missionCountsByStatus.getOrDefault(MissionStatus.SUCCESS, 0L).intValue();
+ int partialSuccesses = missionCountsByStatus.getOrDefault(MissionStatus.PARTIAL, 0L).intValue();
+ int failures = missionCountsByStatus.getOrDefault(MissionStatus.FAILED, 0L).intValue();
+ int contractBreaches = missionCountsByStatus.getOrDefault(MissionStatus.BREACH, 0L).intValue();
+
+ // place the values into the map
+ combatRecord.put("partialSuccesses", partialSuccesses);
+ combatRecord.put("successes", successes);
+ combatRecord.put("failures", failures);
+ combatRecord.put("contractsBreached", contractBreaches);
+
+ // Calculate combat record rating
+ int combatRecordRating = (successes * 5) - (failures * 10) - (contractBreaches * 25);
+
+ // if the campaign has a retainer, check retainer duration
+ if (campaign.getRetainerStartDate() != null) {
+ int retainerDuration = (int) ChronoUnit.YEARS.between(campaign.getRetainerStartDate(), campaign.getLocalDate());
+ combatRecord.put("retainerDuration", retainerDuration);
+ combatRecordRating += retainerDuration * 5;
+ } else {
+ combatRecord.put("retainerDuration", 0);
+ }
+
+ // add the total rating to the map
+ combatRecord.put("total", combatRecordRating);
+
+ // post a log to aid debugging
+ logger.debug("Combat Record Rating = {}",
+ combatRecord.keySet().stream()
+ .map(key -> String.format("%s: %d", key, combatRecord.get(key)))
+ .collect(Collectors.joining("\n")));
+
+ // return the completed map
+ return combatRecord;
+ }
+}
diff --git a/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/CommandRating.java b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/CommandRating.java
new file mode 100644
index 0000000000..58d6050625
--- /dev/null
+++ b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/CommandRating.java
@@ -0,0 +1,123 @@
+package mekhq.campaign.rating.CamOpsReputation;
+
+import megamek.logging.MMLogger;
+import mekhq.campaign.Campaign;
+import mekhq.campaign.CampaignOptions;
+import mekhq.campaign.personnel.Person;
+import mekhq.campaign.personnel.SkillType;
+import mekhq.campaign.personnel.enums.randomEvents.personalities.Aggression;
+import mekhq.campaign.personnel.enums.randomEvents.personalities.Ambition;
+import mekhq.campaign.personnel.enums.randomEvents.personalities.Greed;
+import mekhq.campaign.personnel.enums.randomEvents.personalities.Social;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class CommandRating {
+ private static final MMLogger logger = MMLogger.create(CommandRating.class);
+
+ /**
+ * Calculates the rating of a commander based on their skills and personality.
+ *
+ * @param campaign the campaign the commander belongs to
+ * @param commander the commander to calculate the rating for
+ * @return a map containing the commander's rating in different areas:
+ * - "leadership": the commander's leadership skill value
+ * - "tactics": the commander's tactics skill value
+ * - "strategy": the commander's strategy skill value
+ * - "negotiation": the commander's negotiation skill value
+ * - "traits": the commander's traits (not currently tracked, always 0)
+ * - "personality": the value of the commander's personality characteristics (or 0, if disabled)
+ */
+ protected static Map calculateCommanderRating(Campaign campaign, Person commander) {
+ Map commandRating = new HashMap<>();
+
+ commandRating.put("leadership", getSkillValue(commander, SkillType.S_LEADER));
+ commandRating.put("tactics", getSkillValue(commander, SkillType.S_TACTICS));
+ commandRating.put("strategy", getSkillValue(commander, SkillType.S_STRATEGY));
+ commandRating.put("negotiation", getSkillValue(commander, SkillType.S_NEG));
+
+ // ATOW traits are not currently tracked by mhq, but when they are, this is where we'd add that data
+ commandRating.put("traits", 0);
+
+ // this will return 0 if personalities are disabled
+ commandRating.put("personality", getPersonalityValue(campaign, commander));
+
+ commandRating.put("total", commandRating.values().stream().mapToInt(rating -> rating).sum());
+
+ logger.debug("Command Rating = {}",
+ commandRating.keySet().stream()
+ .map(key -> key + ": " + commandRating.get(key) + '\n')
+ .collect(Collectors.joining()));
+
+ return commandRating;
+ }
+
+ /**
+ * @return the final skill value for the given skill,
+ * or 0 if the person does not have the skill
+ *
+ * @param person the person
+ * @param skill the skill
+ */
+ private static int getSkillValue(Person person, String skill) {
+ if (person == null) {
+ return 0;
+ }
+
+ if (person.hasSkill(skill)) {
+ return person.getSkill(skill).getExperienceLevel();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Calculates the total value of a person's personality characteristics.
+ *
+ * @param campaign the current campaign
+ * @param person the person to calculate the personality value for
+ * @return the total personality value of the person in the campaign
+ */
+ private static int getPersonalityValue(Campaign campaign, Person person) {
+ if (person == null) {
+ return 0;
+ }
+
+ CampaignOptions campaignOptions = campaign.getCampaignOptions();
+
+ if (campaignOptions.isUseRandomPersonalities() && campaignOptions.isUseRandomPersonalityReputation()) {
+ int personalityValue = 0;
+ int modifier;
+
+ Aggression aggression = person.getAggression();
+ if (!person.getAggression().isNone()) {
+ modifier = aggression.isTraitPositive() ? 1 : -1;
+ personalityValue += aggression.isTraitMajor() ? modifier * 2 : modifier;
+ }
+
+ Ambition ambition = person.getAmbition();
+ if (!person.getAmbition().isNone()) {
+ modifier = ambition.isTraitPositive() ? 1 : -1;
+ personalityValue += ambition.isTraitMajor() ? modifier * 2 : modifier;
+ }
+
+ Greed greed = person.getGreed();
+ if (!person.getGreed().isNone()) {
+ modifier = greed.isTraitPositive() ? 1 : -1;
+ personalityValue += greed.isTraitMajor() ? modifier * 2 : modifier;
+ }
+
+ Social social = person.getSocial();
+ if (!person.getSocial().isNone()) {
+ modifier = social.isTraitPositive() ? 1 : -1;
+ personalityValue += social.isTraitMajor() ? modifier * 2 : modifier;
+ }
+
+ return personalityValue;
+ } else {
+ return 0;
+ }
+ }
+}
diff --git a/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/CrimeRating.java b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/CrimeRating.java
new file mode 100644
index 0000000000..53396722e3
--- /dev/null
+++ b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/CrimeRating.java
@@ -0,0 +1,35 @@
+package mekhq.campaign.rating.CamOpsReputation;
+
+import megamek.logging.MMLogger;
+import mekhq.campaign.Campaign;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class CrimeRating {
+ private static final MMLogger logger = MMLogger.create(CrimeRating.class);
+
+ /**
+ * Calculates the crime rating for a given campaign.
+ *
+ * @param campaign the campaign for which to calculate the crime rating
+ * @return the calculated crime rating
+ */
+ protected static Map calculateCrimeRating(Campaign campaign) {
+ Map crimeRating = new HashMap<>();
+
+ crimeRating.put("piracy", campaign.getCrimePirateModifier());
+ crimeRating.put("other", campaign.getRawCrimeRating());
+
+ int adjustedCrimeRating = campaign.getAdjustedCrimeRating();
+ crimeRating.put("total", adjustedCrimeRating);
+
+ logger.debug("Crime Rating = {}",
+ crimeRating.entrySet().stream()
+ .map(entry -> String.format("%s: %d\n", entry.getKey(), entry.getValue()))
+ .collect(Collectors.joining()));
+
+ return crimeRating;
+ }
+}
diff --git a/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/FinancialRating.java b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/FinancialRating.java
new file mode 100644
index 0000000000..b67c085f26
--- /dev/null
+++ b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/FinancialRating.java
@@ -0,0 +1,35 @@
+package mekhq.campaign.rating.CamOpsReputation;
+
+import megamek.logging.MMLogger;
+import mekhq.campaign.finances.Finances;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class FinancialRating {
+ private static final MMLogger logger = MMLogger.create(FinancialRating.class);
+
+ /**
+ * Calculates the financial rating based on the current financial status.
+ * Negative financial status (having a loan or a negative balance) affects the rating negatively.
+ * @param finances the financial status.
+ * @return a map of the financial rating.
+ */
+ protected static Map calculateFinancialRating(Finances finances) {
+ boolean hasLoan = finances.isInDebt();
+ boolean inDebt = finances.getBalance().isNegative();
+
+ Map financeMap = Map.of(
+ "hasLoan", hasLoan ? 1 : 0,
+ "inDebt", inDebt ? 1 : 0,
+ "total", (hasLoan || inDebt) ? -10 : 0
+ );
+
+ logger.debug("Financial Rating = {}",
+ financeMap.entrySet().stream()
+ .map(entry -> String.format("%s: %d\n", entry.getKey(), entry.getValue()))
+ .collect(Collectors.joining()));
+
+ return financeMap;
+ }
+}
diff --git a/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/OtherModifiers.java b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/OtherModifiers.java
new file mode 100644
index 0000000000..f5edb003bb
--- /dev/null
+++ b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/OtherModifiers.java
@@ -0,0 +1,99 @@
+package mekhq.campaign.rating.CamOpsReputation;
+
+import megamek.logging.MMLogger;
+import mekhq.campaign.Campaign;
+import mekhq.campaign.mission.AtBContract;
+import mekhq.campaign.mission.enums.AtBContractType;
+
+import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class OtherModifiers {
+ private static final MMLogger logger = MMLogger.create(OtherModifiers.class);
+
+ /**
+ * Calculates the 'other modifiers' used by CamOps Reputation
+ *
+ * @param campaign The campaign for which to calculate the modifiers.
+ * @return A map representing the calculated modifiers. The map contains two entries:
+ * - "inactiveYears": The number of inactive years calculated from the campaign options.
+ * - "total": The total value calculated based on the number of inactive years.
+ */
+ protected static Map calculateOtherModifiers(Campaign campaign) {
+ // Calculate inactive years if campaign options allow
+ int inactiveYears = campaign.getCampaignOptions().isUseAtB() ? getInactiveYears(campaign) : 0;
+ int manualModifier = campaign.getCampaignOptions().getManualUnitRatingModifier();
+
+ // Crime rating improvements are handled on New Day, so are not included here.
+
+ // Create a map for modifiers with "inactive years" and "total" calculated from inactive years
+ Map modifierMap = Map.of(
+ "inactiveYears", inactiveYears,
+ "customModifier", manualModifier,
+ "total", manualModifier - (inactiveYears * 5)
+ );
+
+ // Log the calculated modifiers
+ logger.debug("Other Modifiers = {}",
+ modifierMap.entrySet().stream()
+ .map(entry -> String.format("%s: %d\n", entry.getKey(), entry.getValue()))
+ .collect(Collectors.joining()));
+
+ // Return the calculated modifier map
+ return modifierMap;
+ }
+
+ /**
+ * @return the number of years between the oldest mission date and the current date.
+ *
+ * @param campaign the current campaign
+ */
+ private static int getInactiveYears(Campaign campaign) {
+ LocalDate today = campaign.getLocalDate();
+
+ // Build a list of completed contracts, excluding Garrison and Cadre contracts
+ List contracts = getSuitableContracts(campaign);
+
+ // Decide the oldest mission date based on the earliest completion date of the contracts
+ // or the campaign start date if there are no completed contracts
+ LocalDate oldestMissionDate = contracts.isEmpty() ? campaign.getCampaignStartDate()
+ : contracts.stream()
+ .map(AtBContract::getEndingDate)
+ .min(LocalDate::compareTo)
+ .orElse(today);
+
+ // Calculate and return the number of years between the oldest mission date and today
+ return Math.max(0, (int) ChronoUnit.YEARS.between(today, oldestMissionDate));
+ }
+
+ /**
+ * Retrieves a list of suitable AtBContracts for the given Campaign.
+ *
+ * @param campaign The Campaign to retrieve contracts from.
+ * @return A List of suitable AtBContracts.
+ */
+ private static List getSuitableContracts(Campaign campaign) {
+ // Filter mission of type AtBContract and with completed status, check if it's suitable
+ return campaign.getMissions().stream()
+ .filter(c -> (c instanceof AtBContract) && (c.getStatus().isCompleted()))
+ .filter(c -> isSuitableContract((AtBContract) c))
+ .map(c -> (AtBContract) c)
+ .toList();
+ }
+
+ /**
+ * Determines whether a given AtBContract is suitable.
+ * CamOps excludes Garrison and Cadre contracts when calculating inactivity.
+ *
+ * @param contract The AtBContract to check.
+ * @return true if the contract is suitable, false otherwise.
+ */
+ private static boolean isSuitableContract(AtBContract contract) {
+ AtBContractType contractType = contract.getContractType();
+
+ return (!contractType.isGarrisonType() && !contractType.isCadreDuty());
+ }
+}
diff --git a/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/ReputationController.java b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/ReputationController.java
new file mode 100644
index 0000000000..f729c4566c
--- /dev/null
+++ b/MekHQ/src/mekhq/campaign/rating/CamOpsReputation/ReputationController.java
@@ -0,0 +1,608 @@
+package mekhq.campaign.rating.CamOpsReputation;
+
+import megamek.common.annotations.Nullable;
+import megamek.common.enums.SkillLevel;
+import megamek.logging.MMLogger;
+import mekhq.MekHQ;
+import mekhq.campaign.Campaign;
+import mekhq.utilities.MHQXMLUtility;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.PrintWriter;
+import java.time.LocalDate;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static mekhq.campaign.rating.CamOpsReputation.AverageExperienceRating.getAtBModifier;
+import static mekhq.campaign.rating.CamOpsReputation.AverageExperienceRating.getReputationModifier;
+import static mekhq.campaign.rating.CamOpsReputation.AverageExperienceRating.getSkillLevel;
+import static mekhq.campaign.rating.CamOpsReputation.CombatRecordRating.calculateCombatRecordRating;
+import static mekhq.campaign.rating.CamOpsReputation.CommandRating.calculateCommanderRating;
+import static mekhq.campaign.rating.CamOpsReputation.CrimeRating.calculateCrimeRating;
+import static mekhq.campaign.rating.CamOpsReputation.FinancialRating.calculateFinancialRating;
+import static mekhq.campaign.rating.CamOpsReputation.OtherModifiers.calculateOtherModifiers;
+import static mekhq.campaign.rating.CamOpsReputation.SupportRating.calculateSupportRating;
+import static mekhq.campaign.rating.CamOpsReputation.TransportationRating.calculateTransportationRating;
+
+public class ReputationController {
+ // utilities
+ private static final MMLogger logger = MMLogger.create(ReputationController.class);
+
+ private final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.CamOpsReputation",
+ MekHQ.getMHQOptions().getLocale());
+
+ // average experience rating
+ private SkillLevel averageSkillLevel = SkillLevel.NONE;
+ private int averageExperienceRating = 0;
+ private int atbModifier = 0;
+
+ // command rating
+ private Map commanderMap = new HashMap<>();
+ private int commanderRating = 0;
+
+ // combat record rating
+ private Map combatRecordMap = new HashMap<>();
+ private int combatRecordRating = 0;
+
+ // transportation rating
+ private Map transportationCapacities = new HashMap<>();
+ private Map transportationRequirements = new HashMap<>();
+ private Map transportationValues = new HashMap<>();
+ private int transportationRating = 0;
+
+ // support rating
+ private Map administrationRequirements = new HashMap<>();
+ private Map crewRequirements = new HashMap<>();
+ private Map> technicianRequirements = new HashMap<>();
+ private int supportRating = 0;
+
+ // financial rating
+ private Map financialRatingMap = new HashMap<>();
+ private int financialRating = 0;
+
+ // crime rating
+ private LocalDate dateOfLastCrime = null;
+ private Map crimeRatingMap = new HashMap<>();
+ private int crimeRating = 0;
+
+ // other modifiers
+ private Map otherModifiersMap = new HashMap<>();
+ private int otherModifiers = 0;
+
+ // total
+ private int reputationRating = 0;
+
+ //region Getters and Setters
+ public SkillLevel getAverageSkillLevel() {
+ return this.averageSkillLevel;
+ }
+
+ public int getAtbModifier() {
+ return this.atbModifier;
+ }
+
+ public int getReputationRating() {
+ return this.reputationRating;
+ }
+ //endregion Getters and Setters
+
+ /**
+ * Initializes the ReputationController class with default values.
+ */
+ public ReputationController() {}
+
+ /**
+ * Performs and stores all reputation calculations.
+ *
+ * @param campaign the campaign for which to initialize the reputation
+ */
+ @SuppressWarnings(value = "unchecked")
+ public void initializeReputation(Campaign campaign) {
+ // step one: calculate average experience rating
+ averageSkillLevel = getSkillLevel(campaign, true);
+ averageExperienceRating = getReputationModifier(averageSkillLevel);
+ atbModifier = getAtBModifier(campaign);
+
+ // step two: calculate command rating
+ commanderMap = calculateCommanderRating(campaign, campaign.getFlaggedCommander());
+ commanderRating = commanderMap.get("total");
+
+ // step three: calculate combat record rating
+ combatRecordMap = calculateCombatRecordRating(campaign);
+ combatRecordRating = combatRecordMap.get("total");
+
+ // step four: calculate transportation rating
+ List