Skip to content

Commit

Permalink
Merge pull request MegaMek#5243 from IllianiCBT/stratCon_trackSize
Browse files Browse the repository at this point in the history
Updated MekHQ Morale & StratCon Scenario Spawn Methods
  • Loading branch information
IllianiCBT authored Nov 24, 2024
2 parents ff3ffec + 519101f commit 696f5e7
Show file tree
Hide file tree
Showing 11 changed files with 489 additions and 183 deletions.
Binary file modified MekHQ/docs/Stratcon and Against the Bot/MekHQ Morale.pdf
Binary file not shown.
6 changes: 5 additions & 1 deletion MekHQ/resources/mekhq/resources/Campaign.properties
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ newAtBScenario.format=New scenario "{0}" will occur on {1}.
atbScenarioToday.format=Scenario "{0}" is today, deploy a force from your TOE!
atbScenarioTodayWithForce.format=Scenario "{0}" is today, {1} has been deployed!
generalFallbackAddress.text=Commander
garrisonDutyRouted.text=Long-ranged sensors detect no enemy activity in the AO.
contractMoraleReport.text=Current enemy condition is <b>%s</b> on contract %s.\
<br>\
<br>%s
stratConWeeklySupportPoints.text=The skill of your Admin/Transport personnel has created %s<b>%s</b>%s\
\ additional Support Points for contract %s.
stratConWeeklySupportPointsFailed.text=Your Admin/Transport personnel %s<b>failed</b>%s to create\
\ any additional Support Points for contract %s.
\ any additional Support Points for contract %s.
13 changes: 13 additions & 0 deletions MekHQ/resources/mekhq/resources/ContractMarketDialog.properties
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,16 @@ messageChallengeVeryHard.text=We've reviewed the mission details, and it's our d
<br>\
<br><i>If you are committed to this course of action, confirm your deployment.\
<br>Otherwise, you may return to review more suitable assignments.</i>
messageChallengeGarrison.text=You have selected a garrison assignment. As part of this contract,\
\ your unit will be responsible for maintaining a defensive presence in your assigned Sectors.\
\ However, we cannot predict when - or even if - you will be called to fight. Furthermore, we\
\ have no reliable intelligence on potential adversaries or the scale of any engagement that may\
\ arise.\
<br\
><br>The provided data is for estimation purposes only. Situational developments may occur\
\ without warning, and you should be prepared for anything - from prolonged quiet to sudden,\
\ large-scale conflict. Flexibility and readiness will be essential for the success of this\
\ contract.\
<br>\
<br><i>If you are prepared to assume this role, confirm your deployment.\
<br>Otherwise, you may return to review other opportunities.</i>
26 changes: 21 additions & 5 deletions MekHQ/src/mekhq/campaign/Campaign.java
Original file line number Diff line number Diff line change
Expand Up @@ -3652,6 +3652,14 @@ && getLocation().getJumpPath().getLastSystem().getId().equals(contract.getSystem
contract.setStartAndEndDate(getLocalDate().plusDays((int) Math.ceil(getLocation().getTransitTime())));
addReport("The start and end dates of " + contract.getName()
+ " have been shifted to reflect the current ETA.");

if (campaignOptions.isUseStratCon() && contract.getMoraleLevel().isRouted()) {
LocalDate newRoutEndDate = contract.getStartDate()
.plusMonths(Math.max(1, Compute.d6() - 3))
.minusDays(1);
contract.setRoutEndDate(newRoutEndDate);
}

continue;
}

Expand Down Expand Up @@ -3786,14 +3794,22 @@ private void processNewDayATB() {
}

for (AtBContract contract : getActiveAtBContracts()) {
contract.checkMorale(this, getLocalDate());
AtBMoraleLevel oldMorale = contract.getMoraleLevel();

AtBMoraleLevel morale = contract.getMoraleLevel();
contract.checkMorale(this, getLocalDate());
AtBMoraleLevel newMorale = contract.getMoraleLevel();

String report = "Current enemy condition is <b>" + morale + "</b> on contract "
+ contract.getName() + "<br><br>" + morale.getToolTipText();
String report = "";
if (contract.getContractType().isGarrisonDuty()) {
report = resources.getString("garrisonDutyRouted.text");
} else if (oldMorale != newMorale) {
report = String.format(resources.getString("contractMoraleReport.text"),
newMorale, contract.getName(), newMorale.getToolTipText());
}

addReport(report);
if (!report.isBlank()) {
addReport(report);
}
}
}

Expand Down
145 changes: 106 additions & 39 deletions MekHQ/src/mekhq/campaign/mission/AtBContract.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import megamek.client.ui.swing.util.PlayerColour;
import megamek.common.Compute;
import megamek.common.Entity;
import megamek.common.TargetRoll;
import megamek.common.UnitType;
import megamek.common.enums.Gender;
import megamek.common.enums.SkillLevel;
Expand All @@ -48,7 +49,6 @@
import mekhq.campaign.personnel.backgrounds.BackgroundsController;
import mekhq.campaign.personnel.enums.PersonnelRole;
import mekhq.campaign.personnel.enums.Phenotype;
import mekhq.campaign.rating.IUnitRating;
import mekhq.campaign.stratcon.StratconCampaignState;
import mekhq.campaign.stratcon.StratconContractDefinition;
import mekhq.campaign.stratcon.StratconContractInitializer;
Expand All @@ -74,6 +74,7 @@
import java.util.List;
import java.util.*;

