diff --git a/MekHQ/docs/Stratcon and Against the Bot/MekHQ Morale.pdf b/MekHQ/docs/Stratcon and Against the Bot/MekHQ Morale.pdf
index 5a504c5560..c62599e1f0 100644
Binary files a/MekHQ/docs/Stratcon and Against the Bot/MekHQ Morale.pdf and b/MekHQ/docs/Stratcon and Against the Bot/MekHQ Morale.pdf differ
diff --git a/MekHQ/resources/mekhq/resources/Campaign.properties b/MekHQ/resources/mekhq/resources/Campaign.properties
index f83243e46a..a1fcdde7df 100644
--- a/MekHQ/resources/mekhq/resources/Campaign.properties
+++ b/MekHQ/resources/mekhq/resources/Campaign.properties
@@ -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 %s on contract %s.\
+
\
+
%s
\ No newline at end of file
diff --git a/MekHQ/resources/mekhq/resources/ContractMarketDialog.properties b/MekHQ/resources/mekhq/resources/ContractMarketDialog.properties
index b8f7b3021d..5a4d46bfec 100644
--- a/MekHQ/resources/mekhq/resources/ContractMarketDialog.properties
+++ b/MekHQ/resources/mekhq/resources/ContractMarketDialog.properties
@@ -99,3 +99,16 @@ messageChallengeVeryHard.text=We've reviewed the mission details, and it's our d
\
If you are committed to this course of action, confirm your deployment.\
Otherwise, you may return to review more suitable assignments.
+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.\
+
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.\
+
\
+
If you are prepared to assume this role, confirm your deployment.\
+
Otherwise, you may return to review other opportunities.
diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java
index 2d7aa84b72..ced9b4ef54 100644
--- a/MekHQ/src/mekhq/campaign/Campaign.java
+++ b/MekHQ/src/mekhq/campaign/Campaign.java
@@ -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;
}
@@ -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 " + morale + " on contract "
- + contract.getName() + "
" + 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);
+ }
}
}
diff --git a/MekHQ/src/mekhq/campaign/mission/AtBContract.java b/MekHQ/src/mekhq/campaign/mission/AtBContract.java
index 9b45452716..da8806c288 100644
--- a/MekHQ/src/mekhq/campaign/mission/AtBContract.java
+++ b/MekHQ/src/mekhq/campaign/mission/AtBContract.java
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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";
@@ -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()));
@@ -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);
@@ -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)) {
@@ -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();
@@ -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
@@ -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++;
}
@@ -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 unit market",
@@ -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 {
diff --git a/MekHQ/src/mekhq/campaign/mission/enums/AtBContractType.java b/MekHQ/src/mekhq/campaign/mission/enums/AtBContractType.java
index 155507eb8e..442e386053 100644
--- a/MekHQ/src/mekhq/campaign/mission/enums/AtBContractType.java
+++ b/MekHQ/src/mekhq/campaign/mission/enums/AtBContractType.java
@@ -18,8 +18,6 @@
*/
package mekhq.campaign.mission.enums;
-import java.util.ResourceBundle;
-
import megamek.common.Compute;
import megamek.logging.MMLogger;
import mekhq.MekHQ;
@@ -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
@@ -138,6 +138,16 @@ public boolean isGarrisonType() {
public boolean isRaidType() {
return isDiversionaryRaid() || isObjectiveRaid() || isReconRaid() || isExtractionRaid();
}
+
+ /**
+ * Checks if the given contract is offensive.
+ *
+ * @return {@code true} if the contract is of raid type, pirate hunting, or guerrilla warfare;
+ * {@code false} otherwise
+ */
+ public boolean isOffensive() {
+ return isRaidType() || isPirateHunting() || isGuerrillaWarfare() || isPlanetaryAssault();
+ }
// endregion Boolean Comparison Methods
public int calculateLength(final boolean variable, final AtBContract contract) {
diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java b/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java
index 2a18790323..5158a35a45 100644
--- a/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java
+++ b/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java
@@ -19,19 +19,32 @@
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.AtBMoraleLevel;
import mekhq.campaign.mission.enums.ContractCommandRights;
import mekhq.campaign.stratcon.StratconContractDefinition.ObjectiveParameters;
import mekhq.campaign.stratcon.StratconContractDefinition.StrategicObjectiveType;
+import mekhq.campaign.universe.Faction;
+import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Random;
+
+import static java.lang.Math.ceil;
+import static java.lang.Math.round;
+import static megamek.common.Coords.ALL_DIRECTIONS;
+import static mekhq.campaign.rating.IUnitRating.*;
+import static mekhq.campaign.stratcon.StratconRulesManager.addHiddenExternalScenario;
+import static mekhq.campaign.stratcon.StratconRulesManager.processMassRout;
/**
* This class handles StratCon state initialization when a contract is signed.
@@ -70,6 +83,7 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai
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()
@@ -78,7 +92,7 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai
.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);
}
@@ -94,7 +108,7 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai
.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);
}
@@ -184,6 +198,11 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai
false, Collections.emptyList());
}
+ // Initialize non-objective scenarios
+ for (StratconTrackState track : campaignState.getTracks()) {
+ seedPreDeployedForces(contract, campaign, track, true);
+ }
+
// 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
@@ -195,28 +214,96 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai
}
}
+ // Determine starting morale
+ if (contract.getContractType().isGarrisonDuty()) {
+ contract.setMoraleLevel(AtBMoraleLevel.ROUTED);
+
+ LocalDate routEnd = contract.getStartDate().plusMonths(Math.max(1, Compute.d6() - 3)).minusDays(1);
+ contract.setRoutEndDate(routEnd);
+
+ processMassRout(campaignState, true);
+ } else {
+ contract.checkMorale(campaign, campaign.getLocalDate());
+
+ if (contract.getMoraleLevel().isRouted()) {
+ contract.setMoraleLevel(AtBMoraleLevel.CRITICAL);
+ }
+ }
+
// now we're done
}
+ /**
+ * Seeds pre-deployed (hidden) forces in a {@link StratconTrackState}, taking into account
+ * contract type, enemy quality, and enemy faction.
+ *
+ * @param contract the current contract
+ * @param campaign the current campaign.
+ * @param track the relevant {@link StratconTrackState}
+ * @param isEnemy whether we are seeding forces for the enemy, or player's allies
+ */
+ public static int seedPreDeployedForces(AtBContract contract, Campaign campaign,
+ StratconTrackState track, boolean isEnemy) {
+ // TODO remove reductions once we have friendly forces deploying too
+ final int CLAN_CLUSTER = 11; // 22 Stars, reduced to 11
+ final int IS_BATTALION = 14; // 27 Lances, reduced to 14
+ final int COMSTAR_LEVEL_IV = 18; // 36 Level IIs, reduced to 18
+
+ int quality = isEnemy ? contract.getEnemyQuality() : contract.getAllyQuality();
+ double multiplier = switch (quality) {
+ case DRAGOON_F -> 0.25;
+ case DRAGOON_D -> 0.5;
+ case DRAGOON_C -> 0.75;
+ case DRAGOON_A -> 1.5;
+ case DRAGOON_ASTAR -> 2;
+ default -> 1; // DRAGOON_B
+ };
+
+ AtBContractType contractType = contract.getContractType();
+
+ if (contractType.isPirateHunting() || contractType.isGarrisonType()) {
+ multiplier *= 0.5;
+ } else if (contractType.isPlanetaryAssault()) {
+ multiplier *= 2;
+ }
+
+ int elementCount = IS_BATTALION;
+
+ Faction faction = isEnemy ? contract.getEnemy() : contract.getEmployerFaction();
+
+ if (faction.isClan()) {
+ elementCount = CLAN_CLUSTER;
+ } else if (faction.isComStarOrWoB()) {
+ elementCount = COMSTAR_LEVEL_IV;
+ }
+
+ elementCount = (int) ceil(elementCount * multiplier);
+
+ for (int i = 0; i < elementCount; i++) {
+ addHiddenExternalScenario(campaign, contract, track, null, false);
+ }
+
+ return elementCount;
+ }
+
/**
* 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);
@@ -287,7 +374,7 @@ private static void initializeTrackFacilities(StratconTrackState trackState, int
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," +
@@ -341,7 +428,7 @@ private static void initializeObjectiveScenarios(Campaign campaign, AtBContract
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," +
@@ -387,30 +474,123 @@ private static void initializeObjectiveScenarios(Campaign campaign, AtBContract
}
/**
- * 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).
+ *
+ * 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 @Nullable StratconCoords getUnoccupiedCoords(StratconTrackState trackState, boolean allowPlayerFacilities) {
+ int trackHeight = trackState.getHeight();
+ int trackWidth = trackState.getWidth();
+
+ List
+ * Loops through all tracks in the campaign state.
+ * For each track, it retrieves all scenarios.
+ * If scenario's deployment date is {@code null} and scenario is not a strategic objective,
+ * it is removed from the track and then updated.
+ *
+ * @param campaignState the relevant StratCon campaign state.
+ * @param removeAll whether to remove all scenarios, including those with dates or
+ * strategic objectives. This should be used sparingly.
+ */
+ public static void processMassRout(StratconCampaignState campaignState, boolean removeAll) {
+ for (StratconTrackState track : campaignState.getTracks()) {
+ List