diff --git a/MekHQ/docs/Stratcon and Against the Bot/MekHQ Morale.pdf b/MekHQ/docs/Stratcon and Against the Bot/MekHQ Morale.pdf new file mode 100644 index 0000000000..5a504c5560 Binary files /dev/null and b/MekHQ/docs/Stratcon and Against the Bot/MekHQ Morale.pdf differ diff --git a/MekHQ/resources/mekhq/resources/Mission.properties b/MekHQ/resources/mekhq/resources/Mission.properties index 71e6e21395..6b82d8b25b 100644 --- a/MekHQ/resources/mekhq/resources/Mission.properties +++ b/MekHQ/resources/mekhq/resources/Mission.properties @@ -42,20 +42,20 @@ AtBLanceRole.UNASSIGNED.text=Unassigned AtBLanceRole.UNASSIGNED.toolTipText=The lance is not currently assigned to combat duties. # AtBMoraleLevel Enum -AtBMoraleLevel.BROKEN.text=Broken -AtBMoraleLevel.BROKEN.toolTipText=The unit's morale has broken, and its men are in full retreat. -AtBMoraleLevel.VERY_LOW.text=Very Low -AtBMoraleLevel.VERY_LOW.toolTipText=The unit is on precipice of breaking, their leadership barely holding the unit together. -AtBMoraleLevel.LOW.text=Low -AtBMoraleLevel.LOW.toolTipText=The unit is demoralized, but is still fighting cohesively. -AtBMoraleLevel.NORMAL.text=Normal -AtBMoraleLevel.NORMAL.toolTipText=The unit is ready to fight. -AtBMoraleLevel.HIGH.text=High -AtBMoraleLevel.HIGH.toolTipText=The unit is ready and glad to fight. -AtBMoraleLevel.VERY_HIGH.text=Very High -AtBMoraleLevel.VERY_HIGH.toolTipText=The unit is dedicated to their cause. -AtBMoraleLevel.UNBREAKABLE.text=Unbreakable -AtBMoraleLevel.UNBREAKABLE.toolTipText=The unit's trust in their leadership and dedication to their cause is nigh-on unbreakable, and they look forward to fighting their enemies. +AtBMoraleLevel.ROUTED.text=Routed +AtBMoraleLevel.ROUTED.toolTipText=The enemy is in full retreat, suffering devastating losses and scattered. They pose no significant threat and are incapable of organizing a counterattack. +AtBMoraleLevel.CRITICAL.text=Critical +AtBMoraleLevel.CRITICAL.toolTipText=The enemy is in a dire state, with most of their forces destroyed or incapacitated. Their ability to fight is severely compromised, and morale is near breaking. +AtBMoraleLevel.WEAKENED.text=Weakened +AtBMoraleLevel.WEAKENED.toolTipText=The enemy is losing ground, sustaining significant casualties, and is disorganized. However, they can still put up resistance in isolated areas. +AtBMoraleLevel.STALEMATE.text=Stalemate +AtBMoraleLevel.STALEMATE.toolTipText=Both sides are evenly matched, with neither gaining a clear advantage. Skirmishes continue, but the outcome remains uncertain. +AtBMoraleLevel.ADVANCING.text=Advancing +AtBMoraleLevel.ADVANCING.toolTipText=The enemy is gaining momentum, making coordinated strikes, and forcing your forces to fall back. They are beginning to dominate key areas of the battlefield. +AtBMoraleLevel.DOMINATING.text=Dominating +AtBMoraleLevel.DOMINATING.toolTipText=The enemy has the upper hand, controlling critical objectives and inflicting heavy casualties. Your forces are under significant pressure, and defeat is imminent. +AtBMoraleLevel.OVERWHELMING.text=Overwhelming +AtBMoraleLevel.OVERWHELMING.toolTipText=The enemy is completely overwhelming your forces, executing a final push for total victory. Your forces are on the verge of collapse, with no hope of recovery. # ContractCommandRights Enum ContractCommandRights.INTEGRATED.text=Integrated diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 0c4448bda2..551831e652 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -67,6 +67,7 @@ import mekhq.campaign.mission.*; import mekhq.campaign.mission.atb.AtBScenarioFactory; import mekhq.campaign.mission.enums.AtBLanceRole; +import mekhq.campaign.mission.enums.AtBMoraleLevel; import mekhq.campaign.mission.enums.MissionStatus; import mekhq.campaign.mission.enums.ScenarioStatus; import mekhq.campaign.mod.am.InjuryUtil; @@ -3517,6 +3518,17 @@ && getLocation().getJumpPath().getLastSystem().getId().equals(contract.getSystem } } + /** + * Processes the new day actions for various AtB systems + *
+ * It generates contract offers in the contract market,
+ * updates ship search expiration and results,
+ * processes ship search on Mondays,
+ * awards training experience to eligible training lances on active contracts on Mondays,
+ * adds or removes dependents at the start of the year if the options are enabled,
+ * rolls for morale at the start of the month,
+ * and processes ATB scenarios.
+ */
private void processNewDayATB() {
contractMarket.generateContractOffers(this);
@@ -3590,9 +3602,13 @@ && getCampaignOptions().getRandomDependentMethod().isAgainstTheBot()
}
for (AtBContract contract : getActiveAtBContracts()) {
- contract.checkMorale(this, getLocalDate(), getAtBUnitRatingMod());
- addReport("Enemy Morale is now " + contract.getMoraleLevel()
- + " on contract " + contract.getName());
+ contract.checkMorale(this, getLocalDate());
+
+ AtBMoraleLevel morale = contract.getMoraleLevel();
+
+ String report = "Current enemy condition is '" + morale + "' on contract " + contract.getName() + "
" + morale.getToolTipText() + "";
+
+ addReport(report);
}
}
diff --git a/MekHQ/src/mekhq/campaign/force/Lance.java b/MekHQ/src/mekhq/campaign/force/Lance.java
index 80ad56c3a4..c22758c257 100644
--- a/MekHQ/src/mekhq/campaign/force/Lance.java
+++ b/MekHQ/src/mekhq/campaign/force/Lance.java
@@ -21,19 +21,7 @@
*/
package mekhq.campaign.force;
-import java.io.PrintWriter;
-import java.time.LocalDate;
-import java.util.UUID;
-
-import org.w3c.dom.NamedNodeMap;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
-import megamek.common.Compute;
-import megamek.common.Entity;
-import megamek.common.EntityWeightClass;
-import megamek.common.Infantry;
-import megamek.common.UnitType;
+import megamek.common.*;
import megamek.logging.MMLogger;
import mekhq.campaign.Campaign;
import mekhq.campaign.mission.AtBContract;
@@ -45,6 +33,13 @@
import mekhq.campaign.unit.Unit;
import mekhq.campaign.universe.Faction;
import mekhq.utilities.MHQXMLUtility;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.PrintWriter;
+import java.time.LocalDate;
+import java.util.UUID;
/**
* Used by Against the Bot to track additional information about each force
@@ -288,7 +283,7 @@ public AtBScenario checkForBattle(Campaign c) {
int roll;
// thresholds are coded from charts with 1-100 range, so we add 1 to mod to
// adjust 0-based random int
- int battleTypeMod = 1 + (AtBMoraleLevel.NORMAL.ordinal() - getContract(c).getMoraleLevel().ordinal()) * 5;
+ int battleTypeMod = 1 + (AtBMoraleLevel.STALEMATE.ordinal() - getContract(c).getMoraleLevel().ordinal()) * 5;
battleTypeMod += getContract(c).getBattleTypeMod();
// debugging code that will allow you to force the generation of a particular
diff --git a/MekHQ/src/mekhq/campaign/mission/AtBContract.java b/MekHQ/src/mekhq/campaign/mission/AtBContract.java
index e798589b76..ddfe096aea 100644
--- a/MekHQ/src/mekhq/campaign/mission/AtBContract.java
+++ b/MekHQ/src/mekhq/campaign/mission/AtBContract.java
@@ -41,7 +41,6 @@
import mekhq.campaign.mission.enums.AtBMoraleLevel;
import mekhq.campaign.personnel.Bloodname;
import mekhq.campaign.personnel.Person;
-import mekhq.campaign.personnel.SkillType;
import mekhq.campaign.personnel.backgrounds.BackgroundsController;
import mekhq.campaign.personnel.enums.Phenotype;
import mekhq.campaign.rating.IUnitRating;
@@ -180,7 +179,7 @@ public AtBContract(String name) {
sharesPct = 0;
batchallAccepted = true;
- setMoraleLevel(AtBMoraleLevel.NORMAL);
+ setMoraleLevel(AtBMoraleLevel.STALEMATE);
routEnd = null;
numBonusParts = 0;
priorLogisticsFailure = false;
@@ -246,111 +245,120 @@ public static boolean isMinorPower(final String factionCode) {
}
/**
- * Checks the morale level of the campaign based on various factors.
+ * Checks and updates the morale which depends on various conditions such as the rout end date,
+ * skill levels, victories, defeats, etc. This method also updates the enemy status based on the
+ * morale level.
*
- * @param campaign The ongoing campaign.
- * @param today The current date.
- * @param dragoonRating The player's dragoon rating
+ * @param today The current date in the context.
*/
- public void checkMorale(Campaign campaign, LocalDate today, int dragoonRating) {
- if (null != routEnd) {
+ 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;
+
+ // 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.NORMAL);
+ setMoraleLevel(AtBMoraleLevel.STALEMATE);
routEnd = null;
updateEnemy(campaign, today); // mix it up a little
} else {
- setMoraleLevel(AtBMoraleLevel.BROKEN);
+ setMoraleLevel(AtBMoraleLevel.ROUTED);
}
return;
}
+
+ // Initialize counters for victories and defeats
int victories = 0;
int defeats = 0;
LocalDate lastMonth = today.minusMonths(1);
- for (Scenario s : getScenarios()) {
- if ((s.getDate() != null) && lastMonth.isAfter(s.getDate())) {
+ // Loop through scenarios, counting victories and defeats that fall within the target month
+ for (Scenario scenario : getScenarios()) {
+ if ((scenario.getDate() != null) && lastMonth.isAfter(scenario.getDate())) {
continue;
}
- if (s.getStatus().isOverallVictory()) {
+ if (scenario.getStatus().isOverallVictory()) {
victories++;
- } else if (s.getStatus().isOverallDefeat()) {
+ } else if (scenario.getStatus().isOverallDefeat()) {
defeats++;
}
- }
- //
- // From: Official AtB Rules 2.31
- //
-
- // Enemy skill rating: Green -1, Veteran +1, Elite +2
- int mod = Math.max(getEnemySkill().ordinal() - 3, -1);
+ if (scenario.getStatus().isDecisiveVictory()) {
+ victories++;
+ } else if (scenario.getStatus().isDecisiveDefeat()) {
+ defeats++;
+ } else if (scenario.getStatus().isPyrrhicVictory()) {
+ victories--;
+ }
+ }
- // Player Dragoon/MRBC rating: F +2, D +1, B -1, A -2
- mod -= dragoonRating - IUnitRating.DRAGOON_C;
+ // Calculate various modifiers for morale
+ int enemySkillModifier = getEnemySkill().getAdjustedValue() - SkillLevel.REGULAR.getAdjustedValue();
+ int allySkillModifier = getAllySkill().getAdjustedValue() - SkillLevel.REGULAR.getAdjustedValue();
- // For every 5 player victories in last month: -1
- mod -= victories / 5;
+ int performanceModifier = 0;
- // For every 2 player defeats in last month: +1
- mod += defeats / 2;
+ if (victories > (defeats * 2)) {
+ performanceModifier -= 2;
+ } else if (victories > defeats) {
+ performanceModifier--;
+ } else if (defeats > (victories * 2)) {
+ performanceModifier += 2;
+ } else {
+ performanceModifier++;
+ }
- // "Several weekly events affect the morale roll, so, beyond the
- // modifiers presented here, notice that some events add
- // bonuses/minuses to this roll."
- mod += moraleMod;
+ int miscModifiers = moraleMod;
- // Enemy type: Pirates: -2
- // Rebels/Mercs/Minor factions: -1
- // Clans: +2
+ // Additional morale modifications depending on faction properties
if (Factions.getInstance().getFaction(enemyCode).isPirate()) {
- mod -= 2;
- } else if (Factions.getInstance().getFaction(enemyCode).isRebel() ||
- isMinorPower(enemyCode) ||
- Factions.getInstance().getFaction(enemyCode).isMercenary()) {
- mod -= 1;
+ 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()) {
- mod += 2;
+ miscModifiers += 2;
}
- // If no player victories in last month: +1
- if (victories == 0) {
- mod++;
- }
-
- // If no player defeats in last month: -1
- if (defeats == 0) {
- mod--;
- }
-
- // After finding the applicable modifiers, roll according to the
- // following table to find the new morale level:
- // 1 or less: Morale level decreases 2 levels
- // 2 – 5: Morale level decreases 1 level
- // 6 – 8: Morale level remains the same
- // 9 - 12: Morale level increases 1 level
- // 13 or more: Morale increases 2 levels
- int roll = Compute.d6(2) + mod;
+ // Total morale modifier calculation
+ int totalModifier = enemySkillModifier - allySkillModifier + performanceModifier + miscModifiers;
+ int roll = Compute.d6(2) + totalModifier;
+ // Morale level determination based on roll value
final AtBMoraleLevel[] moraleLevels = AtBMoraleLevel.values();
- if (roll <= 1) {
+
+ if (roll < 2) {
setMoraleLevel(moraleLevels[Math.max(getMoraleLevel().ordinal() - 2, 0)]);
- } else if (roll <= 5) {
+ } else if (roll < 5) {
setMoraleLevel(moraleLevels[Math.max(getMoraleLevel().ordinal() - 1, 0)]);
- } else if ((roll >= 9) && (roll <= 12)) {
- setMoraleLevel(moraleLevels[Math.min(getMoraleLevel().ordinal() + 1, moraleLevels.length - 1)]);
- } else if (roll >= 13) {
+ } else if ((roll > 12)) {
setMoraleLevel(moraleLevels[Math.min(getMoraleLevel().ordinal() + 2, moraleLevels.length - 1)]);
+ } else if ((roll > 9)) {
+ setMoraleLevel(moraleLevels[Math.min(getMoraleLevel().ordinal() + 1, moraleLevels.length - 1)]);
}
- // Enemy defeated, retreats or do not offer opposition to the player
- // forces, equal to a early victory for contracts that are not
- // Garrison-type, and a 1d6-3 (minimum 1) months without enemy
- // activity for Garrison-type contracts.
- if (getMoraleLevel().isRout() && getContractType().isGarrisonType()) {
- routEnd = today.plusMonths(Math.max(1, Compute.d6() - 3)).minusDays(1);
+ // Additional morale updates if morale level is set to 'Routed' and contract type is a garrison type
+ if (getMoraleLevel().isRouted()) {
+ if (getContractType().isGarrisonType()) {
+ routEnd = today.plusMonths(Math.max(1, Compute.d6() - 3)).minusDays(1);
+ } else {
+ campaign.addReport("With the enemy routed, any remaining objectives have been successfully completed." +
+ " 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;
}
+ // Reset external morale modifier
moraleMod = 0;
}
@@ -446,7 +454,7 @@ public int getScore() {
&& (((AtBScenario) s).getScenarioType() == AtBScenario.BASEATTACK)
&& ((AtBScenario) s).isAttacker() && s.getStatus().isOverallVictory()) {
earlySuccess = true;
- } else if (getMoraleLevel().isRout() && !getContractType().isGarrisonType()) {
+ } else if (getMoraleLevel().isRouted() && !getContractType().isGarrisonType()) {
earlySuccess = true;
}
}
diff --git a/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioFactory.java
index 5786e9444d..cb63e6ff0b 100644
--- a/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioFactory.java
+++ b/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioFactory.java
@@ -18,14 +18,6 @@
*/
package mekhq.campaign.mission.atb;
-import java.time.LocalDate;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-
import megamek.codeUtilities.ObjectUtility;
import megamek.logging.MMLogger;
import mekhq.campaign.Campaign;
@@ -34,6 +26,9 @@
import mekhq.campaign.mission.AtBScenario;
import mekhq.campaign.mission.atb.scenario.*;
+import java.time.LocalDate;
+import java.util.*;
+
public class AtBScenarioFactory {
private static final MMLogger logger = MMLogger.create(AtBScenarioFactory.class);
@@ -192,7 +187,7 @@ public static void createScenariosForNewWeek(Campaign c) {
// Don't generate scenarios for contracts with morale below the morale limit of
// Low
- if (contract.getMoraleLevel().isVeryLow() || contract.getMoraleLevel().isRout()) {
+ if (contract.getMoraleLevel().isCritical() || contract.getMoraleLevel().isRouted()) {
continue;
}
@@ -222,72 +217,74 @@ public static void createScenariosForNewWeek(Campaign c) {
}
// endregion Generate Scenarios
- // region Unbreakable Morale Missions
- // Make sure Unbreakable morale missions have a base attack scenario generated
- if (!hasBaseAttack && contract.getMoraleLevel().isUnbreakable()) {
- /*
- * find a lance to act as defender, giving preference
- * first to those assigned to the same contract,
- * then to those assigned to defense roles
- */
- List