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 11 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
23 changes: 22 additions & 1 deletion MekHQ/src/mekhq/campaign/mission/AtBContract.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
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 Down Expand Up @@ -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.stratcon.StratconContractInitializer.seedPreDeployedForces;
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 @@ -431,7 +433,14 @@ public void checkMorale(Campaign campaign, LocalDate today) {
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);
}
}
} else {
setMoraleLevel(AtBMoraleLevel.ROUTED);
}
Expand Down Expand Up @@ -493,8 +502,20 @@ public void checkMorale(Campaign campaign, LocalDate today) {
miscModifiers += 2;
}

int balanceOfPower = 0;
if (campaign.getCampaignOptions().isUseStratCon()) {
balanceOfPower = -campaign.getLanceList().size() * 5;

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

balanceOfPower += enemyForceCount;
}

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

// Morale level determination based on roll value
Expand Down
192 changes: 160 additions & 32 deletions MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,27 @@
package mekhq.campaign.stratcon;

import megamek.common.Compute;
import megamek.common.annotations.Nullable;
import megamek.logging.MMLogger;
import mekhq.campaign.Campaign;
import mekhq.campaign.force.Force;
import mekhq.campaign.mission.*;
import mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment;
import mekhq.campaign.mission.atb.AtBScenarioModifier;
import mekhq.campaign.mission.enums.AtBContractType;
import mekhq.campaign.mission.enums.ContractCommandRights;
import mekhq.campaign.stratcon.StratconContractDefinition.ObjectiveParameters;
import mekhq.campaign.stratcon.StratconContractDefinition.StrategicObjectiveType;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

import static java.lang.Math.round;
import static megamek.common.Coords.ALL_DIRECTIONS;
import static mekhq.campaign.stratcon.StratconRulesManager.addHiddenExternalScenario;
import static mekhq.campaign.stratcon.StratconRulesManager.calculateScenarioOdds;

/**
* This class handles StratCon state initialization when a contract is signed.
Expand Down Expand Up @@ -70,6 +78,7 @@

int maximumTrackIndex = Math.max(0, contract.getRequiredLances() / NUM_LANCES_PER_TRACK);
int planetaryTemperature = campaign.getLocation().getPlanet().getTemperature(campaign.getLocalDate());
double planetaryDiameter = campaign.getLocation().getPlanet().getDiameter();

for (int x = 0; x < maximumTrackIndex; x++) {
int scenarioOdds = contractDefinition.getScenarioOdds()
Expand All @@ -78,7 +87,7 @@
.get(Compute.randomInt(contractDefinition.getDeploymentTimes().size()));

StratconTrackState track = initializeTrackState(NUM_LANCES_PER_TRACK, scenarioOdds, deploymentTime,
planetaryTemperature);
planetaryTemperature, planetaryDiameter);
track.setDisplayableName(String.format("Sector %d", x));
campaignState.addTrack(track);
}
Expand All @@ -94,7 +103,7 @@
.get(Compute.randomInt(contractDefinition.getDeploymentTimes().size()));

StratconTrackState track = initializeTrackState(oddLanceCount, scenarioOdds, deploymentTime,
planetaryTemperature);
planetaryTemperature, planetaryDiameter);
track.setDisplayableName(String.format("Sector %d", campaignState.getTracks().size()));
campaignState.addTrack(track);
}
Expand Down Expand Up @@ -184,6 +193,13 @@
false, Collections.emptyList());
}

// Initialize non-objective scenarios
for (StratconTrackState track : campaignState.getTracks()) {
if (seedPreDeployedForces(contract, campaign, track)) {
break;
}
}

// clean up objectives for integrated command:
// we're still going to have all the objective facilities and scenarios
// but the player has no control over where they go, so they're
Expand All @@ -198,25 +214,70 @@
// now we're done
}

/**
* Seeds pre-deployed (hidden) forces in a {@link StratconTrackState}, taking into account
* contract type and intensity.
*
* @param contract the current contract
* @param campaign the current campaign.
* @param track the relevant {@link StratconTrackState}
*
* @return a boolean where {@code true} means forces were not deployed due to being garrison
* type or pirate hunting and {@code false} implies forces have been deployed successfully.
*/
public static boolean seedPreDeployedForces(AtBContract contract, Campaign campaign, StratconTrackState track) {
AtBContractType contractType = contract.getContractType();

// If the contract is a garrison type, we don't want to generate what will appear to be
// a full-scale invasion on day one. Furthermore, Pirates do not have enough resources
// to deploy standing forces in this manner.
if (contractType.isGarrisonType() || contractType.isPirateHunting()) {
return true;
}

// otherwise, seed each sector with hidden forces.
// the number of hidden forces is dependent on the type of contract.
final int OFFENSIVE_MULTIPLIER = 10;
final int DEFENSIVE_MULTIPLIER = 20;

int multiplier = DEFENSIVE_MULTIPLIER;

if (contractType.isGarrisonType() || contractType.isPirateHunting()) {
multiplier = (int) (DEFENSIVE_MULTIPLIER * 1.5);
} else if (contractType.isRaidType() || contractType.isGuerrillaWarfare()) {
multiplier = OFFENSIVE_MULTIPLIER;
} else if (contract.getContractType().isPlanetaryAssault()) {
multiplier = OFFENSIVE_MULTIPLIER / 2;
}

int preDeployedScenarios = track.getSize() / multiplier;
preDeployedScenarios = (int) round(preDeployedScenarios
* ((double) calculateScenarioOdds(track, contract, false) / 100));

for (int i = 0; i < preDeployedScenarios; i++) {
addHiddenExternalScenario(campaign, contract, track, null, false);
}
return false;
}

