Skip to content

Commit

Permalink
Merge pull request #4859 from IllianiCBT/morale_rework
Browse files Browse the repository at this point in the history
Reworked AtB Morale, Rebranding it as MekHQ Morale
  • Loading branch information
IllianiCBT authored Sep 26, 2024
2 parents b3314a8 + 2821393 commit 6fd4a31
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 235 deletions.
Binary file not shown.
28 changes: 14 additions & 14 deletions MekHQ/resources/mekhq/resources/Mission.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 19 additions & 3 deletions MekHQ/src/mekhq/campaign/Campaign.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -3678,6 +3679,17 @@ && getLocation().getJumpPath().getLastSystem().getId().equals(contract.getSystem
}
}

/**
* Processes the new day actions for various AtB systems
* <p>
* 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);

Expand Down Expand Up @@ -3752,9 +3764,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 = "<html>Current enemy condition is '" + morale + "' on contract " + contract.getName() + "<br><br>" + morale.getToolTipText() + "</html>";

addReport(report);
}
}

Expand Down
23 changes: 9 additions & 14 deletions MekHQ/src/mekhq/campaign/force/Lance.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
148 changes: 78 additions & 70 deletions MekHQ/src/mekhq/campaign/mission/AtBContract.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -180,7 +179,7 @@ public AtBContract(String name) {

sharesPct = 0;
batchallAccepted = true;
setMoraleLevel(AtBMoraleLevel.NORMAL);
setMoraleLevel(AtBMoraleLevel.STALEMATE);
routEnd = null;
numBonusParts = 0;
priorLogisticsFailure = false;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
}
Expand Down
Loading

0 comments on commit 6fd4a31

Please sign in to comment.