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

Reworked AtB Morale, Rebranding it as MekHQ Morale #4859

Merged
merged 3 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted to rename the levels for three reasons:

  • to add distance from official morale rules, so there can be no confusion that this is unofficial
  • to better reflect how we're currently using these morale levels, as not just a measurement of enemy mental wellbeing, but also material capacity to fight back.
  • with some of the other user-facing terminology changes, this helps give the illusion of fighting a persistent enemy

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 @@ -3517,6 +3518,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 @@ -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 = "<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