import static java.lang.Math.ceil;
import static java.lang.Math.round;
import static megamek.client.ratgenerator.ModelRecord.NETWORK_NONE;
import static megamek.client.ratgenerator.UnitTable.findTable;
Expand All @@ -84,6 +85,7 @@
import static megamek.common.enums.SkillLevel.parseFromString;
import static mekhq.campaign.mission.AtBDynamicScenarioFactory.getEntity;
import static mekhq.campaign.mission.BotForceRandomizer.UNIT_WEIGHT_UNSPECIFIED;
import static mekhq.campaign.rating.IUnitRating.*;
import static mekhq.campaign.universe.Factions.getFactionLogo;
import static mekhq.campaign.universe.fameAndInfamy.BatchallFactions.BATCHALL_FACTIONS;
import static mekhq.gui.dialog.HireBulkPersonnelDialog.overrideSkills;
Expand Down Expand Up @@ -181,6 +183,16 @@ protected AtBContract() {
this(null);
}

/**
* Sets the end date of the rout.
* This should only be applied on contracts whose morale equals ROUTED
*
* @param routEnd the {@code LocalDate} representing the end date of the rout
*/
public void setRoutEndDate(LocalDate routEnd) {
this.routEnd = routEnd;
}

public AtBContract(String name) {
super(name, "Independent");
employerCode = "IND";
Expand All @@ -193,9 +205,9 @@ public AtBContract(String name) {

setContractType(AtBContractType.GARRISON_DUTY);
setAllySkill(REGULAR);
allyQuality = IUnitRating.DRAGOON_C;
allyQuality = DRAGOON_C;
setEnemySkill(REGULAR);
enemyQuality = IUnitRating.DRAGOON_C;
enemyQuality = DRAGOON_C;
allyBotName = "Ally";
enemyBotName = "Enemy";
setAllyCamouflage(new Camouflage(Camouflage.COLOUR_CAMOUFLAGE, PlayerColour.RED.name()));
Expand Down Expand Up @@ -434,21 +446,99 @@ public static boolean isMinorPower(final String factionCode) {
public void checkMorale(Campaign campaign, LocalDate today) {
// Check whether enemy forces have been reinforced, and whether any current rout continues
// beyond its expected date
boolean routContinue = Compute.randomInt(4) < 3;
boolean routContinue = Compute.randomInt(4) == 0;

// If there is a rout end date, and it's past today, update morale and enemy state accordingly
if (routEnd != null && !routContinue) {
if (today.isAfter(routEnd)) {
setMoraleLevel(AtBMoraleLevel.STALEMATE);
routEnd = null;

updateEnemy(campaign, today); // mix it up a little
} else {
setMoraleLevel(AtBMoraleLevel.ROUTED);
}
return;
}

// Initialize counters for victories and defeats
TargetRoll targetNumber = new TargetRoll();

// Confidence:
int enemySkillRating = getEnemySkill().getAdjustedValue() - 2;
int allySkillRating = getAllySkill().getAdjustedValue() - 2;

if (getCommandRights().isIndependent()) {
allySkillRating = (campaign.getCampaignOptions().getUnitRatingMethod().isFMMR() ? getAllySkill()
: campaign.getReputation().getAverageSkillLevel()).getAdjustedValue();
allySkillRating -= 2;
}

final LocalDate THE_GREAT_REFUSAL = LocalDate.of(3060, 4, 12);

if (campaign.getLocalDate().isBefore(THE_GREAT_REFUSAL)) {
if (getEnemy().isClan() && !getEmployerFaction().isClan()) {
enemySkillRating++;
} else if (!getEnemy().isClan() && getEmployerFaction().isClan()) {
allySkillRating++;
}
}

int confidence = enemySkillRating - allySkillRating;
targetNumber.addModifier(confidence, "confidence");

// Reliability:
int reliability = getEnemyQuality();

Faction enemy = getEnemy();
if (enemy.isClan()) {
reliability = Math.max(5, reliability + 1);
}

reliability = switch (reliability) {
case DRAGOON_F -> -1;
case DRAGOON_D -> {
if (Compute.randomInt(1) == 0) {
yield -1;
} else {
yield 0;
}
}
case DRAGOON_C -> 0;
case DRAGOON_B -> {
if (Compute.randomInt(1) == 0) {
yield 0;
} else {
yield +1;
}
}
case DRAGOON_A -> +1;
default -> { // DRAGOON_ASTAR
if (Compute.randomInt(1) == 0) {
yield +1;
} else {
yield +2;
}
}
};

if (enemy.isRebel()
|| enemy.isMinorPower()
|| enemy.isMercenary()
|| enemy.isPirate()) {
reliability--;
} else if (enemy.isClan()) {
reliability++;
}

targetNumber.addModifier(reliability, "reliability");

// Force Type (unimplemented)
// TODO once we have force types defined on the StratCon map, we should handle modifiers here.
// 'Mek or Aircraft == +1
// Vehicle == +0
// Infantry == -1 (if unsupported)

// Performance
int victories = 0;
int defeats = 0;
LocalDate lastMonth = today.minusMonths(1);
Expand All @@ -474,38 +564,22 @@ public void checkMorale(Campaign campaign, LocalDate today) {
}
}

// Calculate various modifiers for morale
int enemySkillModifier = getEnemySkill().getAdjustedValue() - REGULAR.getAdjustedValue();
int allySkillModifier = getAllySkill().getAdjustedValue() - REGULAR.getAdjustedValue();

int performanceModifier = 0;

if (victories > (defeats * 2)) {
performanceModifier -= 2;
if (victories >= (defeats * 2)) {
performanceModifier -= 4;
} else if (victories > defeats) {
performanceModifier--;
} else if (defeats > (victories * 2)) {
performanceModifier -= 2;
} else if (defeats >= (victories * 2)) {
performanceModifier += 4;
} else if (defeats > victories) {
performanceModifier += 2;
} else {
performanceModifier++;
}

int miscModifiers = moraleMod;

// Additional morale modifications depending on faction properties
if (Factions.getInstance().getFaction(enemyCode).isPirate()) {
miscModifiers -= 2;
} else if (Factions.getInstance().getFaction(enemyCode).isRebel()
|| isMinorPower(enemyCode)
|| Factions.getInstance().getFaction(enemyCode).isMercenary()) {
miscModifiers -= 1;
} else if (Factions.getInstance().getFaction(enemyCode).isClan()) {
miscModifiers += 2;
}
targetNumber.addModifier(performanceModifier, "performanceModifier");

// Total morale modifier calculation
int totalModifier = enemySkillModifier - allySkillModifier + performanceModifier + miscModifiers;
int roll = Compute.d6(2) + totalModifier;
int roll = Compute.d6(2) + targetNumber.getValue();

// Morale level determination based on roll value
final AtBMoraleLevel[] moraleLevels = AtBMoraleLevel.values();
Expand All @@ -531,13 +605,6 @@ public void checkMorale(Campaign campaign, LocalDate today) {
}
}

// Process the results of the reinforcement roll
if (!getMoraleLevel().isRouted() && !routContinue) {
setMoraleLevel(moraleLevels[Math.min(getMoraleLevel().ordinal() + 1, moraleLevels.length - 1)]);
campaign.addReport("Long ranged scans have detected the arrival of additional enemy forces.");
return;
}

// Reset external morale modifier
moraleMod = 0;
}
Expand Down Expand Up @@ -595,7 +662,7 @@ public int getRepairLocation(final int unitRating) {
repairLocation = Unit.SITE_FACILITY_MAINTENANCE;
}

if (unitRating >= IUnitRating.DRAGOON_B) {
if (unitRating >= DRAGOON_B) {
repairLocation++;
}

Expand Down Expand Up @@ -876,7 +943,7 @@ public void checkEvents(Campaign c) {
case 6:
final String unitName = c.getUnitMarket().addSingleUnit(c,
UnitMarketType.EMPLOYER, MEK, getEmployerFaction(),
IUnitRating.DRAGOON_F, 50);
DRAGOON_F, 50);
if (unitName != null) {
text += String.format(
"Surplus Sale: %s offered by employer on the <a href='UNIT_MARKET'>unit market</a>",
Expand Down Expand Up @@ -1824,7 +1891,7 @@ public int calculateContractDifficulty(Campaign campaign) {
double difference = enemyPower - playerPower;
double percentDifference = (difference / playerPower) * 100;

int mappedValue = (int) Math.ceil(Math.abs(percentDifference) / 20);
int mappedValue = (int) ceil(Math.abs(percentDifference) / 20);
if (percentDifference < 0) {
mappedValue = 5 - mappedValue;
} else {
Expand Down
4 changes: 2 additions & 2 deletions MekHQ/src/mekhq/campaign/mission/enums/AtBContractType.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
*/
package mekhq.campaign.mission.enums;

import java.util.ResourceBundle;

import megamek.common.Compute;
import megamek.logging.MMLogger;
import mekhq.MekHQ;
Expand All @@ -28,6 +26,8 @@
import mekhq.campaign.mission.AtBScenario;
import mekhq.campaign.universe.enums.EraFlag;

import java.util.ResourceBundle;

public enum AtBContractType {
// TODO: Missing Camops Mission Types: ASSASSINATION, ESPIONAGE, MOLE_HUNTING, OBSERVATION_RAID,
// RETAINER, SABOTAGE, TERRORISM, HIGH_RISK
Expand Down
Loading

0 comments on commit 696f5e7

Please sign in to comment.