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 0a31a8fc22..a72aea8008 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; @@ -3678,6 +3679,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); @@ -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 = "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 9b57aa6f78..d14e1e822e 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 lList = new ArrayList<>(); - for (Lance l : lances.values()) { - if ((l.getMissionId() == contract.getId()) && l.getRole().isDefence() && l.isEligible(c)) { - lList.add(l); - } - } - - if (lList.isEmpty()) { + // region Overwhelming Morale Missions + // Make sure Overwhelming morale missions have a base attack scenario generated + if (!c.getCampaignOptions().isUseStratCon()) { + if (!hasBaseAttack && contract.getMoraleLevel().isOverwhelming()) { + /* + * 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 lList = new ArrayList<>(); for (Lance l : lances.values()) { - if ((l.getMissionId() == contract.getId()) && l.isEligible(c)) { + if ((l.getMissionId() == contract.getId()) && l.getRole().isDefence() && l.isEligible(c)) { lList.add(l); } } - } - if (lList.isEmpty()) { - for (Lance l : lances.values()) { - if (l.isEligible(c)) { - lList.add(l); + if (lList.isEmpty()) { + for (Lance l : lances.values()) { + if ((l.getMissionId() == contract.getId()) && l.isEligible(c)) { + lList.add(l); + } + } + } + + if (lList.isEmpty()) { + for (Lance l : lances.values()) { + if (l.isEligible(c)) { + lList.add(l); + } } } - } - if (!lList.isEmpty()) { - Lance lance = ObjectUtility.getRandomItem(lList); - AtBScenario atbScenario = AtBScenarioFactory.createScenario(c, lance, - AtBScenario.BASEATTACK, false, Lance.getBattleDate(c.getLocalDate())); - if (atbScenario != null) { - if ((lance.getMissionId() == atbScenario.getMissionId()) - || (lance.getMissionId() == Lance.NO_MISSION)) { - for (int i = 0; i < sList.size(); i++) { - if (sList.get(i).getLanceForceId() == lance.getForceId()) { - if (dontGenerateForces.contains(atbScenario.getId())) { - dontGenerateForces.remove(atbScenario.getId()); + if (!lList.isEmpty()) { + Lance lance = ObjectUtility.getRandomItem(lList); + AtBScenario atbScenario = AtBScenarioFactory.createScenario(c, lance, + AtBScenario.BASEATTACK, false, Lance.getBattleDate(c.getLocalDate())); + if (atbScenario != null) { + if ((lance.getMissionId() == atbScenario.getMissionId()) + || (lance.getMissionId() == Lance.NO_MISSION)) { + for (int i = 0; i < sList.size(); i++) { + if (sList.get(i).getLanceForceId() == lance.getForceId()) { + if (dontGenerateForces.contains(atbScenario.getId())) { + dontGenerateForces.remove(atbScenario.getId()); + } + sList.set(i, atbScenario); + break; } - sList.set(i, atbScenario); - break; } + } else { + // edge case: lance assigned to another mission gets assigned the scenario, + // we need to remove any scenario they are assigned to already + c.getMission(lance.getMissionId()).getScenarios() + .removeIf(scenario -> (scenario instanceof AtBScenario) + && (((AtBScenario) scenario).getLanceForceId() == lance.getForceId())); + } + if (!sList.contains(atbScenario)) { + sList.add(atbScenario); + } + if (!assignedLances.contains(lance.getForceId())) { + assignedLances.add(lance.getForceId()); } } else { - // edge case: lance assigned to another mission gets assigned the scenario, - // we need to remove any scenario they are assigned to already - c.getMission(lance.getMissionId()).getScenarios() - .removeIf(scenario -> (scenario instanceof AtBScenario) - && (((AtBScenario) scenario).getLanceForceId() == lance.getForceId())); - } - if (!sList.contains(atbScenario)) { - sList.add(atbScenario); - } - if (!assignedLances.contains(lance.getForceId())) { - assignedLances.add(lance.getForceId()); + logger.error("Unable to generate Base Attack scenario."); } } else { - logger.error("Unable to generate Base Attack scenario."); + logger.warn("No lances assigned to mission " + contract.getName() + + ". Can't generate an Unbreakable Morale base defense mission for this force."); } - } else { - logger.warn("No lances assigned to mission " + contract.getName() - + ". Can't generate an Unbreakable Morale base defense mission for this force."); } } // endregion Unbreakable Morale Missions diff --git a/MekHQ/src/mekhq/campaign/mission/enums/AtBMoraleLevel.java b/MekHQ/src/mekhq/campaign/mission/enums/AtBMoraleLevel.java index 324591bca9..54cbbe5342 100644 --- a/MekHQ/src/mekhq/campaign/mission/enums/AtBMoraleLevel.java +++ b/MekHQ/src/mekhq/campaign/mission/enums/AtBMoraleLevel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2021-2024 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -18,20 +18,23 @@ */ package mekhq.campaign.mission.enums; -import java.util.ResourceBundle; - import megamek.logging.MMLogger; import mekhq.MekHQ; +import java.util.ResourceBundle; + +/** + * The AtBMoraleLevel enum represents the different enemy morale conditions used by AtB systems. + */ public enum AtBMoraleLevel { // region Enum Declarations - BROKEN("AtBMoraleLevel.BROKEN.text", "AtBMoraleLevel.BROKEN.toolTipText"), - VERY_LOW("AtBMoraleLevel.VERY_LOW.text", "AtBMoraleLevel.VERY_LOW.toolTipText"), - LOW("AtBMoraleLevel.LOW.text", "AtBMoraleLevel.LOW.toolTipText"), - NORMAL("AtBMoraleLevel.NORMAL.text", "AtBMoraleLevel.NORMAL.toolTipText"), - HIGH("AtBMoraleLevel.HIGH.text", "AtBMoraleLevel.HIGH.toolTipText"), - VERY_HIGH("AtBMoraleLevel.VERY_HIGH.text", "AtBMoraleLevel.VERY_HIGH.toolTipText"), - UNBREAKABLE("AtBMoraleLevel.UNBREAKABLE.text", "AtBMoraleLevel.UNBREAKABLE.toolTipText"); + ROUTED("AtBMoraleLevel.ROUTED.text", "AtBMoraleLevel.ROUTED.toolTipText"), + CRITICAL("AtBMoraleLevel.CRITICAL.text", "AtBMoraleLevel.CRITICAL.toolTipText"), + WEAKENED("AtBMoraleLevel.WEAKENED.text", "AtBMoraleLevel.WEAKENED.toolTipText"), + STALEMATE("AtBMoraleLevel.STALEMATE.text", "AtBMoraleLevel.STALEMATE.toolTipText"), + ADVANCING("AtBMoraleLevel.ADVANCING.text", "AtBMoraleLevel.ADVANCING.toolTipText"), + DOMINATING("AtBMoraleLevel.DOMINATING.text", "AtBMoraleLevel.DOMINATING.toolTipText"), + OVERWHELMING("AtBMoraleLevel.OVERWHELMING.text", "AtBMoraleLevel.OVERWHELMING.toolTipText"); // endregion Enum Declarations // region Variable Declarations @@ -39,6 +42,12 @@ public enum AtBMoraleLevel { private final String toolTipText; // endregion Variable Declarations + /** + * Initializes a new {@link AtBMoraleLevel} object with the specified name and tooltip text. + * + * @param name the resource key for the name of the Morale Level + * @param toolTipText the resource key for the tooltip text of the Morale Level + */ // region Constructors AtBMoraleLevel(final String name, final String toolTipText) { final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Mission", @@ -48,6 +57,11 @@ public enum AtBMoraleLevel { } // endregion Constructors + /** + * Retrieves the tooltip text associated with this object. + * + * @return the tooltip text + */ // region Getters public String getToolTipText() { return toolTipText; @@ -55,73 +69,141 @@ public String getToolTipText() { // endregion Getters // region Boolean Comparison Methods - public boolean isRout() { - return this == BROKEN; + /** + * Checks if the current object is equal to the value of {@code ROUTED}. + * + * @return {@code true} if the current object is equal to {@code ROUTED}, {@code false} otherwise. + */ + public boolean isRouted() { + return this == ROUTED; } - public boolean isVeryLow() { - return this == VERY_LOW; + /** + * Checks if the current object is equal to the value of {@code CRITICAL}. + * + * @return {@code true} if the current object is equal to {@code CRITICAL}, {@code false} + * otherwise. + */ + public boolean isCritical() { + return this == CRITICAL; } - public boolean isLow() { - return this == LOW; + /** + * Checks if the current object is equal to the value of {@code WEAKENED}. + * + * @return {@code true} if the current object is equal to {@code WEAKENED}, {@code false} + * otherwise. + */ + public boolean isWeakened() { + return this == WEAKENED; } - public boolean isNormal() { - return this == NORMAL; + /** + * Checks if the current object is equal to the value of {@code STALEMATE}. + * + * @return {@code true} if the current object is equal to {@code STALEMATE}, {@code false} + * otherwise. + */ + public boolean isStalemate() { + return this == STALEMATE; } - public boolean isHigh() { - return this == HIGH; + /** + * Checks if the current object is equal to the value of {@code ADVANCING}. + * + * @return {@code true} if the current object is equal to {@code ADVANCING}, {@code false} + * otherwise. + */ + public boolean isAdvancing() { + return this == ADVANCING; } - public boolean isVeryHigh() { - return this == VERY_HIGH; + /** + * Checks if the current object is equal to the value of {@code DOMINATING}. + * + * @return {@code true} if the current object is equal to {@code DOMINATING}, {@code false} + * otherwise. + */ + public boolean isDominating() { + return this == DOMINATING; } - public boolean isUnbreakable() { - return this == UNBREAKABLE; + /** + * Checks if the current object is equal to the value of {@code OVERWHELMING}. + * + * @return {@code true} if the current object is equal to {@code OVERWHELMING}, {@code false} + * otherwise. + */ + public boolean isOverwhelming() { + return this == OVERWHELMING; } // endregion Boolean Comparison Methods - // region File I/O /** - * @param text containing the AtBMoraleLevel - * @return the saved AtBMoraleLevel + * Parses a string representation of a morale level and returns the corresponding + * {@link AtBMoraleLevel} enum value. + * + * @param moraleLevel the string representation of a morale level + * @return the {@link AtBMoraleLevel} enum value corresponding to the given morale level string, + * or {@code STALEMATE} if the string cannot be parsed */ - public static AtBMoraleLevel parseFromString(final String text) { + // region File I/O + public static AtBMoraleLevel parseFromString(final String moraleLevel) { try { - return valueOf(text); - } catch (Exception ignored) { - - } + return valueOf(moraleLevel); + } catch (Exception ignored) {} try { - switch (Integer.parseInt(text)) { + switch (Integer.parseInt(moraleLevel)) { case 0: - return BROKEN; + return ROUTED; case 1: - return VERY_LOW; + return CRITICAL; case 2: - return LOW; + return WEAKENED; case 3: - return NORMAL; + return STALEMATE; case 4: - return HIGH; + return ADVANCING; case 5: - return VERY_HIGH; + return DOMINATING; case 6: - return UNBREAKABLE; + return OVERWHELMING; default: break; } - } catch (Exception ignored) { + } catch (Exception ignored) {} + //start <50.01 compatibility handler, replace it after post-40.10.1 Milestone + switch (moraleLevel) { + case "BROKEN" -> { + return ROUTED; + } + case "VERY_LOW" -> { + return CRITICAL; + } + case "LOW" -> { + return WEAKENED; + } + case "NORMAL" -> { + return STALEMATE; + } + case "HIGH" -> { + return ADVANCING; + } + case "VERY_HIGH" -> { + return DOMINATING; + } + case "UNBREAKABLE" -> { + return OVERWHELMING; + } + default -> {} } + //end <50.01 compatibility handler MMLogger.create(AtBMoraleLevel.class) - .error("Unable to parse " + text + " into an AtBMoraleLevel. Returning NORMAL."); - return NORMAL; + .error("Unable to parse " + moraleLevel + " into an AtBMoraleLevel. Returning NORMAL."); + return STALEMATE; } // endregion File I/O diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java index ff7c98e7ea..dbb37eb785 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -106,7 +106,14 @@ public static void generateScenariosForTrack(Campaign campaign, AtBContract cont // generated // and use the random force to drive opfor generation (#required lances // multiplies the BV budget of all - for (int scenarioIndex = 0; scenarioIndex < track.getRequiredLanceCount(); scenarioIndex++) { + int scenarioRolls = track.getRequiredLanceCount(); + + if (contract.getMoraleLevel().isDominating()) { + scenarioRolls++; + } else if (contract.getMoraleLevel().isOverwhelming()) { + scenarioRolls += 2; + } + for (int scenarioIndex = 0; scenarioIndex < scenarioRolls; scenarioIndex++) { int targetNum = calculateScenarioOdds(track, contract, false); // if we haven't already used all the player forces and are required to randomly @@ -1380,35 +1387,43 @@ public static boolean canManuallyDeployAnyForce(StratconCoords coords, public static int calculateScenarioOdds(StratconTrackState track, AtBContract contract, boolean playerDeployingForce) { // rules: - // broken morale: 0% - // very low morale: -10% when deploying forces to track, 0% attack - // low morale: -5% - // high morale: +5% - // very high morale: +10% - // unbreakable: special case, let's do +15% for now + // ROUTED: 0% + // CRITICAL: -10% when deploying forces to track, 0% attack + // WEAKENED: -5% + // ADVANCING: +5% + // DOMINATING: +10% + // OVERWHELMING: +100% int moraleModifier = 0; switch (contract.getMoraleLevel()) { - case BROKEN: + case ROUTED: return 0; - case VERY_LOW: + case CRITICAL: if (playerDeployingForce) { moraleModifier = -10; } else { return 0; } break; - case LOW: + case WEAKENED: moraleModifier = -5; break; - case HIGH: + case ADVANCING: moraleModifier = 5; break; - case VERY_HIGH: - moraleModifier = 10; + case DOMINATING: + if (playerDeployingForce) { + moraleModifier = 20; + } else { + return 10; + } break; - case UNBREAKABLE: - moraleModifier = 15; + case OVERWHELMING: + if (playerDeployingForce) { + moraleModifier = 50; + } else { + return 25; + } break; default: break; diff --git a/MekHQ/src/mekhq/gui/dialog/CustomizeAtBContractDialog.java b/MekHQ/src/mekhq/gui/dialog/CustomizeAtBContractDialog.java index bfbc9f157f..a2c49d2baa 100644 --- a/MekHQ/src/mekhq/gui/dialog/CustomizeAtBContractDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/CustomizeAtBContractDialog.java @@ -20,19 +20,6 @@ */ package mekhq.gui.dialog; -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.ResourceBundle; -import java.util.Set; - -import javax.swing.*; - import megamek.client.ui.baseComponents.MMComboBox; import megamek.client.ui.dialogs.CamoChooserDialog; import megamek.client.ui.preferences.JWindowPreference; @@ -55,6 +42,15 @@ import mekhq.gui.utilities.JSuggestField; import mekhq.gui.utilities.MarkdownEditorPanel; +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ResourceBundle; +import java.util.Set; + +import static megamek.client.ui.WrapLayout.wordWrap; + /** * @author Neoancient */ @@ -215,7 +211,7 @@ public Component getListCellRendererComponent(final JList list, final Object final boolean cellHasFocus) { super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if (value instanceof AtBMoraleLevel) { - list.setToolTipText(((AtBMoraleLevel) value).getToolTipText()); + list.setToolTipText(wordWrap(((AtBMoraleLevel) value).getToolTipText())); } return this; } diff --git a/MekHQ/src/mekhq/gui/view/MissionViewPanel.java b/MekHQ/src/mekhq/gui/view/MissionViewPanel.java index cccc07377a..147beba69a 100644 --- a/MekHQ/src/mekhq/gui/view/MissionViewPanel.java +++ b/MekHQ/src/mekhq/gui/view/MissionViewPanel.java @@ -34,6 +34,8 @@ import java.awt.event.MouseEvent; import java.util.ResourceBundle; +import static megamek.client.ui.WrapLayout.wordWrap; + /** * A custom panel that gets filled in with goodies from a scenario object * @author Jay Lawson (jaylawson39 at yahoo.com) @@ -922,7 +924,7 @@ public void mouseClicked(MouseEvent e) { lblMorale.setName("lblMorale"); lblMorale.setText(resourceMap.getString("lblMorale.text")); - lblMorale.setToolTipText(contract.getMoraleLevel().getToolTipText()); + lblMorale.setToolTipText(wordWrap(contract.getMoraleLevel().getToolTipText())); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = y; @@ -932,7 +934,7 @@ public void mouseClicked(MouseEvent e) { txtMorale.setName("txtMorale"); txtMorale.setText(contract.getMoraleLevel().toString()); - txtMorale.setToolTipText(contract.getMoraleLevel().getToolTipText()); + txtMorale.setToolTipText(wordWrap(contract.getMoraleLevel().getToolTipText())); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = y++;