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

Updated StratCon Sector Sizes & Added Persistent OpFor #5230

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b80ba27
Add planetary diameter to track state initialization
IllianiCBT Nov 20, 2024
bdf3772
Refactor Stratcon scenario generation to handle null scenarios
IllianiCBT Nov 20, 2024
01e3230
Add `getSize` method to StratconTrackState
IllianiCBT Nov 20, 2024
08f789b
Initialize non-objective scenarios for StratCon contracts
IllianiCBT Nov 20, 2024
127d15d
Update contract initializers for pirate and guerrilla warfare
IllianiCBT Nov 20, 2024
01a8309
Refactored scenario generation logic in Stratcon modules
IllianiCBT Nov 21, 2024
278f10a
Add option for scenarios to spawn on player facilities
IllianiCBT Nov 21, 2024
19ad220
Refactor scenario odds and add daily movement processing
IllianiCBT Nov 21, 2024
363b297
Update copyright years and license information
IllianiCBT Nov 21, 2024
11ba6b5
Refactored seedPreDeployedForces method to be private
IllianiCBT Nov 21, 2024
74aa6d4
Enable pre-deployed forces seeding in StratCon missions
IllianiCBT Nov 21, 2024
bad4866
Fixed infinite loop in weekly enemy reinforcements.
IllianiCBT Nov 21, 2024
8877ecb
Updated PreSeeded Enemy Forces Spawn
IllianiCBT Nov 21, 2024
6450289
Add mass rout processing to Stratcon campaign
IllianiCBT Nov 21, 2024
b961d1f
Add conditional check for StratCon before processing mass rout
IllianiCBT Nov 21, 2024
9db1860
Optimize StratCon handling and improve offensive definition
IllianiCBT Nov 21, 2024
d34b122
Add player force weighting for adjacent coordinate selection
IllianiCBT Nov 21, 2024
ded864f
Fixed deployment issue for scenarios without a deployment date.
IllianiCBT Nov 21, 2024
626a527
Add morale handling and routing logic to contracts
IllianiCBT Nov 22, 2024
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
Binary file modified MekHQ/docs/Stratcon and Against the Bot/MekHQ Morale.pdf
Binary file not shown.
4 changes: 4 additions & 0 deletions MekHQ/resources/mekhq/resources/Campaign.properties
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,7 @@ 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
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 @@ -3648,6 +3648,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 @@ -3782,14 +3790,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
189 changes: 159 additions & 30 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 @@ -39,6 +40,7 @@
import mekhq.campaign.event.MissionChangedEvent;
import mekhq.campaign.finances.Money;
import mekhq.campaign.force.Force;
import mekhq.campaign.force.Lance;
import mekhq.campaign.market.enums.UnitMarketType;
import mekhq.campaign.mission.atb.AtBScenarioFactory;
import mekhq.campaign.mission.enums.AtBContractType;
Expand All @@ -48,10 +50,10 @@
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;
import mekhq.campaign.stratcon.StratconTrackState;
import mekhq.campaign.unit.Unit;
import mekhq.campaign.universe.Faction;
import mekhq.campaign.universe.Factions;
Expand All @@ -74,6 +76,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 +87,9 @@
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.stratcon.StratconContractInitializer.seedPreDeployedForces;
import static mekhq.campaign.stratcon.StratconRulesManager.processMassRout;
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 +187,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 +209,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 @@ -424,21 +440,105 @@ 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

if (campaign.getCampaignOptions().isUseStratCon()) {
for (StratconTrackState track : getStratconCampaignState().getTracks()) {
seedPreDeployedForces(this, campaign, track, true);
}
}
} 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 @@ -464,10 +564,6 @@ 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)) {
Expand All @@ -480,22 +576,58 @@ public void checkMorale(Campaign campaign, LocalDate today) {
performanceModifier++;
}

int miscModifiers = moraleMod;
targetNumber.addModifier(performanceModifier, "performanceModifier");

// Balance of Power
int balanceOfPower = 0;
if (campaign.getCampaignOptions().isUseStratCon()) {
int playerForceCount = 0;

// 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;
for (Lance lance : campaign.getLances().values()) {
try {
Force force = campaign.getForce(lance.getForceId());

if (force.isCombatForce()) {
playerForceCount++;
}
} catch (Exception ex) {
logger.error(String.format("Failed to fetch force %s: %s",
lance.getForceId(), ex.getMessage()));
}
}

playerForceCount = (int) ceil((double) playerForceCount / campaign.getActiveContracts().size());

if (getCommandRights().isHouse() || getCommandRights().isLiaison()) {
playerForceCount = (int) round(playerForceCount * 1.25);
} else if (getCommandRights().isIntegrated()) {
playerForceCount = (int) round(playerForceCount * 1.5);
}

int enemyForceCount = 0;
for (StratconTrackState track : getStratconCampaignState().getTracks()) {
enemyForceCount += track.getScenarios().size();
}

if (playerForceCount >= (enemyForceCount * 3)) {
balanceOfPower = -6;
} else if (playerForceCount >= (enemyForceCount * 2)) {
balanceOfPower = -4;
} else if (playerForceCount > enemyForceCount) {
balanceOfPower = -2;
} else if (enemyForceCount >= (playerForceCount * 3)) {
balanceOfPower = 6;
} else if (enemyForceCount >= (playerForceCount * 2)) {
balanceOfPower = 4;
} else if (enemyForceCount > playerForceCount) {
balanceOfPower = 2;
}
}

targetNumber.addModifier(balanceOfPower, "balanceOfPower");

// 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 @@ -519,13 +651,10 @@ public void checkMorale(Campaign campaign, LocalDate today) {
" The contract will conclude tomorrow.");
setEndDate(today.plusDays(1));
}
}

// 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;
if (campaign.getCampaignOptions().isUseStratCon()) {
processMassRout(getStratconCampaignState(), true);
}
}

// Reset external morale modifier
Expand Down Expand Up @@ -585,7 +714,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 @@ -866,7 +995,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 @@ -1814,7 +1943,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
Loading