/**
* Set up initial state of a track, dimensions are based on number of assigned
* lances.
*/
public static StratconTrackState initializeTrackState(int numLances, int scenarioOdds,
int deploymentTime, int planetaryTemp) {
// to initialize a track,
// 1. we set the # of required lances
// 2. set the track size to a total of numlances * 28 hexes, a rectangle that is
// wider than it is taller
// the idea being to create a roughly rectangular playing field that,
// if one deploys a scout lance each week to a different spot, can be more or
// less fully covered

int deploymentTime, int planetaryTemp,
double planetaryDiameter) {
StratconTrackState retVal = new StratconTrackState();
retVal.setRequiredLanceCount(numLances);

// calculate planet surface area
double radius = planetaryDiameter / 2;
double planetSurfaceArea = 4 * Math.PI * Math.pow(radius, 2);
// This gives us a decently sized track, without it feeling too large
planetSurfaceArea /= 1000000;

// set width and height
int numHexes = numLances * 28;
int numHexes = (int) round(planetSurfaceArea);
int height = (int) Math.floor(Math.sqrt(numHexes));
int width = numHexes / height;
retVal.setWidth(width);
Expand Down Expand Up @@ -287,7 +348,7 @@
sf.setStrategicObjective(strategicObjective);
sf.getLocalModifiers().addAll(modifiers);

StratconCoords coords = getUnoccupiedCoords(trackState);
StratconCoords coords = getUnoccupiedCoords(trackState, false);

if (coords == null) {
logger.warn(String.format("Unable to place facility on track %s," +
Expand Down Expand Up @@ -341,7 +402,7 @@
ScenarioTemplate template = StratconScenarioFactory.getSpecificScenario(
objectiveScenarios.get(Compute.randomInt(objectiveScenarios.size())));

StratconCoords coords = getUnoccupiedCoords(trackState);
StratconCoords coords = getUnoccupiedCoords(trackState, false);

if (coords == null) {
logger.error(String.format("Unable to place objective scenario on track %s," +
Expand Down Expand Up @@ -387,30 +448,97 @@
}

/**
* Utility function that, given a track state, picks a random set of unoccupied
* coordinates.
* Searches for a suitable coordinate on the given {@link StratconTrackState}.
* The suitability of a coordinate is determined by the absence of a scenario in the coordinate
* and the absence of a facility or the presence of a player-allied facility (determined by the
* value of allowPlayerFacilities).
* <p>
* The method iterates through all possible coordinates and shuffles them to ensure randomness.
* A random coordinate is then selected from the list of suitable coordinates and returned.
*
* @param trackState the {@link StratconTrackState} object on which to perform the search
* @param allowPlayerFacilities a {@link boolean} value indicating whether player-owned facilities
* should be considered suitable
* @return a {@link StratconCoords} object representing the coordinates of a suitable location,
* or {@code null} if no suitable location was found
*/
public static StratconCoords getUnoccupiedCoords(StratconTrackState trackState) {
// Maximum number of attempts
int maxAttempts = trackState.getWidth() * trackState.getHeight();
int attempts = 0;

int x = Compute.randomInt(trackState.getWidth());
int y = Compute.randomInt(trackState.getHeight());
StratconCoords coords = new StratconCoords(x, y);

while ((trackState.getFacility(coords) != null || trackState.getScenario(coords) != null) && attempts < maxAttempts) {
x = Compute.randomInt(trackState.getWidth());
y = Compute.randomInt(trackState.getHeight());
coords = new StratconCoords(x, y);
attempts++;
public static @Nullable StratconCoords getUnoccupiedCoords(StratconTrackState trackState, boolean allowPlayerFacilities) {
int trackHeight = trackState.getHeight();
int trackWidth = trackState.getWidth();

List<StratconCoords> suitableCoords = new ArrayList<>();

for (int y = 0; y < trackHeight; y++) {
for (int x = 0; x < trackWidth; x++) {
StratconCoords coords = new StratconCoords(x, y);

if (trackState.getScenario(coords) != null) {
continue;
}

if (trackState.getFacility(coords) == null) {
suitableCoords.add(coords);
continue;
}

if (trackState.getFacility(coords).getOwner() != ForceAlignment.Opposing) {
if (allowPlayerFacilities) {
suitableCoords.add(coords);
}
}
}
}

Collections.shuffle(suitableCoords);

if (suitableCoords.isEmpty()) {
return null;

Check warning

Code scanning / CodeQL

Random used only once Warning

Random object created and used only once.
} else {
int randomIndex = new Random().nextInt(suitableCoords.size());
return suitableCoords.get(randomIndex);
}
}

/**
* Searches for and returns a suitable adjacent coordinate to the given origin coordinates on
* the provided {@link StratconTrackState}.
* The method checks all the possible directions and considers a coordinate suitable if it
* doesn't contain a scenario and if it either doesn't contain a facility or contains a
* player-allied one.
* If there are multiple suitable coordinates, one is chosen at random.
*
* @param originCoords the {@link StratconCoords} around which to search for a suitable coordinate
* @param trackState the {@link StratconTrackState} on which to perform the search
* @return a {@link StratconCoords} object representing the coordinates of a suitable adjacent
* location, or {code null} if no suitable location was found.
*/
public static @Nullable StratconCoords getUnoccupiedAdjacentCoords(StratconCoords originCoords,
StratconTrackState trackState) {
List<StratconCoords> suitableCoords = new ArrayList<>();

for (int direction : ALL_DIRECTIONS) {
StratconCoords newCoords = originCoords.translate(direction);

if (trackState.getScenario(newCoords) != null) {
continue;
}

if (trackState.getFacility(newCoords) == null) {
suitableCoords.add(newCoords);
continue;
}

if (trackState.getFacility(newCoords).getOwner() != ForceAlignment.Opposing) {
suitableCoords.add(newCoords);
}
}

if (attempts == maxAttempts) {
if (suitableCoords.isEmpty()) {
return null;
}

Check warning

Code scanning / CodeQL

Random used only once Warning

Random object created and used only once.

return coords;
int randomIndex = new Random().nextInt(suitableCoords.size());
return suitableCoords.get(randomIndex);
}

/**
Expand Down
Loading