diff --git a/MekHQ/data/forcegenerator/2823.xml b/MekHQ/data/forcegenerator/2823.xml index de49d09932..4a6707e447 100644 --- a/MekHQ/data/forcegenerator/2823.xml +++ b/MekHQ/data/forcegenerator/2823.xml @@ -2401,7 +2401,7 @@ - CC:4IS:5,Periphery.Deep:5,FS:4,Periphery:5,TC:7,CS:4-,FWL:4,NIOPS:4-,DC:7 + CC:4,IS:5,Periphery.Deep:5,FS:4,Periphery:5,TC:7,CS:4-,FWL:4,NIOPS:4-,DC:7 General:8,Periphery.Deep:8,Periphery:8 diff --git a/MekHQ/data/forcegenerator/3075.xml b/MekHQ/data/forcegenerator/3075.xml index 95ac05bf60..11c364dd90 100644 --- a/MekHQ/data/forcegenerator/3075.xml +++ b/MekHQ/data/forcegenerator/3075.xml @@ -9557,7 +9557,7 @@ - MOC:8CC:7,HL:7,FRR:7,IS:8,Periphery.Deep:7,WOB:4,FS:7,BAN:2,Periphery:8,CS:4,LA:7,FWL:7,NIOPS:4,DC:7 + MOC:8,CC:7,HL:7,FRR:7,IS:8,Periphery.Deep:7,WOB:4,FS:7,BAN:2,Periphery:8,CS:4,LA:7,FWL:7,NIOPS:4,DC:7 FWL:3,WOB:6 @@ -10639,7 +10639,7 @@ - CC:2FRR:4,CLAN:2,IS:2,Periphery.Deep:5,WOB:2-,MERC:2,FS:1,CIR:3,Periphery:2,CS:2-,LA:4,NIOPS:2- + CC:2,FRR:4,CLAN:2,IS:2,Periphery.Deep:5,WOB:2-,MERC:2,FS:1,CIR:3,Periphery:2,CS:2-,LA:4,NIOPS:2- apc HL:2-,IS:4-,Periphery.Deep:8,Periphery:4- diff --git a/MekHQ/data/forcegenerator/3085.xml b/MekHQ/data/forcegenerator/3085.xml index 8c238b8e42..f2ad4abe0c 100644 --- a/MekHQ/data/forcegenerator/3085.xml +++ b/MekHQ/data/forcegenerator/3085.xml @@ -4768,7 +4768,7 @@ - CC:3,MOC:3,CHH:2-,HL:3,CEI:2,FR:2,MERC:4,CDP:4,RA:2Periphery:4,ROS:6,NIOPS:6,CJF:2- + CC:3,MOC:3,CHH:2-,HL:3,CEI:2,FR:2,MERC:4,CDP:4,RA:2,Periphery:4,ROS:6,NIOPS:6,CJF:2- recon,escort CC:4,CEI:4,ROS:5 @@ -11513,7 +11513,7 @@ - MOC:2CC:5,RA.OA:2,LA:5,ROS:5,FS:5,MERC:5,Periphery:2,DC:4 + MOC:2,CC:5,RA.OA:2,LA:5,ROS:5,FS:5,MERC:5,Periphery:2,DC:4 General:8 diff --git a/MekHQ/data/scenariotemplates/Low-Atmosphere Reinforcements Intercepted.xml b/MekHQ/data/scenariotemplates/Low-Atmosphere Reinforcements Intercepted.xml new file mode 100644 index 0000000000..a5a78268e3 --- /dev/null +++ b/MekHQ/data/scenariotemplates/Low-Atmosphere Reinforcements Intercepted.xml @@ -0,0 +1,143 @@ + + + Low-Atmosphere Reinforcements Intercepted + Destroy hostile air forces. + Hostile air units have intercepted your reinforcements. Destroy at least 50% of the enemy force while preserving 50% of your own units. + + + false + 50 + 50 + 5 + LowAtmosphere + true + 5 + + + + Player + + -1 + false + -3 + 0 + true + true + true + true + false + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + 5 + 0 + 0 + 1.0 + Player + 0 + 1 + 4 + 0 + 50 + 0 + None + false + + + + OpFor + + -1 + false + -3 + 0 + true + false + true + false + false + + 5 + 0 + 2 + 1.0 + OpFor + 1 + 5 + 4 + 0 + 50 + 5 + OppositeEdge + Player + false + + + + + + + OpFor + + + + + ScenarioVictory + Fixed + 1 + + + + + ScenarioDefeat + Fixed + 1 + + + + Destroy or rout 50% of the following force(s) and unit(s): + NONE + ForceWithdraw + 50 + true + None + + + + Player + + + + + ScenarioVictory + Fixed + 1 + + + + + ScenarioDefeat + Fixed + 1 + + + + Preserve 50% of the following force(s) and unit(s): + NONE + Preserve + 50 + true + None + + + diff --git a/MekHQ/data/scenariotemplates/Reinforcements Intercepted.xml b/MekHQ/data/scenariotemplates/Reinforcements Intercepted.xml new file mode 100644 index 0000000000..5d4ddfbbd4 --- /dev/null +++ b/MekHQ/data/scenariotemplates/Reinforcements Intercepted.xml @@ -0,0 +1,143 @@ + + + Reinforcements Intercepted + Destroy hostile forces. + Hostile units have intercepted your reinforcements. Destroy at least 50% of the enemy force while preserving 50% of your own units. + + + false + 0 + 0 + 5 + AllGroundTerrain + true + 5 + + + + Player + + -1 + false + -2 + 0 + true + true + true + true + false + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + 5 + 0 + 0 + 1.0 + Player + 0 + 1 + 4 + 0 + 50 + 0 + None + false + + + + OpFor + + -1 + false + -2 + 0 + true + false + true + false + false + + 5 + 0 + 2 + 1.0 + OpFor + 1 + 5 + 4 + 0 + 50 + 0 + OppositeEdge + Player + false + + + + + + + OpFor + + + + + ScenarioVictory + Fixed + 1 + + + + + ScenarioDefeat + Fixed + 1 + + + + Destroy or rout 50% of the following force(s) and unit(s): + NONE + ForceWithdraw + 50 + true + None + + + + Player + + + + + ScenarioVictory + Fixed + 1 + + + + + ScenarioDefeat + Fixed + 1 + + + + Preserve 50% of the following force(s) and unit(s): + NONE + Preserve + 50 + true + None + + + diff --git a/MekHQ/data/scenariotemplates/Space Reinforcements Intercepted.xml b/MekHQ/data/scenariotemplates/Space Reinforcements Intercepted.xml new file mode 100644 index 0000000000..0d26af9a88 --- /dev/null +++ b/MekHQ/data/scenariotemplates/Space Reinforcements Intercepted.xml @@ -0,0 +1,147 @@ + + + Space Reinforcements Intercepted + Destroy hostile air forces. + Hostile air units have intercepted your reinforcements. Destroy at least 50% of the enemy force while preserving 50% of your own units. + false + false + + + false + 50 + 50 + 5 + Space + true + 5 + + + + Player + + -1 + false + 9 + 0 + true + true + true + true + false + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + 5 + 0 + 0 + 1.0 + Player + 0 + 1 + 4 + 0 + + 50 + 0 + None + false + + + + OpFor + + -1 + false + -3 + 0 + true + false + true + false + false + + 5 + 0 + 2 + 1.0 + OpFor + 1 + 5 + 4 + 0 + + 50 + 5 + OppositeEdge + Player + false + + + + + + + OpFor + + + + + ScenarioVictory + Fixed + 1 + + + + + ScenarioDefeat + Fixed + 1 + + + + Destroy or rout 50% of the following force(s) and unit(s): + NONE + ForceWithdraw + 50 + true + None + + + + Player + + + + + ScenarioVictory + Fixed + 1 + + + + + ScenarioDefeat + Fixed + 1 + + + + Preserve 50% of the following force(s) and unit(s): + NONE + Preserve + 50 + true + None + + + diff --git a/MekHQ/resources/mekhq/resources/AtBStratCon.properties b/MekHQ/resources/mekhq/resources/AtBStratCon.properties index 015741d7b9..511cc6ab4f 100644 --- a/MekHQ/resources/mekhq/resources/AtBStratCon.properties +++ b/MekHQ/resources/mekhq/resources/AtBStratCon.properties @@ -1,17 +1,55 @@ -supportPoint.text=Support Point -supportPointConvert.text=Will convert VP from SP -reinforcementRoll.Text=Reinforcement Roll +regular.text=Regular Reinforcement Roll +lanceInFightRole.text=Improved Reinforcement Roll fromChainedScenario.text=Lance already deployed -lanceInFightRole.text=Fight-Role Reinforcement Roll lblDefensiveMinefieldCount.text=Defensive Minefield Count: %d lblSelectIndividualUnits.text=Select individual units (%d max) lblDefensivePostureInstructions.Text=This lance is on a defensive deployment and you may deploy additional infantry, battle armor, or minefields. -lblLeadershipInstructions.Text=The force commander's leadership allows the deployment of additional auxiliary units - choose from the list below. -lblFCLeadershipAvailable.Text=Force commander's leadership: %d%s%s -lblLeaderUnitsUsed.Text=
%d units already assigned -lblLeadershipReinforcementsUnavailable.Text=
Leadership reinforcements unavailable +lblLeadershipInstructions.Text=The force commander's leadership allows the deployment of\ + \ additional auxiliary units - choose from the list below.\ +
\ +
Available BV: %sSelect a force from the list below.\ +
\ +
If multiple forces are selected, only the first will be deployed. +selectReinforcementsForTemplate.Text=Select reinforcements from the list below.\ +
\ +
Each attempt will cost 1 Support Point and may be unsuccessful. +selectReinforcementsForTemplateNoSupportPoints.Text=Unable to assign reinforcements. No Support Points available. + +reinforcementsNoSupportPoints.text=Attempting to reinforce scenario %s, %sAutomatic Failure%s You\ + \ have no remaining Support Points. +reinforcementsNoAdmin.text=Attempting to reinforce scenario %s, %sERROR%s nobody has been\ + \ assigned to an Admin/Command position. Roll automatically fails. No Support Points were\ + \ spent in this attempt. +reinforcementsNoAdminSkill.text=Attempting to reinforce scenario %s, %sERROR%s %s does not have the\ + \ Administration skill. Roll automatically fails. No Support Points were spent in this\ + \ attempt. +reinforcementsAttempt.text=Attempting to reinforce scenario %s, roll %s%s vs. %s: +reinforcementsCriticalFailure.text=%sCritical Command Failure%s. Reinforcement attempt\ + \ fails and Support Point is lost. +reinforcementsSuccess.text=%sReinforcement Success%s. +reinforcementsSuccessRouted.text=%sReinforcement Success%s. The enemy has routed\ + \ and is unable to intercept your reinforcements. +reinforcementsCommandFailure.text=%sCommand Failure%s. Reinforcement attempt\ + \ fails and Support Point is lost. +reinforcementsInterceptionAttempt.text=Due to a %sCommand Failure%s your reinforcements are\ + \ out of position. Enemy forces were dispatched in an attempt to capitalize on this tactical error. +reinforcementsErrorNoCommander.text=%sError%s. There is no commander assigned to this force.\ + \ Reinforcement attempt fails and Support Point is lost. You should report this bug. +reinforcementsErrorUnableToFetchCommander.text= %sError%s. We were unable to fetch the\ + \ commander using the commander ID logged for this force. You should report this bug.\ + \ Reinforcement attempt fails and Support Point is lost. +reinforcementCommanderNoSkill.text=%sAutomatic Evasion Failure%s. The commander of the\ + \ reinforcements does not possess the Tactics skill. They were unable to evade enemy\ + \ forces. An interception scenario has occurred. The reinforcing force has already been assigned\ + \ to this new scenario. +reinforcementEvasionSuccessful.text=%sEvasion Success%s (%s vs. %s). The commander of the\ + \ reinforcements was able to use their Tactics skill to evade the enemy long enough to\ + \ reach safety. Reinforcements were delayed, but successful. +reinforcementEvasionUnsuccessful.text=%sEvasion Unsuccessful%s (%s vs. %s). The commander\ + \ of the reinforcements attempted to use their Tactics skill to evade the enemy\ + \ interception, but was unsuccessful. An interception scenario has occurred. The reinforcing\ + \ force has already been assigned to this new scenario. \ No newline at end of file diff --git a/MekHQ/resources/mekhq/resources/ContractMarketDialog.properties b/MekHQ/resources/mekhq/resources/ContractMarketDialog.properties index 5a4d46bfec..d4fa8f98bd 100644 --- a/MekHQ/resources/mekhq/resources/ContractMarketDialog.properties +++ b/MekHQ/resources/mekhq/resources/ContractMarketDialog.properties @@ -34,7 +34,7 @@ lblTransport.text=Transport Terms: lblSalvageRights.text=Salvage Rights: lblStraightSupport.text=Straight Support: lblBattleLossComp.text=Battle Loss Compensation: -lblRequiredLances.text=Required Lances: +lblRequiredLances.text=Required Combat Forces: lblRenegotiate.text=Renegotiate diff --git a/MekHQ/resources/mekhq/resources/Mission.properties b/MekHQ/resources/mekhq/resources/Mission.properties index 6b82d8b25b..b1572c3fc6 100644 --- a/MekHQ/resources/mekhq/resources/Mission.properties +++ b/MekHQ/resources/mekhq/resources/Mission.properties @@ -64,7 +64,7 @@ ContractCommandRights.INTEGRATED.toolTipText=-
- No map scouting (StratCon).\
\
Integrated command rights, standard for government forces, streamline command and control, particularly in large-scale operations involving multiple forces, ensuring effective collaboration without inter-service rivalry or confusion over command authority. -ContractCommandRights.INTEGRATED.stratConText=The employer will make Lance assignments. Complete required scenarios to fulfill contract conditions. +ContractCommandRights.INTEGRATED.stratConText=The employer will make Lance assignments.
Complete required scenarios to fulfill contract conditions. ContractCommandRights.HOUSE.text=House ContractCommandRights.HOUSE.toolTipText=- Keep your Campaign Victory Points (CVP) positive. Winning a non-initiated scenario: +1 CVP. Losing a non-initiated scenario: -1 CVP.\ diff --git a/MekHQ/resources/mekhq/resources/NewContractDialog.properties b/MekHQ/resources/mekhq/resources/NewContractDialog.properties index e53aec9e9f..f44e6ca6fe 100644 --- a/MekHQ/resources/mekhq/resources/NewContractDialog.properties +++ b/MekHQ/resources/mekhq/resources/NewContractDialog.properties @@ -44,7 +44,7 @@ lblAllyBotName.text=Ally Bot Name: lblEnemyBotName.text=Enemy Bot Name: lblAllyCamo.text=Ally Camo lblEnemyCamo.text=Enemy Camo -lblRequiredLances.text=Required Lances: +lblRequiredLances.text=Required Combat Forces: lblEnemyMorale.text=Enemy Morale: lblContractScoreArbitraryModifier.text=Contract Score Modifier: lblBasePay.text=Base Pay: diff --git a/MekHQ/resources/mekhq/resources/ScenarioTableModel.properties b/MekHQ/resources/mekhq/resources/ScenarioTableModel.properties index 1af4002967..66033b477b 100644 --- a/MekHQ/resources/mekhq/resources/ScenarioTableModel.properties +++ b/MekHQ/resources/mekhq/resources/ScenarioTableModel.properties @@ -2,4 +2,5 @@ col_name.text=Scenario Name col_status.text=Resolution col_date.text=Date col_assign.text=Units Assigned +col_sector.text=Sector col_unknown.text=? diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index e4368a7479..c0beb16477 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -144,12 +144,13 @@ import static mekhq.campaign.force.StrategicFormation.recalculateStrategicFormations; import static mekhq.campaign.market.contractMarket.ContractAutomation.performAutomatedActivation; +import static mekhq.campaign.personnel.SkillType.S_ADMIN; import static mekhq.campaign.personnel.backgrounds.BackgroundsController.randomMercenaryCompanyNameGenerator; import static mekhq.campaign.personnel.education.EducationController.getAcademy; import static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.Payout.isBreakingContract; import static mekhq.campaign.unit.Unit.SITE_FACILITY_BASIC; import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; - +import static mekhq.campaign.mission.AtBContract.pickRandomCamouflage; /** * The main campaign class, keeps track of teams and units * @@ -224,7 +225,7 @@ public class Campaign implements ITechManager { private boolean gmMode; private transient boolean overviewLoadingValue = true; - private Camouflage camouflage = new Camouflage(Camouflage.COLOUR_CAMOUFLAGE, PlayerColour.BLUE.name()); + private Camouflage camouflage = pickRandomCamouflage(3025, "Root"); private PlayerColour colour = PlayerColour.BLUE; private StandardForceIcon unitIcon = new UnitIcon(null, null); @@ -2721,6 +2722,33 @@ public TargetRoll getTargetFor(Person medWork, Person doctor) { return admin; } + /** + * This method finds and returns the most senior command administrator. + * It checks for both primary and secondary roles of the administrator. + * In case of multiple administrators with the command role, it uses the + * {@code outRanksUsingSkillTiebreaker} method to decide the seniority. + * + * @return the senior administrator with a command role, or {@code null} if no such + * administrator exists. + */ + public @Nullable Person getSeniorAdminCommandPerson() { + Person seniorAdmin = null; + + for (Person person : getAdmins()) { + if (person.getPrimaryRole().isAdministratorCommand() || person.getSecondaryRole().isAdministratorCommand()) { + if (seniorAdmin == null) { + seniorAdmin = person; + continue; + } + + if (person.outRanksUsingSkillTiebreaker(this, seniorAdmin)) { + seniorAdmin = person; + } + } + } + return seniorAdmin; + } + /** * Gets a list of applicable logistics personnel, or an empty list * if acquisitions automatically succeed. @@ -3904,7 +3932,7 @@ private void processNewDayATB() { * Admin/Transport personnel skill levels and contract start dates are considered during negotiations. * Side effects include state changes and report generation. */ - private void negotiateAdditionalSupportPoints() { + public void negotiateAdditionalSupportPoints() { // Fetch a list of all Admin/Transport personnel List adminTransport = new ArrayList<>(); @@ -3917,10 +3945,10 @@ private void negotiateAdditionalSupportPoints() { // Sort that list based on skill adminTransport.sort((person1, person2) -> { - Skill person1Skill = person1.getSkill(SkillType.S_ADMIN); + Skill person1Skill = person1.getSkill(S_ADMIN); int person1SkillValue = person1Skill.getLevel() + person1Skill.getBonus(); - Skill person2Skill = person2.getSkill(SkillType.S_ADMIN); + Skill person2Skill = person2.getSkill(S_ADMIN); int person2SkillValue = person2Skill.getLevel() + person2Skill.getBonus(); return Double.compare(person1SkillValue, person2SkillValue); @@ -3937,7 +3965,7 @@ private void negotiateAdditionalSupportPoints() { // out of Admin/Transport personnel, or we run out of active contracts for (AtBContract contract : sortedContracts) { int negoatiatedSupportPoints = 0; - int tracks = contract.getStratconCampaignState().getTracks().size(); + int maximumSupportPointsNegotiated = contract.getRequiredLances(); if (adminTransport.isEmpty()) { break; @@ -3949,14 +3977,19 @@ private void negotiateAdditionalSupportPoints() { Person assignedAdmin = adminTransport.get(0); adminTransport.remove(0); - int targetNumber = assignedAdmin.getSkill(SkillType.S_ADMIN).getFinalSkillValue(); + int targetNumber = assignedAdmin.getSkill(S_ADMIN).getFinalSkillValue(); int roll = Compute.d6(2); if (roll >= targetNumber) { negoatiatedSupportPoints++; + + int marginOfSuccess = (roll - targetNumber) / 4; + + negoatiatedSupportPoints += marginOfSuccess; } - if (negoatiatedSupportPoints >= tracks) { + if (negoatiatedSupportPoints >= maximumSupportPointsNegotiated) { + negoatiatedSupportPoints = maximumSupportPointsNegotiated; break; } } @@ -4537,7 +4570,7 @@ private void processReputationChanges() { public int getInitiativeBonus() { return initiativeBonus; } - + public void setInitiativeBonus(int bonus) { initiativeBonus = bonus; } @@ -4545,7 +4578,7 @@ public void setInitiativeBonus(int bonus) { public void applyInitiativeBonus(int bonus) { if (bonus > initiativeMaxBonus) { initiativeMaxBonus = bonus; - } + } if ((bonus + initiativeBonus) > initiativeMaxBonus) { initiativeBonus = initiativeMaxBonus; } else { diff --git a/MekHQ/src/mekhq/campaign/force/StrategicFormation.java b/MekHQ/src/mekhq/campaign/force/StrategicFormation.java index c5305bf5b5..40d94b3f65 100644 --- a/MekHQ/src/mekhq/campaign/force/StrategicFormation.java +++ b/MekHQ/src/mekhq/campaign/force/StrategicFormation.java @@ -52,6 +52,7 @@ import static megamek.common.EntityWeightClass.WEIGHT_ULTRA_LIGHT; import static mekhq.campaign.force.Force.STRATEGIC_FORMATION_OVERRIDE_NONE; import static mekhq.campaign.force.Force.STRATEGIC_FORMATION_OVERRIDE_TRUE; +import static mekhq.campaign.force.FormationLevel.LANCE; /** * Used by Against the Bot & StratCon to track additional information about each force @@ -64,9 +65,9 @@ public class StrategicFormation { private static final MMLogger logger = MMLogger.create(StrategicFormation.class); - public static final int STR_IS = 4; - public static final int STR_CLAN = 5; - public static final int STR_CS = 6; + public static final int LANCE_SIZE = 4; + public static final int STAR_SIZE = 5; + public static final int LEVEL_II_SIZE = 6; public static final long ETYPE_GROUND = ETYPE_MEK | ETYPE_TANK | Entity.ETYPE_INFANTRY | ETYPE_PROTOMEK; @@ -81,20 +82,51 @@ public class StrategicFormation { /** * Determines the standard size for a given faction. The size varies depending on whether the - * faction is a Clan, ComStar/WoB, or others (Inner Sphere). + * faction is a Clan, ComStar/WoB, or others (Inner Sphere). This overloaded method defaults to + * Lance/Star/Level II * * @param faction The {@link Faction} object for which the standard force size is to be calculated. - * @return The standard force size for the given faction. It returns {@code STR_CLAN} if the - * faction is a Clan, {@code STR_CS} if the faction is ComStar or WoB, and {@code STR_IS} otherwise. + * @return The standard force size, at the provided formation level, for the provided faction */ public static int getStandardForceSize(Faction faction) { - if (faction.isClan()) { - return STR_CLAN; + return getStandardForceSize(faction, LANCE.getDepth()); + } + + /** + * Determines the standard size for a given faction. The size varies depending on whether the + * faction is a Clan, ComStar/WoB, or others (Inner Sphere). + * + * @param faction The {@link Faction} object for which the standard force size is to be calculated. + * @param formationLevelDepth The {@link FormationLevel} {@code Depth} from which the standard + * force size is to be calculated. + * @return The standard force size, at the provided formation level, for the provided faction + */ + public static int getStandardForceSize(Faction faction, int formationLevelDepth) { + int formationSize; + if (faction.isClan() || faction.isMarianHegemony()) { + formationSize = STAR_SIZE; } else if (faction.isComStarOrWoB()) { - return STR_CS; + formationSize = LEVEL_II_SIZE; } else { - return STR_IS; + formationSize = LANCE_SIZE; } + + if (formationLevelDepth == LANCE.getDepth()) { + return formationSize; + } + + formationLevelDepth++; // Lance is depth 0, so we need to add +1 to get the number of iterations + + for (int i = 0; i < formationLevelDepth; i++) { + + if (faction.isComStarOrWoB()) { + formationSize *= 6; + } else { + formationSize *= 3; + } + } + + return formationSize; } /** diff --git a/MekHQ/src/mekhq/campaign/icons/enums/LayeredForceIconOperationalStatus.java b/MekHQ/src/mekhq/campaign/icons/enums/LayeredForceIconOperationalStatus.java index 5da4524ad0..23a955b4f1 100644 --- a/MekHQ/src/mekhq/campaign/icons/enums/LayeredForceIconOperationalStatus.java +++ b/MekHQ/src/mekhq/campaign/icons/enums/LayeredForceIconOperationalStatus.java @@ -88,17 +88,11 @@ public static LayeredForceIconOperationalStatus determineLayeredForceIconOperati return NOT_OPERATIONAL; } - switch (unit.getDamageState()) { - case Entity.DMG_NONE: - return FULLY_OPERATIONAL; - case Entity.DMG_LIGHT: - case Entity.DMG_MODERATE: - return SUBSTANTIALLY_OPERATIONAL; - case Entity.DMG_HEAVY: - case Entity.DMG_CRIPPLED: - return MARGINALLY_OPERATIONAL; - default: - return NOT_OPERATIONAL; - } + return switch (unit.getDamageState()) { + case Entity.DMG_NONE -> FULLY_OPERATIONAL; + case Entity.DMG_LIGHT, Entity.DMG_MODERATE -> SUBSTANTIALLY_OPERATIONAL; + case Entity.DMG_HEAVY, Entity.DMG_CRIPPLED -> MARGINALLY_OPERATIONAL; + default -> NOT_OPERATIONAL; + }; } } diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java index 80f929e24b..b106407860 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java @@ -2,7 +2,6 @@ import megamek.Version; import megamek.codeUtilities.MathUtility; -import megamek.common.Compute; import megamek.common.enums.SkillLevel; import megamek.logging.MMLogger; import mekhq.campaign.Campaign; @@ -22,6 +21,13 @@ import java.io.PrintWriter; import java.util.*; +import static java.lang.Math.floor; +import static java.lang.Math.max; +import static java.lang.Math.round; +import static megamek.common.Compute.d6; +import static mekhq.campaign.force.StrategicFormation.getStandardForceSize; +import static mekhq.campaign.mission.AtBContract.getEffectiveNumUnits; + /** * Abstract base class for various Contract Market types in AtB/Stratcon. Responsible for generation * and initialization of AtBContracts. @@ -174,18 +180,43 @@ protected void updateReport(Campaign campaign) { /** * Determines the number of required lances to be deployed for a contract. For Mercenary subcontracts - * this defaults to 1; otherwise the number is based on the number of combat units in the campaign. - * @param campaign - * @param contract + * this defaults to 1; otherwise, the number is based on the number of combat units in the + * campaign. Modified by a 2d6 roll if {@code bypassVariance} is {@code false}. + * @param campaign the current campaign + * @param contract the relevant contract + * @param bypassVariance if {@code true} requirements will not be semi-randomized. * @return The number of lances required to be deployed. */ - public int calculateRequiredLances(Campaign campaign, AtBContract contract) { - int maxDeployedLances = calculateMaxDeployedLances(campaign); + public int calculateRequiredLances(Campaign campaign, AtBContract contract, boolean bypassVariance) { + int maxDeployedLances = max(calculateMaxDeployedLances(campaign), 1); if (contract.isSubcontract()) { return 1; } else { - int requiredLances = Math.max(AtBContract.getEffectiveNumUnits(campaign) / 6, 1); - return Math.min(requiredLances, maxDeployedLances); + int formationSize = getStandardForceSize(campaign.getFaction()); + int availableForces = max(getEffectiveNumUnits(campaign) / formationSize, 1); + + // We allow for one reserve force per 3 depth 0 forces (lances, etc) + availableForces -= max((int) floor((double) availableForces / 3), 1); + + if (!bypassVariance) { + int roll = d6(2); + + if (roll == 2) { + availableForces = (int) round((double) availableForces * 0.25); + } else if (roll == 3) { + availableForces = (int) round((double) availableForces * 0.5); + } else if (roll < 5) { + availableForces = (int) round((double) availableForces * 0.75); + } else if (roll == 12) { + availableForces = (int) round((double) availableForces * 1.75); + } else if (roll == 11) { + availableForces = (int) round((double) availableForces * 1.5); + } else if (roll > 9) { + availableForces = (int) round((double) availableForces * 1.25); + } + } + + return MathUtility.clamp(availableForces, 1, maxDeployedLances); } } @@ -228,7 +259,7 @@ protected int getQualityRating(int roll) { } protected void rollCommandClause(final Contract contract, final int modifier) { - final int roll = Compute.d6(2) + modifier; + final int roll = d6(2) + modifier; if (roll < 3) { contract.setCommandRights(ContractCommandRights.INTEGRATED); } else if (roll < 8) { @@ -242,14 +273,14 @@ protected void rollCommandClause(final Contract contract, final int modifier) { protected void rollSalvageClause(AtBContract contract, int mod, int contractMaxSalvagePercentage) { contract.setSalvageExchange(false); - int roll = Math.min(Compute.d6(2) + mod, 13); + int roll = Math.min(d6(2) + mod, 13); if (roll < 2) { contract.setSalvagePct(0); } else if (roll < 4) { contract.setSalvageExchange(true); int r; do { - r = Compute.d6(2); + r = d6(2); } while (r < 4); contract.setSalvagePct(Math.min((r - 3) * 10, contractMaxSalvagePercentage)); } else { @@ -258,7 +289,7 @@ protected void rollSalvageClause(AtBContract contract, int mod, int contractMaxS } protected void rollSupportClause(AtBContract contract, int mod) { - int roll = Compute.d6(2) + mod; + int roll = d6(2) + mod; contract.setStraightSupport(0); contract.setBattleLossComp(0); if (roll < 3) { @@ -273,7 +304,7 @@ protected void rollSupportClause(AtBContract contract, int mod) { } protected void rollTransportClause(AtBContract contract, int mod) { - int roll = Compute.d6(2) + mod; + int roll = d6(2) + mod; if (roll < 2) { contract.setTransportComp(0); } else if (roll < 6) { @@ -299,7 +330,7 @@ protected AtBContractType findMissionType(int unitRatingMod, boolean majorPower) AtBContractType.SECURITY_DUTY, AtBContractType.OBJECTIVE_RAID, AtBContractType.GARRISON_DUTY, AtBContractType.CADRE_DUTY, AtBContractType.DIVERSIONARY_RAID } }; - int roll = MathUtility.clamp(Compute.d6(2) + unitRatingMod - IUnitRating.DRAGOON_C, 2, 12); + int roll = MathUtility.clamp(d6(2) + unitRatingMod - IUnitRating.DRAGOON_C, 2, 12); return table[majorPower ? 0 : 1][roll - 2]; } @@ -316,7 +347,7 @@ protected void setEnemyCode(AtBContract contract) { protected void setAttacker(AtBContract contract) { boolean isAttacker = !contract.getContractType().isGarrisonType() - || (contract.getContractType().isReliefDuty() && (Compute.d6() < 4)) + || (contract.getContractType().isReliefDuty() && (d6() < 4)) || contract.getEnemy().isRebel(); contract.setAttacker(isAttacker); } @@ -372,12 +403,12 @@ protected void setAllyRating(AtBContract contract, int year) { // facing front-line units mod += 1; } - contract.setAllySkill(getSkillRating(Compute.d6(2) + mod)); + contract.setAllySkill(getSkillRating(d6(2) + mod)); if (year > 2950 && year < 3039 && !Factions.getInstance().getFaction(contract.getEmployerCode()).isClan()) { mod -= 1; } - contract.setAllyQuality(getQualityRating(Compute.d6(2) + mod)); + contract.setAllyQuality(getQualityRating(d6(2) + mod)); } protected void setEnemyRating(AtBContract contract, int year) { @@ -397,12 +428,12 @@ protected void setEnemyRating(AtBContract contract, int year) { if (Factions.getInstance().getFaction(contract.getEmployerCode()).isClan()) { mod += contract.isAttacker() ? 2 : 4; } - contract.setEnemySkill(getSkillRating(Compute.d6(2) + mod)); + contract.setEnemySkill(getSkillRating(d6(2) + mod)); if (year > 2950 && year < 3039 && !Factions.getInstance().getFaction(contract.getEnemyCode()).isClan()) { mod -= 1; } - contract.setEnemyQuality(getQualityRating(Compute.d6(2) + mod)); + contract.setEnemyQuality(getQualityRating(d6(2) + mod)); } public void writeToXML(final PrintWriter pw, int indent) { @@ -475,8 +506,7 @@ public static AbstractContractMarket generateInstanceFromXML(Node wn, Campaign c // Restore any parent contract references for (Contract contract : retVal.contracts) { - if (contract instanceof AtBContract) { - final AtBContract atbContract = (AtBContract) contract; + if (contract instanceof AtBContract atbContract) { atbContract.restore(c); } } diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java index b07f628147..9dcac0d8bf 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java @@ -20,10 +20,6 @@ */ package mekhq.campaign.market.contractMarket; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Set; - import megamek.common.Compute; import megamek.common.annotations.Nullable; import megamek.common.enums.SkillLevel; @@ -39,11 +35,11 @@ import mekhq.campaign.personnel.SkillType; import mekhq.campaign.personnel.enums.PersonnelRole; import mekhq.campaign.rating.IUnitRating; -import mekhq.campaign.universe.Faction; -import mekhq.campaign.universe.Factions; -import mekhq.campaign.universe.PlanetarySystem; -import mekhq.campaign.universe.RandomFactionGenerator; -import mekhq.campaign.universe.Systems; +import mekhq.campaign.universe.*; + +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Set; /** * Contract offers that are generated monthly under AtB rules. @@ -302,7 +298,7 @@ private void checkForSubcontracts(Campaign campaign, AtBContract contract, int u contract.calculateLength(campaign.getCampaignOptions().isVariableContractLength()); setContractClauses(contract, unitRatingMod, campaign); - contract.setRequiredLances(calculateRequiredLances(campaign, contract)); + contract.setRequiredLances(calculateRequiredLances(campaign, contract, false)); contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel()); @@ -396,7 +392,7 @@ protected AtBContract generateAtBSubcontract(Campaign campaign, } contract.setTransportComp(100); - contract.setRequiredLances(calculateRequiredLances(campaign, contract)); + contract.setRequiredLances(calculateRequiredLances(campaign, contract, false)); contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel()); contract.calculateContract(campaign); @@ -438,7 +434,7 @@ private void addFollowup(Campaign campaign, followup.calculateLength(campaign.getCampaignOptions().isVariableContractLength()); setContractClauses(followup, campaign.getAtBUnitRatingMod(), campaign); - contract.setRequiredLances(calculateRequiredLances(campaign, contract)); + contract.setRequiredLances(calculateRequiredLances(campaign, contract, false)); contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); followup.setPartsAvailabilityLevel(followup.getContractType().calculatePartsAvailabilityLevel()); @@ -488,7 +484,11 @@ public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract multiplier *= 1.1; } - int requiredLances = calculateRequiredLances(campaign, contract); + int baseRequiredLances = calculateRequiredLances(campaign, contract, true); + int requiredLances = contract.getRequiredLances(); + + multiplier *= (double) requiredLances / baseRequiredLances; + int maxDeployedLances = calculateMaxDeployedLances(campaign); if (requiredLances > maxDeployedLances && campaign.getCampaignOptions().isAdjustPaymentForStrategy()) { multiplier *= (double) maxDeployedLances / (double) requiredLances; diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java index 9758f72992..062369afa8 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java @@ -43,8 +43,8 @@ */ public class CamOpsContractMarket extends AbstractContractMarket { private static final MMLogger logger = MMLogger.create(CamOpsContractMarket.class); - private static int BASE_NEGOTIATION_TARGET = 8; - private static int EMPLOYER_NEGOTIATION_SKILL_LEVEL = 5; + private static final int BASE_NEGOTIATION_TARGET = 8; + private static final int EMPLOYER_NEGOTIATION_SKILL_LEVEL = 5; public CamOpsContractMarket() { super(ContractMarketMethod.CAM_OPS); @@ -204,7 +204,7 @@ private Optional generateContract(Campaign campaign, ReputationCont // Step 6: Determine the initial contract clauses setContractClauses(contract, contractTerms); // Step 7: Determine the number of required lances (Not CamOps RAW) - contract.setRequiredLances(calculateRequiredLances(campaign, contract)); + contract.setRequiredLances(calculateRequiredLances(campaign, contract, false)); // Step 8: Calculate the payment contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); // Step 9: Determine parts availability diff --git a/MekHQ/src/mekhq/campaign/mission/AtBContract.java b/MekHQ/src/mekhq/campaign/mission/AtBContract.java index 070c4b7f1d..6bf29b5b2b 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBContract.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBContract.java @@ -75,14 +75,19 @@ import java.util.*; import static java.lang.Math.ceil; +import static java.lang.Math.max; import static java.lang.Math.round; import static megamek.client.ratgenerator.ModelRecord.NETWORK_NONE; import static megamek.client.ratgenerator.UnitTable.findTable; +import static megamek.common.Compute.d6; import static megamek.common.UnitType.MEK; import static megamek.common.enums.SkillLevel.ELITE; import static megamek.common.enums.SkillLevel.REGULAR; import static megamek.common.enums.SkillLevel.parseFromInteger; import static megamek.common.enums.SkillLevel.parseFromString; +import static mekhq.campaign.force.FormationLevel.BATTALION; +import static mekhq.campaign.force.FormationLevel.COMPANY; +import static mekhq.campaign.force.StrategicFormation.getStandardForceSize; import static mekhq.campaign.mission.AtBDynamicScenarioFactory.getEntity; import static mekhq.campaign.mission.BotForceRandomizer.UNIT_WEIGHT_UNSPECIFIED; import static mekhq.campaign.rating.IUnitRating.*; @@ -229,9 +234,12 @@ public AtBContract(String name) { } public void initContractDetails(Campaign campaign) { - if (getEffectiveNumUnits(campaign) <= 12) { + int companySize = getStandardForceSize(campaign.getFaction(), COMPANY.getDepth()); + int battalionSize = getStandardForceSize(campaign.getFaction(), BATTALION.getDepth()); + + if (getEffectiveNumUnits(campaign) <= companySize) { setOverheadComp(OH_FULL); - } else if (getEffectiveNumUnits(campaign) <= 48) { + } else if (getEffectiveNumUnits(campaign) <= battalionSize) { setOverheadComp(OH_HALF); } else { setOverheadComp(OH_NONE); @@ -270,6 +278,7 @@ public static Camouflage pickRandomCamouflage(int currentYear, String factionCod allPaths = Files.find(Paths.get(ROOT_DIRECTORY + camouflageDirectory + '/'), Integer.MAX_VALUE, (path, bfa) -> bfa.isRegularFile()) .toList(); + } catch (IOException e) { logger.error("Error getting list of camouflages", e); } @@ -326,6 +335,7 @@ private static String getCamouflageDirectory(int currentYear, String factionCode case "SL" -> "Star League Defense Force"; case "TC" -> "Taurian Concordat"; case "WOB" -> "Word of Blake"; + case "Root" -> ""; default -> { Faction faction = Factions.getInstance().getFaction(factionCode); @@ -394,7 +404,8 @@ public void calculateLength(final boolean variable) { * @return The number of lances required. */ public static int calculateRequiredLances(Campaign campaign) { - return Math.max(getEffectiveNumUnits(campaign) / 6, 1); + int formationSize = getStandardForceSize(campaign.getFaction()); + return max(getEffectiveNumUnits(campaign) / formationSize, 1); } public static int getEffectiveNumUnits(Campaign campaign) { @@ -495,7 +506,7 @@ public void checkMorale(Campaign campaign, LocalDate today) { Faction enemy = getEnemy(); if (enemy.isClan()) { - reliability = Math.max(5, reliability + 1); + reliability = max(5, reliability + 1); } reliability = switch (reliability) { @@ -590,7 +601,7 @@ public void checkMorale(Campaign campaign, LocalDate today) { "+" + performanceModifier : performanceModifier)); // Total morale modifier calculation - int roll = Compute.d6(2) + targetNumber.getValue(); + int roll = d6(2) + targetNumber.getValue(); logger.info(String.format("Total Modifier: %s", targetNumber.getValue())); logger.info(String.format("Roll: %s", roll)); @@ -598,10 +609,10 @@ public void checkMorale(Campaign campaign, LocalDate today) { final AtBMoraleLevel[] moraleLevels = AtBMoraleLevel.values(); if (roll < 2) { - setMoraleLevel(moraleLevels[Math.max(getMoraleLevel().ordinal() - 2, 0)]); + setMoraleLevel(moraleLevels[max(getMoraleLevel().ordinal() - 2, 0)]); logger.info("Result: Morale Level -2"); } else if (roll < 5) { - setMoraleLevel(moraleLevels[Math.max(getMoraleLevel().ordinal() - 1, 0)]); + setMoraleLevel(moraleLevels[max(getMoraleLevel().ordinal() - 1, 0)]); logger.info("Result: Morale Level -1"); } else if ((roll > 12)) { setMoraleLevel(moraleLevels[Math.min(getMoraleLevel().ordinal() + 2, moraleLevels.length - 1)]); @@ -616,7 +627,7 @@ public void checkMorale(Campaign campaign, LocalDate today) { // 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); + routEnd = today.plusMonths(max(1, d6() - 3)).minusDays(1); } else { campaign.addReport("With the enemy routed, any remaining objectives have been successfully completed." + " The contract will conclude tomorrow."); @@ -751,12 +762,12 @@ public int getContractScoreArbitraryModifier() { public void doBonusRoll(Campaign campaign) { int number; - int roll = Compute.d6(); + int roll = d6(); switch (roll) { case 1: /* 1d6 dependents */ if (campaign.getCampaignOptions().isUseRandomDependentAddition()) { - number = Compute.d6(); + number = d6(); campaign.addReport("Bonus: " + number + " dependent" + ((number > 1) ? "s" : "")); for (int i = 0; i < number; i++) { @@ -893,7 +904,7 @@ public void checkEvents(Campaign c) { break; case EVT_BETRAYAL: String text = "Special Event: Betrayal (employer minor breach)
"; - switch (Compute.d6()) { + switch (d6()) { case 1: text += "Major logistics problem: parts availability level for the rest of the contract becomes one level lower."; partsAvailabilityLevel--; @@ -937,7 +948,7 @@ public void checkEvents(Campaign c) { break; case EVT_SPECIALEVENTS: text = "Special Event: "; - switch (Compute.d6()) { + switch (d6()) { case 1: text += "Change of Alliance: Next Enemy Morale roll gets a +1 modifier."; moraleMod++; @@ -1023,9 +1034,9 @@ public boolean contractExtended(final Campaign campaign) { } final int extension; - final int roll = Compute.d6(); + final int roll = d6(); if (roll == 1) { - extension = Math.max(1, getLength() / 2); + extension = max(1, getLength() / 2); } else if (roll == 2) { extension = 1; } else { @@ -1917,7 +1928,7 @@ public int calculateContractDifficulty(Campaign campaign) { mappedValue = 5 + mappedValue; } - return Math.min(Math.max(mappedValue, 1), 10); + return Math.min(max(mappedValue, 1), 10); } /** diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java index 1f0f78a656..32be95056e 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java @@ -22,13 +22,17 @@ import megamek.common.Entity; import megamek.common.annotations.Nullable; import megamek.common.enums.SkillLevel; +import megamek.logging.MMLogger; import mekhq.campaign.Campaign; +import mekhq.campaign.force.Force; import mekhq.campaign.force.StrategicFormation; +import mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment; import mekhq.campaign.mission.ScenarioForceTemplate.ForceGenerationMethod; import mekhq.campaign.mission.atb.AtBScenarioModifier; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.SkillType; import mekhq.campaign.rating.IUnitRating; +import mekhq.campaign.unit.Unit; import mekhq.utilities.MHQXMLUtility; import org.apache.commons.lang3.StringUtils; import org.w3c.dom.Element; @@ -39,6 +43,11 @@ import java.text.ParseException; import java.util.*; +import static mekhq.campaign.mission.AtBDynamicScenarioFactory.getPlanetOwnerAlignment; +import static mekhq.campaign.mission.AtBDynamicScenarioFactory.getPlanetOwnerFaction; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Allied; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.PlanetOwner; + /** * Data structure intended to hold data relevant to AtB Dynamic Scenarios (AtB 3.0) * @author NickAragua @@ -67,6 +76,7 @@ public static class BenchedEntityData { private double effectivePlayerBVMultiplier; // Additive multiplier private int friendlyReinforcementDelayReduction; + private List friendlyDelayedReinforcements; private int hostileReinforcementDelayReduction; // derived fields used for various calculations @@ -85,6 +95,9 @@ public static class BenchedEntityData { private transient Map playerUnitTemplates; private transient List scenarioModifiers; + private static final MMLogger logger = MMLogger.create(AtBDynamicScenario.class); + + public AtBDynamicScenario() { super(); @@ -92,6 +105,7 @@ public AtBDynamicScenario() { setEffectivePlayerUnitCountMultiplier(0.0); setEffectivePlayerBVMultiplier(0.0); setFriendlyReinforcementDelayReduction(0); + setFriendlyDelayedReinforcements(new ArrayList<>()); setHostileReinforcementDelayReduction(0); setEffectiveOpforSkill(SkillLevel.REGULAR); setEffectiveOpforQuality(IUnitRating.DRAGOON_C); @@ -312,6 +326,14 @@ public void setEffectiveOpforQuality(int qualityLevel) { effectiveOpforQuality = qualityLevel; } + public List getFriendlyDelayedReinforcements() { + return friendlyDelayedReinforcements; + } + + public void setFriendlyDelayedReinforcements(final List friendlyDelayedReinforcements) { + this.friendlyDelayedReinforcements = friendlyDelayedReinforcements; + } + public int getFriendlyReinforcementDelayReduction() { return friendlyReinforcementDelayReduction; } @@ -482,6 +504,7 @@ protected void writeToXMLEnd(final PrintWriter pw, int indent) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, "effectivePlayerUnitCountMultiplier", getEffectivePlayerUnitCountMultiplier()); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "effectivePlayerBVMultiplier", getEffectivePlayerBVMultiplier()); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "friendlyReinforcementDelayReduction", getFriendlyReinforcementDelayReduction()); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "friendlyDelayedReinforcements", getFriendlyDelayedReinforcements()); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "hostileReinforcementDelayReduction", getHostileReinforcementDelayReduction()); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "effectiveOpforSkill", getEffectiveOpforSkill().name()); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "effectiveOpforQuality", getEffectiveOpforQuality()); @@ -525,6 +548,11 @@ protected void loadFieldsFromXmlNode(final Node wn, final Version version, final setEffectivePlayerBVMultiplier(Double.parseDouble(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("friendlyReinforcementDelayReduction")) { setFriendlyReinforcementDelayReduction(Integer.parseInt(wn2.getTextContent().trim())); + } else if (wn2.getNodeName().equalsIgnoreCase("friendlyDelayedReinforcements")) { + String[] values = wn2.getTextContent().split(","); + for (String value : values) { + getFriendlyDelayedReinforcements().add(UUID.fromString(value)); + } } else if (wn2.getNodeName().equalsIgnoreCase("hostileReinforcementDelayReduction")) { setHostileReinforcementDelayReduction(Integer.parseInt(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("effectiveOpforSkill")) { @@ -578,4 +606,64 @@ public void clearAllForcesAndPersonnel(Campaign campaign) { public String getBattlefieldControlDescription() { return ""; } + + /** + * Returns the total battle value (BV) either for allied forces or opposing forces in + * a given contract campaign, as per the parameter {@code isAllied}. + *

+ * If {@code isAllied} is {@code true}, the method calculates the total BV for the allied + * forces inclusive of player forces. If {@code isAllied} is {@code false}, the total BV for + * opposing forces is calculated. + *

+ * The calculation is done based on Bot forces attributed to each side. In the case of + * PlanetOwner, the alignment of the owner faction is considered to determine the ownership of + * Bot forces. + * + * @param campaign The campaign in which the forces are participating. + * @param isAllied A boolean value indicating whether to calculate the total BV for + * allied forces (if true) or opposing forces (if false). + * @return The total battle value (BV) either for the allied forces or + * opposing forces, as specified by the parameter isAllied. + */ + public int getTeamTotalBattleValue(Campaign campaign, boolean isAllied) { + AtBContract contract = getContract(campaign); + int totalBattleValue = 0; + + for (BotForce botForce : getBotForces()) { + int battleValue = botForce.getTotalBV(campaign); + + int team = botForce.getTeam(); + + if (team == PlanetOwner.ordinal()) { + String planetOwnerFaction = getPlanetOwnerFaction(contract, campaign.getLocalDate()); + ForceAlignment forceAlignment = getPlanetOwnerAlignment(contract, planetOwnerFaction, campaign.getLocalDate()); + team = forceAlignment.ordinal(); + } + + if (team <= Allied.ordinal()) { + if (isAllied) { + totalBattleValue += battleValue; + } + } else if (!isAllied) { + totalBattleValue += battleValue; + } + } + + if (isAllied) { + Force playerForces = this.getForces(campaign); + + for (UUID unitID : playerForces.getAllUnits(false)) { + try { + Unit unit = campaign.getUnit(unitID); + Entity entity = unit.getEntity(); + + totalBattleValue += entity.calculateBattleValue(); + } catch (Exception ex) { + logger.warn(ex.getMessage(), ex); + } + } + } + + return totalBattleValue; + } } diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java index 6971a79c47..20f56264b9 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java @@ -71,6 +71,7 @@ import static megamek.common.Compute.randomInt; import static megamek.common.UnitType.*; import static megamek.common.planetaryconditions.Wind.TORNADO_F4; +import static mekhq.campaign.force.StrategicFormation.getStandardForceSize; import static mekhq.campaign.mission.Scenario.T_GROUND; import static mekhq.campaign.mission.ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX; import static mekhq.campaign.mission.ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_CIVILIANS; @@ -94,15 +95,11 @@ public class AtBDynamicScenarioFactory { // indexed by dragoons rating private static final int[] infantryToBAUpgradeTNs = { 12, 10, 8, 6, 4, 2 }; - private static final int IS_LANCE_SIZE = 4; - private static final int CLAN_MH_LANCE_SIZE = 5; - private static final int COMSTAR_LANCE_SIZE = 6; - private static final double STRICT = 0.75; private static final double OPPORTUNISTIC = 1.0; private static final double LIBERAL = 1.25; - private static final int REINFORCEMENT_ARRIVAL_SCALE = 30; + private static final int REINFORCEMENT_ARRIVAL_SCALE = 15; private static final ResourceBundle resources = ResourceBundle.getBundle( "mekhq.resources.AtBDynamicScenarioFactory", @@ -283,12 +280,13 @@ private static int generateForces(AtBDynamicScenario scenario, AtBContract contr effectiveUnitCount = calculateEffectiveUnitCount(scenario, campaign, false); for (ScenarioForceTemplate forceTemplate : currentForceTemplates) { + logger.info(String.format("++ Generating a force for the %s template ++", + forceTemplate.getForceName()).toUpperCase()); + if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.FixedMUL.ordinal()) { generatedLanceCount += generateFixedForce(scenario, contract, campaign, forceTemplate); } else { int weightClass = randomForceWeight(); - logger.info(String.format("++ Generating a force for the %s template ++", forceTemplate.getForceName()).toUpperCase()); - generatedLanceCount += generateForce(scenario, contract, campaign, effectiveBV, effectiveUnitCount, weightClass, forceTemplate, false); } @@ -398,6 +396,10 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac effectiveBV = (int) round(effectiveBV * difficultyMultiplier); effectiveUnitCount = (int) round(effectiveUnitCount * difficultyMultiplier); + if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal()) { + logger.info(String.format("Effective xBV Budget: %s (adjusted for difficulty)", + effectiveBV)); + } // Intentional fall-through: opposing third parties are either the contracted // enemy or "Unidentified Hostiles" which are considered pirates or bandit caste // with random quality and skill @@ -439,18 +441,21 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac // Get the number of units in the typical ground tactical formation. // This will differ depending on whether the owner uses Inner Sphere lances, // Clan stars, or CS/WOB Level II formations. - int lanceSize = getLanceSize(factionCode); + int lanceSize = getStandardForceSize(faction); // determine generation parameters int forceBV = 0; double forceMultiplier = forceTemplate.getForceMultiplier(); - if (forceMultiplier == 0) { - forceMultiplier = 1; - } + if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal() + || forceTemplate.getGenerationMethod() == ForceGenerationMethod.UnitCountScaled.ordinal()) { + // This means force multiplier wasn't initialized in the template + if (forceMultiplier == 0) { + forceMultiplier = 1; + logger.warn(String.format("Force multiplier is zero for %s", forceTemplate.getForceName())); + } - if (forceMultiplier != 1) { - logger.info(String.format("Force BV Multiplier: %s (from scenario template)", forceMultiplier)); + logger.info(String.format("Force Multiplier: %s (from scenario template)", forceMultiplier)); } int forceBVBudget = (int) (effectiveBV * forceMultiplier); @@ -459,8 +464,8 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac forceBVBudget = (int) (forceBVBudget * ((double) campaign.getCampaignOptions().getScenarioModBV() / 100)); } - if (forceTemplate.getForceMultiplier() != 1) { - logger.info(String.format("BV Budget was %s, now %s (includes Modifier settings and Multiplier)", + if (forceTemplate.getForceMultiplier() != 1 && forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal()) { + logger.info(String.format("BV Budget was %s, now %s", effectiveBV, forceBVBudget)); } @@ -468,6 +473,9 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.UnitCountScaled.ordinal()) { forceUnitBudget = (int) (effectiveUnitCount * forceTemplate.getForceMultiplier()); + + logger.info(String.format("Unit Budget was %s, now %s", + effectiveUnitCount, forceUnitBudget)); } else if ((forceTemplate.getGenerationMethod() == ForceGenerationMethod.FixedUnitCount.ordinal()) || (forceTemplate.getGenerationMethod() == ForceGenerationMethod.PlayerOrFixedUnitCount.ordinal())) { forceUnitBudget = forceTemplate.getFixedUnitCount() == ScenarioForceTemplate.FIXED_UNIT_SIZE_LANCE ? lanceSize : forceTemplate.getFixedUnitCount(); @@ -2957,7 +2965,7 @@ public static int calculateEffectiveBV(AtBDynamicScenario scenario, Campaign cam } } - logger.info(String.format("Total Base %s Budget: %s (adjusted for campaign difficulty)", + logger.info(String.format("Total Base %s Budget: %s (may be adjusted by Effective Player BV Multiplier)", generationMethod, bvBudget)); return bvBudget; @@ -3502,8 +3510,29 @@ public static void setPlayerDeploymentTurns(AtBDynamicScenario scenario, Campaig if (deployRound == ScenarioForceTemplate.ARRIVAL_TURN_STAGGERED_BY_LANCE) { setDeploymentTurnsStaggeredByLance(forceEntities); } else if (deployRound == ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS) { - setDeploymentTurnsForReinforcements(forceEntities, - strategy + scenario.getFriendlyReinforcementDelayReduction()); + setDeploymentTurnsForReinforcements(forceEntities, strategy + scenario.getFriendlyReinforcementDelayReduction()); + + // Here we selectively overwrite the earlier entries + if (!scenario.getFriendlyDelayedReinforcements().isEmpty()) { + List delayedEntities = new ArrayList<>(); + + for (UUID unitId : scenario.getFriendlyDelayedReinforcements()) { + try { + Unit unit = campaign.getUnit(unitId); + Entity entity = unit.getEntity(); + + delayedEntities.add(entity); + } catch (Exception ex) { + logger.error(ex.getMessage(), ex); + } + } + + if (!delayedEntities.isEmpty()) { + setDeploymentTurnsForReinforcements(delayedEntities, + strategy + scenario.getFriendlyReinforcementDelayReduction(), + true); + } + } } else { for (Entity entity : forceEntities) { entity.setDeployRound(deployRound); @@ -3628,10 +3657,41 @@ private static void setDeploymentTurnsStaggered(List entityList, int tur * @param turnModifier A number to subtract from the deployment turn. */ public static void setDeploymentTurnsForReinforcements(List entityList, int turnModifier) { + setDeploymentTurnsForReinforcements(entityList, turnModifier, false); + } + + /** + * Given a list of entities, set the arrival turns for them as if they were all + * reinforcements on the same side. This overloaded method allows for defining whether the + * force was delayed. + * + * @param entityList List of entities to process + * @param turnModifier A number to subtract from the deployment turn. + * @param isDelayed Whether the arrival of the entities was delayed + */ + public static void setDeploymentTurnsForReinforcements(List entityList, int turnModifier, + boolean isDelayed) { int minimumSpeed = 999; + int arrivalScale = REINFORCEMENT_ARRIVAL_SCALE; + + // First, we organize the reinforcements into pools. + // This ensures each Force's reinforcements are handled separately. + Map delayByForce = new HashMap<>(); // first, we figure out the slowest "atb speed" of this group. for (Entity entity : entityList) { + if (isDelayed) { + int forceId = entity.getForceId(); + + if (delayByForce.containsKey(forceId)) { + arrivalScale = delayByForce.get(forceId); + } else { + int delayedArrivalScale = REINFORCEMENT_ARRIVAL_SCALE * (randomInt(2) + 2); + delayByForce.put(forceId, delayedArrivalScale); + arrivalScale = delayedArrivalScale; + } + } + // don't include transported units in this calculation if (entity.getTransportId() != Entity.NONE) { continue; @@ -3644,17 +3704,15 @@ public static void setDeploymentTurnsForReinforcements(List entityList, if ((speed < minimumSpeed) && (speed > 0)) { minimumSpeed = speed; } - } - // the actual arrival turn will be the scale divided by the slowest speed. - // so, a group of Atlases (3/5) should arrive on turn 10 (30 / 3) - // a group of jump-capable Griffins (5/8/5) should arrive on turn 5 (30 / 6) - // a group of Ostscouts (8/12/8) should arrive on turn 3 (30 / 9, rounded down) - // we then subtract the passed-in turn modifier, which is usually the - // commander's strategy skill level. - int actualArrivalTurn = Math.max(0, (REINFORCEMENT_ARRIVAL_SCALE / minimumSpeed) - turnModifier); + // the actual arrival turn will be the scale divided by the slowest speed. + // so, a group of Atlases (3/5) should arrive at turn 7 (20 / 3) + // a group of jump-capable Griffins (5/8/5) should arrive on turn 3 (20 / 6, rounded down) + // a group of Ostscouts (8/12/8) should arrive on turn 2 (20 / 9, rounded down) + // we then subtract the passed-in turn modifier, which is usually the + // commander's strategy skill level. + int actualArrivalTurn = Math.max(0, (arrivalScale / minimumSpeed) - turnModifier); - for (Entity entity : entityList) { entity.setDeployRound(actualArrivalTurn); } } @@ -3758,32 +3816,6 @@ private static void setScenarioRerolls(AtBDynamicScenario scenario, Campaign cam scenario.setRerolls(tacticsSkill); } - /** - * Convenience function to get the standard ground tactical formation size, - * based on faction. In - * the case of Clan factions, this returns the number of points rather than a - * number of units, - * as points may be 2 ground vehicles or 5 ProtoMeks. - * TODO: conventional infantry typically uses 3 units per formation (company) - - * make a separate method - * - * @param factionCode string with faction short name/lookup key - * @return Number of units (points for Clan) in the formation - */ - public static int getLanceSize(String factionCode) { - Faction faction = Factions.getInstance().getFaction(factionCode); - if (faction != null) { - if (faction.isClan() || faction.isMarianHegemony()) { - // Clans and the Marian Hegemony use a fundamental unit size of 5. - return CLAN_MH_LANCE_SIZE; - } else if (faction.isComStarOrWoB()) { - // ComStar and WoB use a fundamental unit size of 6. - return COMSTAR_LANCE_SIZE; - } - } - - return IS_LANCE_SIZE; - } /** * Worker function to determine the formation size of fixed wing aircraft. @@ -3877,7 +3909,7 @@ private static void correctNonAeroFlyerBehavior(List entityList, int boa * @param currentDate Current date. * @return Faction code. */ - private static String getPlanetOwnerFaction(AtBContract contract, LocalDate currentDate) { + static String getPlanetOwnerFaction(AtBContract contract, LocalDate currentDate) { String factionCode = "MERC"; // planet owner is the first of the factions that owns the current planet. @@ -3903,8 +3935,7 @@ private static String getPlanetOwnerFaction(AtBContract contract, LocalDate curr * @param currentDate Current date. * @return ForceAlignment. */ - private static ForceAlignment getPlanetOwnerAlignment(AtBContract contract, String factionCode, - LocalDate currentDate) { + static ForceAlignment getPlanetOwnerAlignment(AtBContract contract, String factionCode, LocalDate currentDate) { // if the faction is one of the planet owners, see if it's either the employer // or opfor. If it's not, third-party. if (contract.getSystem().getFactions(currentDate).contains(factionCode)) { diff --git a/MekHQ/src/mekhq/campaign/mission/CommonObjectiveFactory.java b/MekHQ/src/mekhq/campaign/mission/CommonObjectiveFactory.java index bdb8938f83..dea23e1026 100644 --- a/MekHQ/src/mekhq/campaign/mission/CommonObjectiveFactory.java +++ b/MekHQ/src/mekhq/campaign/mission/CommonObjectiveFactory.java @@ -31,6 +31,8 @@ import java.util.ResourceBundle; import java.util.UUID; +import static mekhq.campaign.force.StrategicFormation.getStandardForceSize; + /** * This class contains code for the creation of some common objectives for an AtB scenario * @author NickAragua @@ -219,7 +221,7 @@ public static ScenarioObjective getBreakthrough(AtBContract contract, AtBScenari * Worker function - adds designated lance or currently assigned player units to objective */ private static void addAssignedPlayerUnitsToObjective(AtBScenario scenario, Campaign campaign, ScenarioObjective objective) { - int expectedNumUnits = AtBDynamicScenarioFactory.getLanceSize(campaign.getFactionCode()); + int expectedNumUnits = getStandardForceSize(campaign.getFaction()); if (scenario.isBigBattle()) { expectedNumUnits *= 2; } else if (scenario.isSpecialScenario()) { diff --git a/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java b/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java index a558b6d06c..9d4c5feabe 100644 --- a/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java +++ b/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java @@ -35,10 +35,12 @@ import mekhq.campaign.personnel.Skills; import mekhq.campaign.rating.IUnitRating; import mekhq.campaign.unit.Unit; +import mekhq.campaign.universe.Faction; import mekhq.campaign.universe.Factions; import java.util.UUID; +import static mekhq.campaign.force.StrategicFormation.getStandardForceSize; import static mekhq.campaign.mission.AtBDynamicScenarioFactory.generateForce; import static mekhq.campaign.mission.AtBDynamicScenarioFactory.randomForceWeight; @@ -79,6 +81,9 @@ private static void postAddForce(Campaign campaign, AtBDynamicScenario scenario, int weightClass = randomForceWeight(); + logger.info(String.format("++ Generating a force for the %s template ++", + templateToApply.getForceName()).toUpperCase()); + generateForce(scenario, scenario.getContract(campaign), campaign, effectiveBV, effectiveUnitCount, weightClass, templateToApply, true); @@ -125,7 +130,8 @@ public static void removeUnits(AtBDynamicScenario scenario, Campaign campaign, F factionCode = scenario.getContract(campaign).getEnemyCode(); } - actualUnitsToRemove = AtBDynamicScenarioFactory.getLanceSize(factionCode); + Faction faction = Factions.getInstance().getFaction(factionCode); + actualUnitsToRemove = getStandardForceSize(faction); } for (int x = 0; x < actualUnitsToRemove; x++) { diff --git a/MekHQ/src/mekhq/campaign/parts/SVArmor.java b/MekHQ/src/mekhq/campaign/parts/SVArmor.java index 1c79b0a6e2..0563097fd7 100644 --- a/MekHQ/src/mekhq/campaign/parts/SVArmor.java +++ b/MekHQ/src/mekhq/campaign/parts/SVArmor.java @@ -18,11 +18,6 @@ */ package mekhq.campaign.parts; -import java.io.PrintWriter; -import java.util.Objects; - -import org.w3c.dom.Node; - import megamek.common.Entity; import megamek.common.EquipmentType; import megamek.common.ITechnology; @@ -33,6 +28,12 @@ import mekhq.campaign.finances.Money; import mekhq.campaign.work.IAcquisitionWork; import mekhq.utilities.MHQXMLUtility; +import org.w3c.dom.Node; + +import java.io.PrintWriter; +import java.util.Objects; + +import static megamek.common.EquipmentType.T_ARMOR_SV_BAR_2; /** * Standard support vehicle armor, which can differ by BAR and tech rating. @@ -107,9 +108,16 @@ public Money getValueNeeded() { @Override public Money getStickerPrice() { + // The value of '< T_ARMOR_SV_BAR_2' means that the armor does not exist at that tech level + // (or it is not SV BAR armor). + if (bar < T_ARMOR_SV_BAR_2) { + return Money.zero(); + } + // always in 5-ton increments - return Money.of(5.0 / ArmorType.svArmor(bar).getSVWeightPerPoint(techRating) - * ArmorType.svArmor(bar).getCost()); + double weightPerPoint = ArmorType.svArmor(bar).getSVWeightPerPoint(techRating); + double calculatedAmount = 5.0 / weightPerPoint * ArmorType.svArmor(bar).getCost(); + return Money.of(calculatedAmount); } @Override diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java b/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java index e9086269ca..17eaa6c5b9 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java @@ -18,14 +18,6 @@ */ package mekhq.campaign.stratcon; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -import javax.xml.namespace.QName; - -import org.w3c.dom.Node; - import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBElement; import jakarta.xml.bind.Marshaller; @@ -34,8 +26,17 @@ import jakarta.xml.bind.annotation.XmlElementWrapper; import jakarta.xml.bind.annotation.XmlRootElement; import jakarta.xml.bind.annotation.XmlTransient; +import jakarta.xml.bind.annotation.adapters.XmlAdapter; +import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import megamek.logging.MMLogger; import mekhq.campaign.mission.AtBContract; +import org.w3c.dom.Node; + +import javax.xml.namespace.QName; +import java.io.PrintWriter; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; /** * Contract-level state object for a StratCon campaign. @@ -63,7 +64,9 @@ public class StratconCampaignState { @XmlElementWrapper(name = "campaignTracks") @XmlElement(name = "campaignTrack") - private List tracks; + private final List tracks; + + private List weeklyScenarios; @XmlTransient public AtBContract getContract() { @@ -76,10 +79,12 @@ public void setContract(AtBContract contract) { public StratconCampaignState() { tracks = new ArrayList<>(); + weeklyScenarios = new ArrayList<>(); } public StratconCampaignState(AtBContract contract) { tracks = new ArrayList<>(); + weeklyScenarios = new ArrayList<>(); setContract(contract); } @@ -104,6 +109,21 @@ public void addTrack(StratconTrackState track) { tracks.add(track); } + @XmlJavaTypeAdapter(value = LocalDateAdapter.class) + @XmlElementWrapper(name = "weeklyScenarios") + @XmlElement(name = "weeklyScenario") + public List getWeeklyScenarios() { + return weeklyScenarios; + } + + public void addWeeklyScenario(LocalDate weeklyScenario) { + weeklyScenarios.add(weeklyScenario); + } + + public void setWeeklyScenarios(final List weeklyScenarios) { + this.weeklyScenarios = weeklyScenarios; + } + public int getSupportPoints() { return supportPoints; } @@ -156,11 +176,6 @@ public void useSupportPoint() { supportPoints--; } - public void convertVictoryToSupportPoint() { - victoryPoints--; - supportPoints++; - } - /** * Convenience/speed method of determining whether or not a force with the given * ID has been deployed to a track in this campaign. @@ -239,4 +254,20 @@ public static StratconCampaignState Deserialize(Node xmlNode) { return resultingCampaignState; } + + /** + * This adapter provides a way to convert between a LocalDate and the ISO-8601 string + * representation of the date that is used for XML marshaling and unmarshalling in JAXB. + */ + public static class LocalDateAdapter extends XmlAdapter { + @Override + public String marshal(LocalDate date) { + return date.toString(); + } + + @Override + public LocalDate unmarshal(String date) throws Exception { + return LocalDate.parse(date); + } + } } diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java b/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java index 0c435b7222..1918ce15e0 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java @@ -221,6 +221,9 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai } } + // Determine starting Support Points + campaign.negotiateAdditionalSupportPoints(); + // now we're done } diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java index 24c08c1e41..a87ad37985 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -19,8 +19,9 @@ package mekhq.campaign.stratcon; import megamek.codeUtilities.ObjectUtility; -import megamek.common.Compute; import megamek.common.Minefield; +import megamek.common.TargetRoll; +import megamek.common.TargetRollModifier; import megamek.common.UnitType; import megamek.common.annotations.Nullable; import megamek.common.event.Subscribe; @@ -39,10 +40,10 @@ import mekhq.campaign.mission.ScenarioForceTemplate.ForceGenerationMethod; import mekhq.campaign.mission.ScenarioMapParameters.MapLocation; import mekhq.campaign.mission.atb.AtBScenarioModifier; -import mekhq.campaign.mission.atb.AtBScenarioModifier.EventTiming; import mekhq.campaign.mission.enums.AtBMoraleLevel; +import mekhq.campaign.mission.enums.ContractCommandRights; import mekhq.campaign.personnel.Person; -import mekhq.campaign.personnel.SkillType; +import mekhq.campaign.personnel.Skill; import mekhq.campaign.personnel.turnoverAndRetention.Fatigue; import mekhq.campaign.stratcon.StratconContractDefinition.StrategicObjectiveType; import mekhq.campaign.stratcon.StratconScenario.ScenarioState; @@ -53,13 +54,30 @@ import java.util.*; import java.util.stream.Collectors; +import static java.lang.Math.max; +import static java.lang.Math.min; import static java.lang.Math.round; +import static megamek.common.Compute.d6; +import static megamek.common.Compute.randomInt; import static mekhq.campaign.force.Force.FORCE_NONE; +import static mekhq.campaign.icons.enums.LayeredForceIconOperationalStatus.determineLayeredForceIconOperationalStatus; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Allied; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Opposing; import static mekhq.campaign.mission.ScenarioMapParameters.MapLocation.AllGroundTerrain; import static mekhq.campaign.mission.ScenarioMapParameters.MapLocation.LowAtmosphere; import static mekhq.campaign.mission.ScenarioMapParameters.MapLocation.Space; import static mekhq.campaign.mission.ScenarioMapParameters.MapLocation.SpecificGroundTerrain; +import static mekhq.campaign.personnel.SkillType.S_ADMIN; +import static mekhq.campaign.personnel.SkillType.S_TACTICS; import static mekhq.campaign.stratcon.StratconContractInitializer.getUnoccupiedCoords; +import static mekhq.campaign.stratcon.StratconRulesManager.ReinforcementEligibilityType.FIGHT_LANCE; +import static mekhq.campaign.stratcon.StratconRulesManager.ReinforcementResultsType.DELAYED; +import static mekhq.campaign.stratcon.StratconRulesManager.ReinforcementResultsType.FAILED; +import static mekhq.campaign.stratcon.StratconRulesManager.ReinforcementResultsType.INTERCEPTED; +import static mekhq.campaign.stratcon.StratconRulesManager.ReinforcementResultsType.SUCCESS; +import static mekhq.campaign.stratcon.StratconScenarioFactory.convertSpecificUnitTypeToGeneral; +import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; +import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor; /** * This class contains "rules" logic for the AtB-Stratcon state @@ -67,6 +85,7 @@ * @author NickAragua */ public class StratconRulesManager { + public final static int BASE_LEADERSHIP_BUDGET = 1000; private static final MMLogger logger = MMLogger.create(StratconRulesManager.class); /** @@ -76,56 +95,83 @@ public enum ReinforcementEligibilityType { /** * Nothing */ - None, + NONE, /** * Lance is already deployed to the track */ - ChainedScenario, + CHAINED_SCENARIO, /** - * We pay a support point or convert a Campaign Victory Point to a support point + * We pay a support point and make a regular roll */ - SupportPoint, + REGULAR, /** - * The lance's deployment orders are "Fight" + * The lance's deployment orders are "Fight". We pay a support point and make an enhanced roll */ - FightLance + FIGHT_LANCE } /** - * This function potentially generates non-player-initiated scenarios for the - * given track. + * What were the results of the reinforcement roll? */ - public static void generateScenariosForTrack(Campaign campaign, AtBContract contract, StratconTrackState track) { + public enum ReinforcementResultsType { + /** + * The reinforcement attempt was successful. + */ + SUCCESS, + + /** + * The reinforcements arrive later than normal. + */ + DELAYED, + + /** + * The attempt failed, nothing else happens. + */ + FAILED, + + /** + * The reinforcements were intercepted. + */ + INTERCEPTED + } + + /** + * This method generates scenario dates for each week of the StratCon campaign. + *

+ * The method first determines the number of required scenario rolls based on the required + * lance count from the track, then multiplies that count depending on the contract's morale level. + *

+ * If auto-assign for lances is enabled, and either there are no available forces or the number of + * weekly scenarios equals or exceeds the number of available forces, it breaks from the scenario + * generation loop. + *

+ * For each scenario, a scenario odds target number is calculated, and a roll is made against + * this target. If the roll is less than the target number, a new weekly scenario is created + * with a random date within the week. + * + * @param campaign The campaign. + * @param campaignState The state of the StratCon campaign. + * @param contract The AtBContract for the campaign. + * @param track The StratCon campaign track. + */ + public static void generateScenariosDatesForWeek(Campaign campaign, StratconCampaignState campaignState, + AtBContract contract, StratconTrackState track) { // maps scenarios to force IDs - List generatedScenarios = new ArrayList<>(); final boolean autoAssignLances = contract.getCommandRights().isIntegrated(); - - // get this list just so we have it available List availableForceIDs = getAvailableForceIDs(campaign); - Map> sortedAvailableForceIDs = sortForcesByMapType(availableForceIDs, campaign); - // make X rolls, where X is the number of required lances for the track - // that's the chance to spawn a scenario. - // if a scenario occurs, then we pick a random non-deployed lance and use it to - // drive the opfor generation later - // once we've determined that scenarios occur, we loop through the ones that we - // generated - // and use the random force to drive opfor generation (#required lances - // multiplies the BV budget of all int scenarioRolls = track.getRequiredLanceCount(); - if (!autoAssignLances) { - AtBMoraleLevel moraleLevel = contract.getMoraleLevel(); + AtBMoraleLevel moraleLevel = contract.getMoraleLevel(); - switch (moraleLevel) { - case STALEMATE -> scenarioRolls = (int) round(scenarioRolls * 1.25); - case ADVANCING -> scenarioRolls = (int) round(scenarioRolls * 1.5); - case DOMINATING -> scenarioRolls = scenarioRolls * 2; - case OVERWHELMING -> scenarioRolls = scenarioRolls * 3; - } + switch (moraleLevel) { + case STALEMATE -> scenarioRolls = (int) round(scenarioRolls * 1.25); + case ADVANCING -> scenarioRolls = (int) round(scenarioRolls * 1.5); + case DOMINATING -> scenarioRolls = scenarioRolls * 2; + case OVERWHELMING -> scenarioRolls = scenarioRolls * 3; } for (int scenarioIndex = 0; scenarioIndex < scenarioRolls; scenarioIndex++) { @@ -133,83 +179,156 @@ public static void generateScenariosForTrack(Campaign campaign, AtBContract cont break; } - int targetNum = calculateScenarioOdds(track, contract, false); - int roll = Compute.randomInt(100); + if (autoAssignLances && (campaignState.getWeeklyScenarios().size() >= availableForceIDs.size())) { + break; + } - logger.info(String.format("StratCon Weekly Scenario Roll: %s vs. %s", roll, targetNum)); + int targetNum = calculateScenarioOdds(track, contract, false); + int roll = randomInt(100); - // if we haven't already used all the player forces and are required to randomly - // generate a scenario if (roll < targetNum) { - // pick random coordinates and force to drive the scenario - StratconCoords scenarioCoords = getUnoccupiedCoords(track, true); + LocalDate scenarioDate = campaign.getLocalDate().plusDays(randomInt(7)); + campaignState.addWeeklyScenario(scenarioDate); + logger.info(String.format("StratCon Weekly Scenario Roll: %s vs. %s (%s)", roll, targetNum, scenarioDate)); + } else { + logger.info(String.format("StratCon Weekly Scenario Roll: %s vs. %s", roll, targetNum)); + } + } + } + + /** + * This method generates a weekly scenario for a specific track. + *

+ * First, it initializes empty collections for generated scenarios and available forces, and + * determines whether lances are auto-assigned. + *

+ * Then it generates a requested number of scenarios. If auto-assign is enabled and there + * are no available forces, it breaks from the scenario generation loop. + *

+ * For each scenario, it first tries to create a scenario for existing forces on the track. + * If that is not possible, it selects random force, removes it from available forces, and + * creates a scenario for it. For any scenario, if it is under liaison command, it may set the + * scenario as required and attaches the liaison. + *

+ * After scenarios are generated, OpFors, events, etc. are finalized for each scenario. + * + * @param campaign The current campaign. + * @param campaignState The relevant StratCon campaign state. + * @param contract The relevant contract. + * @param scenarioCount The number of scenarios to generate. + */ + public static void generateDailyScenariosForTrack(Campaign campaign, StratconCampaignState campaignState, + AtBContract contract, int scenarioCount) { + final boolean autoAssignLances = contract.getCommandRights().isIntegrated(); + + // get this list just so we have it available + List availableForceIDs = getAvailableForceIDs(campaign); + + // Build the available force pool - this ensures operational forces have an increased + // chance of being picked + if (autoAssignLances && !availableForceIDs.isEmpty()) { + List availableForcePool = new ArrayList<>(); - if (scenarioCoords == null) { - logger.warn("Target track is full, skipping scenario generation"); + for (int forceId : availableForceIDs) { + Force force = campaign.getForce(forceId); + + if (force == null) { continue; } - // if forces are already assigned to these coordinates, use those instead - // of randomly-selected ones - if (track.getAssignedCoordForces().containsKey(scenarioCoords)) { - StratconScenario scenario = generateScenarioForExistingForces(scenarioCoords, - track.getAssignedCoordForces().get(scenarioCoords), contract, campaign, track); + int operationalStatus = 0; + int unitCount = 0; - if (scenario != null) { - generatedScenarios.add(scenario); + for (UUID unitId : force.getAllUnits(true)) { + try { + Unit unit = campaign.getUnit(unitId); + operationalStatus += determineLayeredForceIconOperationalStatus(unit).ordinal(); + unitCount++; + } catch (Exception e) { + logger.warn(e.getMessage(), e); } - continue; } - // otherwise, pick a random force from the avail - int randomForceIndex = Compute.randomInt(availableForceIDs.size()); + int calculatedOperationStatus = (int) round(Math.pow((3 - (double) operationalStatus / unitCount), 2.0)); + + for (int i = 0; i < calculatedOperationStatus; i++) { + availableForcePool.add(forceId); + } + } + + Collections.shuffle(availableForcePool); + availableForceIDs = availableForcePool; + } + + + Map> sortedAvailableForceIDs = sortForcesByMapType(availableForceIDs, campaign); + + for (int scenarioIndex = 0; scenarioIndex < scenarioCount; scenarioIndex++) { + if (autoAssignLances && availableForceIDs.isEmpty()) { + break; + } + + List tracks = campaignState.getTracks(); + StratconTrackState track = campaignState.getTracks().get(0); + + if (tracks.size() > 1) { + track = ObjectUtility.getRandomItem(tracks); + } + + if (autoAssignLances && availableForceIDs.isEmpty()) { + break; + } + + StratconCoords scenarioCoords = getUnoccupiedCoords(track, true); + + if (scenarioCoords == null) { + logger.warn("Target track is full, skipping scenario generation"); + continue; + } + + // if forces are already assigned to these coordinates, use those instead of randomly + // selected ones + StratconScenario scenario; + if (track.getAssignedCoordForces().containsKey(scenarioCoords)) { + scenario = generateScenarioForExistingForces(scenarioCoords, + track.getAssignedCoordForces().get(scenarioCoords), contract, campaign, track); + // otherwise, pick a random force from the avail + } else { + int randomForceIndex = randomInt(availableForceIDs.size()); int randomForceID = availableForceIDs.get(randomForceIndex); // remove the force from the available lists, so we don't designate it as primary // twice if (autoAssignLances) { - availableForceIDs.remove(randomForceIndex); - } + availableForceIDs.removeIf(id -> id.equals(randomForceIndex)); - // we want to remove the actual int with the value, not the value at the index - sortedAvailableForceIDs.get(AllGroundTerrain).remove((Integer) randomForceID); - sortedAvailableForceIDs.get(LowAtmosphere).remove((Integer) randomForceID); - sortedAvailableForceIDs.get(Space).remove((Integer) randomForceID); + // we want to remove the actual int with the value, not the value at the index + sortedAvailableForceIDs.get(AllGroundTerrain).removeIf(id -> id.equals(randomForceID)); + sortedAvailableForceIDs.get(LowAtmosphere).removeIf(id -> id.equals(randomForceID)); + sortedAvailableForceIDs.get(Space).removeIf(id -> id.equals(randomForceID)); + } // two scenarios on the same coordinates wind up increasing in size if (track.getScenarios().containsKey(scenarioCoords)) { track.getScenarios().get(scenarioCoords).incrementRequiredPlayerLances(); assignAppropriateExtraForceToScenario(track.getScenarios().get(scenarioCoords), - sortedAvailableForceIDs); + sortedAvailableForceIDs); continue; } - StratconScenario scenario = setupScenario(scenarioCoords, randomForceID, campaign, contract, track); - - if (scenario != null) { - generatedScenarios.add(scenario); - } + scenario = setupScenario(scenarioCoords, randomForceID, campaign, contract, track); } - } - - // If we didn't generate any scenarios, we can just return here - if (generatedScenarios.isEmpty()) { - return; - } - // if under liaison command, pick a random scenario from the ones generated - // to set as required and attach liaison - if (contract.getCommandRights().isLiaison()) { - StratconScenario randomScenario = ObjectUtility.getRandomItem(generatedScenarios); - randomScenario.setRequiredScenario(true); - setAttachedUnitsModifier(randomScenario, contract); - } + if (scenario != null) { + // if under liaison command, pick a random scenario from the ones generated + // to set as required and attach liaison + if (contract.getCommandRights().isLiaison() && (randomInt(4) == 0)) { + scenario.setRequiredScenario(true); + setAttachedUnitsModifier(scenario, contract); + } - // now, we loop through all the scenarios we set up - // and generate the opfors / events / etc - // if not auto-assigning lances, we then back out the lance assignments. - for (StratconScenario scenario : generatedScenarios) { - finalizeBackingScenario(campaign, contract, track, autoAssignLances, scenario); + finalizeBackingScenario(campaign, contract, track, autoAssignLances, scenario); + } } } @@ -312,11 +431,11 @@ public static void generateScenariosForTrack(Campaign campaign, AtBContract cont int randomForceID = FORCE_NONE; if (availableForces > 0) { - int randomForceIndex = Compute.randomInt(availableForces); + int randomForceIndex = randomInt(availableForces); randomForceID = availableForceIDs.get(randomForceIndex); } - scenario = setupScenario(scenarioCoords, randomForceID, campaign, contract, track, template); + scenario = setupScenario(scenarioCoords, randomForceID, campaign, contract, track, template, false); } if (scenario == null) { @@ -330,6 +449,36 @@ public static void generateScenariosForTrack(Campaign campaign, AtBContract cont return scenario; } + /** + * Generates a reinforcement interception scenario for a given StratCon track. + * An interception scenario is set up at unoccupied coordinates on the track. + * If the scenario setup is successful, it is finalized and the deployment date for the + * scenario is set as the current date. + * + * @param campaign the current campaign + * @param contract the {@link AtBContract} for which the scenario is created + * @param track the {@link StratconTrackState} where the scenario is located, or {@code null} + * if not located on a track + * @param template the {@link ScenarioTemplate} used to create the scenario + * @param interceptedForce the {@link Force} that's being intercepted in the scenario + */ + public static @Nullable void generateReinforcementInterceptionScenario( + Campaign campaign, AtBContract contract, + StratconTrackState track, ScenarioTemplate template, Force interceptedForce) { + StratconCoords scenarioCoords = getUnoccupiedCoords(track, false); + + StratconScenario scenario = setupScenario(scenarioCoords, interceptedForce.getId(), campaign, + contract, track, template, true); + + if (scenario == null) { + logger.error("Failed to generate a random interception scenario, aborting scenario generation."); + return; + } + + finalizeBackingScenario(campaign, contract, track, true, scenario); + scenario.setDeploymentDate(campaign.getLocalDate()); + } + /** * Adds a {@link StratconScenario} to the specified contract. This scenario is cloaked so will * not be visible until the player uncovers it. @@ -477,7 +626,7 @@ public static void setScenarioParametersFromBiome(StratconTrackState track, Stra facilityBiome = facility.getBiomeTempMap().floorEntry(kelvinTemp).getValue(); } terrainType = facilityBiome.allowedTerrainTypes - .get(Compute.randomInt(facilityBiome.allowedTerrainTypes.size())); + .get(randomInt(facilityBiome.allowedTerrainTypes.size())); } else { terrainType = track.getTerrainTile(coords); } @@ -499,7 +648,7 @@ public static void setScenarioParametersFromBiome(StratconTrackState track, Stra // scenario // TODO: facility spaces will always have a relevant biome if (!backingScenario.isUsingFixedMap()) { - backingScenario.setMap(mapTypeList.get(Compute.randomInt(mapTypeList.size()))); + backingScenario.setMap(mapTypeList.get(randomInt(mapTypeList.size()))); } backingScenario.setLightConditions(); backingScenario.setWeatherConditions(); @@ -621,7 +770,7 @@ private static void swapInPlayerUnits(StratconScenario scenario, Campaign campai for (int forceID : forceIDs) { if (firstForce) { - scenario = setupScenario(scenarioCoords, forceID, campaign, contract, track, template); + scenario = setupScenario(scenarioCoords, forceID, campaign, contract, track, template, false); firstForce = false; if (scenario == null) { @@ -671,9 +820,9 @@ public static void deployForceToCoords(StratconCoords coords, int forceID, Campa // don't create a scenario on top of allied facilities StratconFacility facility = track.getFacility(coords); - boolean isNonAlliedFacility = (facility != null) && (facility.getOwner() != ForceAlignment.Allied); + boolean isNonAlliedFacility = (facility != null) && (facility.getOwner() != Allied); int targetNum = calculateScenarioOdds(track, contract, true); - boolean spawnScenario = (facility == null) && (Compute.randomInt(100) <= targetNum); + boolean spawnScenario = (facility == null) && (randomInt(100) <= targetNum); if (isNonAlliedFacility || spawnScenario) { StratconScenario scenario = setupScenario(coords, forceID, campaign, contract, track); @@ -704,7 +853,7 @@ public static void deployForceToCoords(StratconCoords coords, int forceID, Campa */ private static @Nullable StratconScenario setupScenario(StratconCoords coords, int forceID, Campaign campaign, AtBContract contract, StratconTrackState track) { - return setupScenario(coords, forceID, campaign, contract, track, null); + return setupScenario(coords, forceID, campaign, contract, track, null, false); } /** @@ -723,16 +872,17 @@ public static void deployForceToCoords(StratconCoords coords, int forceID, Campa * @param track The relevant StratCon track. * @param template A specific {@link ScenarioTemplate} to use for scenario setup, or * {@code null} to select the scenario template randomly. + * @param ignoreFacilities Whether we should ignore any facilities at the selected location * @return The newly set up {@link StratconScenario}. */ private static @Nullable StratconScenario setupScenario(StratconCoords coords, int forceID, Campaign campaign, AtBContract contract, StratconTrackState track, - @Nullable ScenarioTemplate template) { + @Nullable ScenarioTemplate template, boolean ignoreFacilities) { StratconScenario scenario; - if (track.getFacilities().containsKey(coords)) { + if (track.getFacilities().containsKey(coords) && !ignoreFacilities) { StratconFacility facility = track.getFacility(coords); - boolean alliedFacility = facility.getOwner() == ForceAlignment.Allied; + boolean alliedFacility = facility.getOwner() == Allied; template = StratconScenarioFactory.getFacilityScenario(alliedFacility); scenario = generateScenario(campaign, contract, track, forceID, coords, template); setupFacilityScenario(scenario, facility); @@ -777,7 +927,7 @@ private static void setupFacilityScenario(StratconScenario scenario, StratconFac // - if so indicated by parameter, roll a random allied facility objective and // add it if not defend AtBScenarioModifier objectiveModifier = null; - boolean alliedFacility = facility.getOwner() == ForceAlignment.Allied; + boolean alliedFacility = facility.getOwner() == Allied; objectiveModifier = alliedFacility ? AtBScenarioModifier.getRandomAlliedFacilityModifier() : AtBScenarioModifier.getRandomHostileFacilityModifier(); @@ -904,77 +1054,271 @@ private static void increaseFatigue(int forceID, Campaign campaign) { * Worker function that processes the effects of deploying a reinforcement force to a scenario * * @param reinforcementType the type of reinforcement being deployed - * @param campaignState the state of the campaign - * @param scenario the current scenario - * @param campaign the campaign instance + * @param campaignState the state of the campaign + * @param scenario the current scenario + * @param campaign the campaign instance * @return {@code true} if the reinforcement deployment is successful, {@code false} otherwise */ - public static boolean processReinforcementDeployment(ReinforcementEligibilityType reinforcementType, - StratconCampaignState campaignState, StratconScenario scenario, Campaign campaign) { - // if the force is already deployed to the track, we're done - // if the force is a fight lance or we're using a support point - // if there is an SP to burn, burn it and we're done - // if there is a VP to burn, burn it and we're done - // now, roll 2d6 + lance commander tactics - // 9+ = deploy - // 6+ = deploy, apply negative modifier to scenario - // 2+ = fail to deploy, apply negative modifier to scenario; if fight lance, - // treat as 6+ - - switch (reinforcementType) { - case FightLance: - case SupportPoint: - if (campaignState.getSupportPoints() > 0) { - campaignState.useSupportPoint(); - return true; - } + public static ReinforcementResultsType processReinforcementDeployment( + Force force, ReinforcementEligibilityType reinforcementType, StratconCampaignState campaignState, + StratconScenario scenario, Campaign campaign) { + final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.AtBStratCon", + MekHQ.getMHQOptions().getLocale()); - int tactics = scenario.getBackingScenario().getLanceCommanderSkill(SkillType.S_TACTICS, campaign); - int roll = Compute.d6(2); - int result = roll + tactics; - - StringBuilder reportStatus = new StringBuilder(); - reportStatus - .append(String.format("Attempting to reinforce scenario %s without SP/VP, roll 2d6 + %d: %d", - scenario.getName(), tactics, result)); - - // fail to reinforce - if ((result < 6) && (reinforcementType != ReinforcementEligibilityType.FightLance)) { - reportStatus.append(" - reinforcement attempt failed."); - campaign.addReport(reportStatus.toString()); - return false; - // succeed but get an extra negative event added to the scenario - } else if (result < 9) { - MapLocation mapLocation = scenario.getScenarioTemplate().mapParameters.getMapLocation(); - AtBScenarioModifier scenarioModifier = AtBScenarioModifier.getRandomBattleModifier(mapLocation, - false); - - // keep rolling until we get an applicable one - // TODO: have the AtBScenarioModifier sort these out instead for performance? - while (scenarioModifier.getEventTiming() != EventTiming.PostForceGeneration) { - scenarioModifier = AtBScenarioModifier.getRandomBattleModifier(mapLocation, false); - } + if (reinforcementType.equals(ReinforcementEligibilityType.CHAINED_SCENARIO)) { + return SUCCESS; + } - scenarioModifier.processModifier(scenario.getBackingScenario(), campaign, - EventTiming.PostForceGeneration); + AtBContract contract = campaignState.getContract(); - reportStatus.append(String.format( - " - reinforcement attempt succeeded; extra negative modifier (%s) applied to scenario.", - scenarioModifier.getModifierName())); - campaign.addReport(reportStatus.toString()); - return true; - // succeed without reservation + // Start by determining who will be making the attempt + Person commandLiaison = campaign.getSeniorAdminCommandPerson(); + + if (commandLiaison == null) { + campaign.addReport(String.format(resources.getString("reinforcementsNoAdmin.text"), + scenario.getName(), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), + CLOSING_SPAN_TAG)); + return FAILED; + } + + // Assuming we found a relevant character, spend the support point required for the attempt + if (campaignState.getSupportPoints() >= 1) { + campaignState.useSupportPoint(); + } else { + campaign.addReport(String.format(resources.getString("reinforcementsNoSupportPoints.text"), + scenario.getName(), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), + CLOSING_SPAN_TAG)); + return FAILED; + } + + // Then calculate the target number and modifiers + + Skill skill = commandLiaison.getSkill(S_ADMIN); + + if (skill == null) { + campaign.addReport(String.format(resources.getString("reinforcementsNoAdminSkill.text"), + scenario.getName(), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), + CLOSING_SPAN_TAG), commandLiaison.getHyperlinkedFullTitle()); + return FAILED; + } + + int skillTargetNumber = skill.getFinalSkillValue(); + + TargetRoll reinforcementTargetNumber = new TargetRoll(); + + // Base Target Number + reinforcementTargetNumber.addModifier(skillTargetNumber, "Base TN"); + + // Facilities Modifier + StratconTrackState track = null; + for (StratconTrackState trackState : campaignState.getTracks()) { + if (trackState.getScenarios().containsValue(scenario)) { + track = trackState; + break; + } + } + + int facilityModifier = 0; + if (track != null) { + for (StratconFacility facility : track.getFacilities().values()) { + if (facility.getOwner().equals(ForceAlignment.Player) || facility.getOwner().equals(Allied)) { + facilityModifier--; } else { - reportStatus.append(" - reinforcement attempt succeeded;"); - campaign.addReport(reportStatus.toString()); - return true; + facilityModifier++; } - case ChainedScenario: - return true; - case None: - default: - return false; + } + } + + reinforcementTargetNumber.addModifier(facilityModifier, "Facilities"); + + // Skill Modifier + int skillModifier = -contract.getAllySkill().getAdjustedValue(); + + ContractCommandRights commandRights = contract.getCommandRights(); + if (commandRights.isIndependent()) { + if (campaign.getCampaignOptions().getUnitRatingMethod().isCampaignOperations()) { + skillModifier = -campaign.getReputation().getAverageSkillLevel().getAdjustedValue(); + } + } + + skillModifier += contract.getEnemySkill().getAdjustedValue(); + + reinforcementTargetNumber.addModifier(skillModifier, "Skill"); + + // Liaison Modifier + int liaisonModifier = 0; + if (commandRights.isLiaison()) { + liaisonModifier -= 1; + } else if (commandRights.isHouse() || commandRights.isIntegrated()) { + liaisonModifier -= 2; + } + + reinforcementTargetNumber.addModifier(liaisonModifier, "Command Rights"); + + // Make the roll + int roll = d6(2); + + // If the formation is in Fight Stance, use the highest of two rolls + String fightStanceReport = ""; + if (reinforcementType == FIGHT_LANCE) { + int secondRoll = d6(2); + roll = max(roll, secondRoll); + fightStanceReport = String.format(" (%s)", roll); + } + + StringBuilder modifierString = new StringBuilder(); + + for (TargetRollModifier modifier : reinforcementTargetNumber.getModifiers()) { + modifierString.append(modifier.getDesc()).append(' ').append(modifier.getValue()).append(' '); + } + + logger.info(String.format("Reinforcement Roll Modifiers: %s", modifierString)); + + StringBuilder reportStatus = new StringBuilder(); + reportStatus.append(String.format(resources.getString("reinforcementsAttempt.text"), + scenario.getName(), roll, fightStanceReport, reinforcementTargetNumber.getValue())); + + // Critical Failure + if (roll == 2) { + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementsCriticalFailure.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), + CLOSING_SPAN_TAG)); + campaign.addReport(reportStatus.toString()); + return FAILED; + } + + // Reinforcement successful + if (roll >= reinforcementTargetNumber.getValue()) { + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementsSuccess.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorPositiveHexColor()), + CLOSING_SPAN_TAG)); + campaign.addReport(reportStatus.toString()); + return SUCCESS; + } + + // Reinforcement roll failed, make interception check + int interceptionOdds = calculateScenarioOdds(track, campaignState.getContract(), true); + int interceptionRoll = randomInt(100); + + // Check passed + if (interceptionRoll >= interceptionOdds) { + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementsCommandFailure.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), + CLOSING_SPAN_TAG)); + campaign.addReport(reportStatus.toString()); + return FAILED; + } + + // Check failed, but enemy is routed + if (contract.getMoraleLevel().isRouted()) { + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementsSuccessRouted.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorPositiveHexColor()), + CLOSING_SPAN_TAG)); + campaign.addReport(reportStatus.toString()); + return SUCCESS; + } + + // Check failed, enemy attempt interception + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementsInterceptionAttempt.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorWarningHexColor()), + CLOSING_SPAN_TAG)); + + UUID commanderId = force.getForceCommanderID(); + + if (commanderId == null) { + logger.error("Force Commander ID is null."); + + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementsErrorNoCommander.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), + CLOSING_SPAN_TAG)); + campaign.addReport(reportStatus.toString()); + return FAILED; + } + + Person commander = campaign.getPerson(commanderId); + + if (commander == null) { + logger.error("Failed to fetch commander from ID."); + + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementsErrorUnableToFetchCommander.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), + CLOSING_SPAN_TAG)); + campaign.addReport(reportStatus.toString()); + return FAILED; + } + + Skill tactics = commander.getSkill(S_TACTICS); + + if (tactics == null) { + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementCommanderNoSkill.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), + CLOSING_SPAN_TAG)); + campaign.addReport(reportStatus.toString()); + + MapLocation mapLocation = scenario.getScenarioTemplate().mapParameters.getMapLocation(); + + String templateString = "data/scenariotemplates/%sReinforcements Intercepted.xml"; + + ScenarioTemplate scenarioTemplate = switch (mapLocation) { + case AllGroundTerrain, SpecificGroundTerrain -> ScenarioTemplate.Deserialize(String.format(templateString, "")); + case Space -> ScenarioTemplate.Deserialize(String.format(templateString, "Space ")); + case LowAtmosphere -> ScenarioTemplate.Deserialize(String.format(templateString, "Low-Atmosphere ")); + }; + + generateReinforcementInterceptionScenario(campaign, contract, track, scenarioTemplate, force); + + return INTERCEPTED; + } + + roll = d6(2); + int baseTargetNumber = 9; + int targetNumber = baseTargetNumber - tactics.getFinalSkillValue(); + + if (roll >= targetNumber) { + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementEvasionSuccessful.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorPositiveHexColor()), + CLOSING_SPAN_TAG, roll, targetNumber)); + + campaign.addReport(reportStatus.toString()); + + if (campaign.getCampaignOptions().isUseFatigue()) { + increaseFatigue(force.getId(), campaign); + } + + return DELAYED; } + + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementEvasionUnsuccessful.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), + CLOSING_SPAN_TAG, roll, targetNumber)); + campaign.addReport(reportStatus.toString()); + + MapLocation mapLocation = scenario.getScenarioTemplate().mapParameters.getMapLocation(); + + String templateString = "data/scenariotemplates/%sReinforcements Intercepted.xml"; + + ScenarioTemplate scenarioTemplate = switch (mapLocation) { + case AllGroundTerrain, SpecificGroundTerrain -> ScenarioTemplate.Deserialize(String.format(templateString, "")); + case Space -> ScenarioTemplate.Deserialize(String.format(templateString, "Space ")); + case LowAtmosphere -> ScenarioTemplate.Deserialize(String.format(templateString, "Low-Atmosphere ")); + }; + + generateReinforcementInterceptionScenario(campaign, contract, track, scenarioTemplate, force); + + return INTERCEPTED; } /** @@ -1000,9 +1344,9 @@ private static void assignAppropriateExtraForceToScenario(StratconScenario scena mapLocations.add(AllGroundTerrain); // can only add ground units to ground battles } - MapLocation selectedLocation = mapLocations.get(Compute.randomInt(mapLocations.size())); + MapLocation selectedLocation = mapLocations.get(randomInt(mapLocations.size())); List forceIDs = sortedAvailableForceIDs.get(selectedLocation); - int forceIndex = Compute.randomInt(forceIDs.size()); + int forceIndex = randomInt(forceIDs.size()); int forceID = forceIDs.get(forceIndex); forceIDs.remove(forceIndex); @@ -1032,7 +1376,7 @@ public static void commitPrimaryForces(Campaign campaign, StratconScenario scena } // set the # of rerolls based on the actual lance assigned. - int tactics = scenario.getBackingScenario().getLanceCommanderSkill(SkillType.S_TACTICS, campaign); + int tactics = scenario.getBackingScenario().getLanceCommanderSkill(S_TACTICS, campaign); scenario.getBackingScenario().setRerolls(tactics); // The number of defensive points available to a force entering a scenario is // 2 x tactics. By default, those points are spent on conventional minefields. @@ -1127,8 +1471,9 @@ private static Map> sortForcesByMapType(List * given force, on the * given track, using the given template. Also registers it with the campaign. */ - static @Nullable StratconScenario generateScenario(Campaign campaign, AtBContract contract, StratconTrackState track, - int forceID, StratconCoords coords, ScenarioTemplate template) { + static @Nullable StratconScenario generateScenario(Campaign campaign, AtBContract contract, + StratconTrackState track, int forceID, + StratconCoords coords, ScenarioTemplate template) { StratconScenario scenario = new StratconScenario(); if (template == null) { @@ -1225,7 +1570,7 @@ private static void applyFacilityModifiers(StratconScenario scenario, StratconTr if (scenarioAtFacility) { modifierIDs = facility.getLocalModifiers(); - } else if (facility.isVisible() || (Compute.randomInt(100) <= 75)) { + } else if (facility.isVisible() || (randomInt(100) <= 75)) { modifierIDs = facility.getSharedModifiers(); } @@ -1277,7 +1622,7 @@ private static void setAlliedForceModifier(StratconScenario scenario, AtBContrac // if an allied unit is present, then we want to make sure that // it's ground units for ground battles - if (Compute.randomInt(100) <= alliedUnitOdds) { + if (randomInt(100) <= alliedUnitOdds) { if ((backingScenario.getTemplate().mapParameters.getMapLocation() == LowAtmosphere) || (backingScenario.getTemplate().mapParameters.getMapLocation() == Space)) { backingScenario.addScenarioModifier( @@ -1344,15 +1689,14 @@ public static void setAttachedUnitsModifier(StratconScenario scenario, AtBContra * current campaign date */ private static void setScenarioDates(StratconTrackState track, Campaign campaign, StratconScenario scenario) { - int deploymentDay = track.getDeploymentTime() < 7 ? Compute.randomInt(7 - track.getDeploymentTime()) : 0; + int deploymentDay = track.getDeploymentTime() < 7 ? randomInt(7 - track.getDeploymentTime()) : 0; setScenarioDates(deploymentDay, track, campaign, scenario); } /** * Worker function that sets scenario deploy/battle/return dates based on the - * track's properties and - * current campaign date. Takes a fixed deployment day of X days from campaign's - * today date. + * track's properties and current campaign date. Takes a fixed deployment day of X days from + * campaign's today date. */ private static void setScenarioDates(int deploymentDay, StratconTrackState track, Campaign campaign, StratconScenario scenario) { @@ -1360,7 +1704,7 @@ private static void setScenarioDates(int deploymentDay, StratconTrackState track // safety code to prevent attempts to generate random int with upper bound of 0 // which is apparently illegal int battleDay = deploymentDay - + (track.getDeploymentTime() > 0 ? Compute.randomInt(track.getDeploymentTime()) : 0); + + (track.getDeploymentTime() > 0 ? randomInt(track.getDeploymentTime()) : 0); int returnDay = deploymentDay + track.getDeploymentTime(); LocalDate deploymentDate = campaign.getLocalDate().plusDays(deploymentDay); @@ -1473,7 +1817,7 @@ public static List getAvailableForceIDs(int unitType, Campaign campaign int primaryUnitType = force.getPrimaryUnitType(campaign); boolean noReinforcementRestriction = !reinforcements || - (getReinforcementType(force.getId(), currentTrack, campaign, campaignState) != ReinforcementEligibilityType.None); + (getReinforcementType(force.getId(), currentTrack, campaign, campaignState) != ReinforcementEligibilityType.NONE); if ((force.getScenarioId() <= 0) && !force.getAllUnits(true).isEmpty() @@ -1554,39 +1898,51 @@ public static List getEligibleDefensiveUnits(Campaign campaign) { * * @return List of unit IDs. */ - public static List getEligibleLeadershipUnits(Campaign campaign, Set forceIDs) { - List retVal = new ArrayList<>(); + public static List getEligibleLeadershipUnits(Campaign campaign, Set forceIDs, int leadershipSkill) { + List eligibleUnits = new ArrayList<>(); + + // If there is no leadership skill, we shouldn't continue + if (leadershipSkill <= 0) { + return eligibleUnits; + } // The criteria are as follows: - // - unit is of a different unit type than the primary unit type of the force - // - unit has a lower BV than the force's lowest BV unit + // - unit is eligible to be spawned on the scenario type + // - unit has a lower BV than the BV budget granted from Leadership + // Leadership budget is capped at 5 levels + int totalBudget = min(BASE_LEADERSHIP_BUDGET * leadershipSkill, BASE_LEADERSHIP_BUDGET * 5); - Integer lowestBV = getLowestBV(campaign, forceIDs); + int primaryUnitType = getPrimaryUnitType(campaign, forceIDs); - // no units assigned, the rest is meaningless. - if (lowestBV == null) { - return retVal; + // If there are no units (somehow), we've no reason to continue + if (primaryUnitType == -1) { + return eligibleUnits; } - int primaryUnitType = getPrimaryUnitType(campaign, forceIDs); - int generalUnitType = StratconScenarioFactory.convertSpecificUnitTypeToGeneral(primaryUnitType); + int generalUnitType = convertSpecificUnitTypeToGeneral(primaryUnitType); - for (Unit u : campaign.getUnits()) { - // the general idea is that we want a different unit type than the primary - // but also something that can be deployed to the scenario - - // e.g. no infantry on air scenarios etc. - boolean validUnitType = (primaryUnitType != u.getEntity().getUnitType()) && - forceCompositionMatchesDeclaredUnitType(u.getEntity().getUnitType(), generalUnitType, true); - - if (validUnitType && !u.isDeployed() && !u.isMothballed() - && (u.getEntity().calculateBattleValue() < lowestBV) - && (u.checkDeployment() == null) - && !isUnitDeployedToStratCon(u)) { - retVal.add(u); + for (UUID unitId : campaign.getForce(0).getAllUnits(true)) { + Unit unit = campaign.getUnit(unitId); + if (unit == null) { + continue; + } + + // the general idea is that we want something that can be deployed to the scenario - + // e.g., no infantry on air scenarios etc. + boolean validUnitType = (forceCompositionMatchesDeclaredUnitType(unit.getEntity().getUnitType(), + generalUnitType, true)); + + if (validUnitType + && !unit.isDeployed() + && !unit.isMothballed() + && (unit.getEntity().calculateBattleValue(true, true) <= totalBudget) + && (unit.checkDeployment() == null) + && !isUnitDeployedToStratCon(unit)) { + eligibleUnits.add(unit); } } - return retVal; + return eligibleUnits; } /** @@ -1605,35 +1961,6 @@ public static boolean isUnitDeployedToStratCon(Unit u) { contract.getStratconCampaignState().isForceDeployedHere(u.getForceId())); } - /** - * Given a campaign and a list of force IDs, calculate the unit with the lowest - * BV. - */ - private static Integer getLowestBV(Campaign campaign, Set forceIDs) { - Integer lowestBV = null; - - for (int forceID : forceIDs) { - Force force = campaign.getForce(forceID); - if (force == null) { - continue; - } - - for (UUID id : force.getUnits()) { - if (campaign.getUnit(id) == null) { - continue; - } - - int currentBV = campaign.getUnit(id).getEntity().calculateBattleValue(); - - if ((lowestBV == null) || (currentBV < lowestBV)) { - lowestBV = currentBV; - } - } - } - - return lowestBV; - } - /** * Calculates the majority unit type for the forces given the IDs. */ @@ -1679,26 +2006,31 @@ public static ReinforcementEligibilityType getReinforcementType(int forceID, Str .flatMap(contract -> contract.getStratconCampaignState().getTracks().stream()) .anyMatch(track -> !Objects.equals(track, trackState) && track.getAssignedForceCoords().containsKey(forceID))) { - return ReinforcementEligibilityType.None; + return ReinforcementEligibilityType.NONE; } // TODO: If the force has completed a scenario which allows it, // it can deploy "for free" (ReinforcementEligibilityType.ChainedScenario) - // if the force is in 'fight' stance, it'll be able to deploy using 'fight - // lance' rules - if (campaign.getStrategicFormationsTable().containsKey(forceID) - && (campaign.getStrategicFormationsTable().get(forceID).getRole().isFighting())) { - return ReinforcementEligibilityType.FightLance; - } + // if the force is in 'fight' stance, it'll be able to deploy using 'fight lance' rules + if (campaign.getStrategicFormationsTable().containsKey(forceID)) { + Hashtable strategicFormations = campaign.getStrategicFormationsTable(); + StrategicFormation formation = strategicFormations.get(forceID); - // otherwise, the force requires support points to deploy - if (campaignState.getSupportPoints() > 0) { - return ReinforcementEligibilityType.SupportPoint; + if (formation == null) { + return ReinforcementEligibilityType.NONE; + } + + if (campaignState.getSupportPoints() > 0) { + if (formation.getRole().isFighting()) { + return FIGHT_LANCE; + } else { + return ReinforcementEligibilityType.REGULAR; + } + } } - /// if we don't have any of these things, it can't be deployed - return ReinforcementEligibilityType.None; + return ReinforcementEligibilityType.NONE; } /** @@ -1721,7 +2053,7 @@ public static boolean canManuallyDeployAnyForce(StratconCoords coords, boolean nonCloakedOrNoscenario = (scenario == null) || scenario.getBackingScenario().isCloaked(); StratconFacility facility = track.getFacility(coords); - boolean alliedFacility = (facility != null) && (facility.getOwner() == ForceAlignment.Allied); + boolean alliedFacility = (facility != null) && (facility.getOwner() == Allied); return (!track.areAnyForceDeployedTo(coords) || alliedFacility) && nonCloakedOrNoscenario; } @@ -1734,7 +2066,7 @@ public static boolean canManuallyDeployAnyForce(StratconCoords coords, public static int calculateScenarioOdds(StratconTrackState track, AtBContract contract, boolean isReinforcements) { if (contract.getMoraleLevel().isRouted()) { - return 0; + return -1; } int moraleModifier = switch (contract.getMoraleLevel()) { @@ -1913,10 +2245,10 @@ public static void switchFacilityOwner(StratconFacility facility) { // if we the facility didn't have any data defined for what happens when it's // captured // fall back to the default of just switching the owner - if (facility.getOwner() == ForceAlignment.Allied) { - facility.setOwner(ForceAlignment.Opposing); + if (facility.getOwner() == Allied) { + facility.setOwner(Opposing); } else { - facility.setOwner(ForceAlignment.Allied); + facility.setOwner(Allied); } } @@ -1985,8 +2317,8 @@ public static boolean processIgnoredScenario(StratconScenario scenario, Stratcon if (localFacility != null) { // if the ignored scenario was on top of an allied facility // then it'll get captured, and the player will possibly lose a SO - if (localFacility.getOwner() == ForceAlignment.Allied) { - localFacility.setOwner(ForceAlignment.Opposing); + if (localFacility.getOwner() == Allied) { + localFacility.setOwner(Opposing); } return true; @@ -2007,7 +2339,7 @@ public static boolean processIgnoredScenario(StratconScenario scenario, Stratcon scenario.setCoords(newCoords); - int daysForward = Math.max(1, track.getDeploymentTime()); + int daysForward = max(1, track.getDeploymentTime()); scenario.setDeploymentDate(scenario.getDeploymentDate().plusDays(daysForward)); scenario.setActionDate(scenario.getActionDate().plusDays(daysForward)); @@ -2051,16 +2383,20 @@ public void startup() { */ @Subscribe public void handleNewDay(NewDayEvent ev) { + Campaign campaign = ev.getCampaign(); + // don't do any of this if StratCon isn't turned on - if (!ev.getCampaign().getCampaignOptions().isUseStratCon()) { + if (!campaign.getCampaignOptions().isUseStratCon()) { return; } - boolean isMonday = ev.getCampaign().getLocalDate().getDayOfWeek() == DayOfWeek.MONDAY; - boolean isStartOfMonth = ev.getCampaign().getLocalDate().getDayOfMonth() == 1; + + LocalDate today = campaign.getLocalDate(); + boolean isMonday = today.getDayOfWeek() == DayOfWeek.MONDAY; + boolean isStartOfMonth = today.getDayOfMonth() == 1; // run scenario generation routine for every track attached to an active // contract - for (AtBContract contract : ev.getCampaign().getActiveAtBContracts()) { + for (AtBContract contract : campaign.getActiveAtBContracts()) { StratconCampaignState campaignState = contract.getStratconCampaignState(); if (campaignState != null) { @@ -2071,7 +2407,7 @@ public void handleNewDay(NewDayEvent ev) { // please do this before generating scenarios for track // to avoid unintentionally cleaning out integrated force deployments on // 0-deployment-length tracks - processTrackForceReturnDates(track, ev.getCampaign()); + processTrackForceReturnDates(track, campaign); processFacilityEffects(track, campaignState, isStartOfMonth); @@ -2079,17 +2415,31 @@ public void handleNewDay(NewDayEvent ev) { // fail it and apply consequences for (StratconScenario scenario : track.getScenarios().values()) { if ((scenario.getDeploymentDate() != null) && - scenario.getDeploymentDate().isBefore(ev.getCampaign().getLocalDate()) && + scenario.getDeploymentDate().isBefore(campaign.getLocalDate()) && scenario.getPrimaryForceIDs().isEmpty()) { processIgnoredScenario(scenario, campaignState); } } - // on monday, generate new scenarios + // on monday, generate new scenario dates if (isMonday) { - generateScenariosForTrack(ev.getCampaign(), contract, track); + generateScenariosDatesForWeek(campaign, campaignState, contract, track); } } + + List weeklyScenarioDates = campaignState.getWeeklyScenarios(); + + if (weeklyScenarioDates.contains(today)) { + int scenarioCount = 0; + for (LocalDate date : weeklyScenarioDates) { + if (date.equals(today)) { + scenarioCount++; + } + } + weeklyScenarioDates.removeIf(date -> date.equals(today)); + + generateDailyScenariosForTrack(campaign, campaignState, contract, scenarioCount); + } } } } diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconScenario.java b/MekHQ/src/mekhq/campaign/stratcon/StratconScenario.java index 6ae8c6135a..4298d8226c 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconScenario.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconScenario.java @@ -15,6 +15,7 @@ import jakarta.xml.bind.annotation.XmlTransient; import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import megamek.common.annotations.Nullable; import mekhq.MekHQ; import mekhq.adapter.DateAdapter; import mekhq.campaign.Campaign; @@ -104,13 +105,14 @@ public void addForce(Force force, String templateID, Campaign campaign) { * Add an individual unit to the backing scenario, trying to associate it with the given template. * Performs house keeping on the unit and scenario and invokes a deployment changed event. */ - public void addUnit(Unit unit, String templateID, boolean useLeadershipPoint) { + public void addUnit(Unit unit, String templateID, boolean useLeadership) { if (!backingScenario.containsPlayerUnit(unit.getId())) { backingScenario.addUnit(unit.getId(), templateID); unit.setScenarioId(getBackingScenarioID()); - if (useLeadershipPoint) { - useLeadershipPoint(); + if (useLeadership) { + int baseBattleValue = unit.getEntity().calculateBattleValue(true, true); + leadershipPointsUsed += baseBattleValue; } MekHQ.triggerEvent(new DeploymentChangedEvent(unit, getBackingScenario())); @@ -165,10 +167,10 @@ public void setCurrentState(ScenarioState state) { @Override public String getInfo() { - return getInfo(true); + return getInfo(null, true); } - public String getInfo(boolean html) { + public String getInfo(@Nullable Campaign campaign, boolean html) { StringBuilder stateBuilder = new StringBuilder(); if (isStrategicObjective()) { @@ -201,22 +203,33 @@ public String getInfo(boolean html) { if (deploymentDate != null) { stateBuilder.append("Deployment Date: ") - .append(deploymentDate.toString()) + .append(deploymentDate) .append("
"); } if (actionDate != null) { stateBuilder.append("Battle Date: ") - .append(actionDate.toString()) + .append(actionDate) .append("
"); } if (returnDate != null) { stateBuilder.append("Return Date: ") - .append(returnDate.toString()) + .append(returnDate) .append("
"); } + if (campaign != null) { + AtBDynamicScenario backingScenario = getBackingScenario(); + + if (backingScenario != null) { + stateBuilder.append(String.format("Hostile BV: %d
", + backingScenario.getTeamTotalBattleValue(campaign, false))); + stateBuilder.append(String.format("Allied BV: %d", + backingScenario.getTeamTotalBattleValue(campaign, true))); + } + } + stateBuilder.append(""); return stateBuilder.toString(); } @@ -353,11 +366,7 @@ public int getLeadershipPointsUsed() { return leadershipPointsUsed; } - public void setLeadershipPointsUsed(int leadershipPointsUsed) { + public void setAvailableLeadershipBudget(int leadershipPointsUsed) { this.leadershipPointsUsed = leadershipPointsUsed; } - - public void useLeadershipPoint() { - leadershipPointsUsed++; - } } diff --git a/MekHQ/src/mekhq/gui/BriefingTab.java b/MekHQ/src/mekhq/gui/BriefingTab.java index a0bcd42608..a0d192e55d 100644 --- a/MekHQ/src/mekhq/gui/BriefingTab.java +++ b/MekHQ/src/mekhq/gui/BriefingTab.java @@ -320,12 +320,11 @@ private void addMission() { ? new NewAtBContractDialog(getFrame(), true, getCampaign()) : new NewContractDialog(getFrame(), true, getCampaign()); ncd.setVisible(true); - this.setVisible(false); comboMission.setSelectedItem(ncd.getContract()); - } else { + } + if (mtd.isMission()) { CustomizeMissionDialog cmd = new CustomizeMissionDialog(getFrame(), true, null, getCampaign()); cmd.setVisible(true); - this.setVisible(false); comboMission.setSelectedItem(cmd.getMission()); } } diff --git a/MekHQ/src/mekhq/gui/StratconPanel.java b/MekHQ/src/mekhq/gui/StratconPanel.java index 389ca18b62..fc6326052c 100644 --- a/MekHQ/src/mekhq/gui/StratconPanel.java +++ b/MekHQ/src/mekhq/gui/StratconPanel.java @@ -18,7 +18,6 @@ import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.force.Force; -import mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment; import mekhq.campaign.stratcon.*; import mekhq.campaign.stratcon.StratconBiomeManifest.ImageType; import mekhq.gui.stratcon.StratconScenarioWizard; @@ -40,6 +39,8 @@ import java.util.HashMap; import java.util.Map; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Allied; + /** * This panel handles AtB-Stratcon GUI interactions with a specific scenario * track. @@ -144,7 +145,7 @@ public void selectTrack(StratconCampaignState campaignState, StratconTrackState // clear hex selection boardState.selectedX = null; boardState.selectedY = null; - infoArea.setText(buildSelectedHexInfo()); + infoArea.setText(buildSelectedHexInfo(campaign)); repaint(); } @@ -471,7 +472,7 @@ private boolean imageLoaded(String imageKey) { } private BufferedImage getFacilityImage(StratconFacility facility) { - String imageKeyPrefix = facility.getOwner() == ForceAlignment.Allied ? StratconBiomeManifest.FACILITY_ALLIED + String imageKeyPrefix = facility.getOwner() == Allied ? StratconBiomeManifest.FACILITY_ALLIED : StratconBiomeManifest.FACILITY_HOSTILE; String imageKey = imageKeyPrefix + facility.getFacilityType().name(); @@ -569,7 +570,7 @@ private void drawScenarios(Graphics2D g2D) { if (currentTrack.getFacility(currentCoords) == null) { drawTextEffect(g2D, scenarioMarker, "Hostile Force Detected", currentCoords); - } else if (currentTrack.getFacility(currentCoords).getOwner() == ForceAlignment.Allied) { + } else if (currentTrack.getFacility(currentCoords).getOwner() == Allied) { drawTextEffect(g2D, scenarioMarker, "Under Attack!", currentCoords); } } @@ -610,7 +611,7 @@ private void drawFacilities(Graphics2D g2D) { StratconFacility facility = currentTrack.getFacility(currentCoords); if ((facility != null) && (facility.isVisible() || trackRevealed || currentTrack.isGmRevealed())) { - g2D.setColor(facility.getOwner() == ForceAlignment.Allied ? Color.CYAN : Color.RED); + g2D.setColor(facility.getOwner() == Allied ? Color.CYAN : Color.RED); BufferedImage facilityImage = getFacilityImage(facility); @@ -813,7 +814,7 @@ public void mouseReleasedHandler(MouseEvent e) { boolean pointFoundOnBoard = detectClickedHex(); if (pointFoundOnBoard) { - infoArea.setText(buildSelectedHexInfo()); + infoArea.setText(buildSelectedHexInfo(campaign)); } repaint(); @@ -850,7 +851,7 @@ public StratconCoords getSelectedCoords() { * containing info such as whether it's been revealed, assigned forces, * scenarios, facilities, etc. */ - private String buildSelectedHexInfo() { + private String buildSelectedHexInfo(Campaign campaign) { StringBuilder infoBuilder = new StringBuilder(); infoBuilder.append("
"); @@ -864,13 +865,13 @@ private String buildSelectedHexInfo() { boolean coordsRevealed = currentTrack.hasActiveTrackReveal() || currentTrack.getRevealedCoords().contains(boardState.getSelectedCoords()); if (coordsRevealed) { - infoBuilder.append("Recon complete
"); + infoBuilder.append("Recon Complete
"); } if (currentTrack.getAssignedCoordForces().containsKey(boardState.getSelectedCoords())) { for (int forceID : currentTrack.getAssignedCoordForces().get(boardState.getSelectedCoords())) { - Force force = campaign.getForce(forceID); + Force force = this.campaign.getForce(forceID); infoBuilder.append(force.getName()).append(" assigned"); if (currentTrack.getStickyForces().contains(forceID)) { @@ -890,12 +891,12 @@ private String buildSelectedHexInfo() { if ((facility != null) && (facility.getFacilityType() != null)) { if (facility.isStrategicObjective()) { infoBuilder.append(String.format("
Contract objective located", - facility.getOwner() == ForceAlignment.Allied + facility.getOwner() == Allied ? MekHQ.getMHQOptions().getFontColorPositiveHexColor() : MekHQ.getMHQOptions().getFontColorNegativeHexColor())); } infoBuilder.append("") @@ -912,14 +913,14 @@ private String buildSelectedHexInfo() { } else { infoBuilder.append("Recon incomplete"); + .append("'>Recon Incomplete"); } infoBuilder.append("
"); StratconScenario selectedScenario = getSelectedScenario(); if ((selectedScenario != null) && ((selectedScenario.getDeploymentDate() != null) || currentTrack.isGmRevealed())) { - infoBuilder.append(selectedScenario.getInfo()); + infoBuilder.append(selectedScenario.getInfo(campaign, true)); } infoBuilder.append(""); diff --git a/MekHQ/src/mekhq/gui/StratconTab.java b/MekHQ/src/mekhq/gui/StratconTab.java index 5999cfc3d3..896caa2fba 100644 --- a/MekHQ/src/mekhq/gui/StratconTab.java +++ b/MekHQ/src/mekhq/gui/StratconTab.java @@ -13,6 +13,7 @@ */ package mekhq.gui; +import megamek.client.ui.swing.util.UIUtil; import megamek.common.event.Subscribe; import mekhq.MekHQ; import mekhq.campaign.event.MissionCompletedEvent; @@ -48,7 +49,8 @@ public class StratconTab extends CampaignGuiTab { private StratconPanel stratconPanel; private JPanel infoPanel; - private JComboBox cboCurrentTrack; + private DefaultListModel listModel = new DefaultListModel<>(); + private JList listCurrentTrack; private JLabel infoPanelText; private JLabel campaignStatusText; private JLabel objectiveStatusText; @@ -87,7 +89,7 @@ public void initTab() { objectiveStatusText.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent me) { - TrackDropdownItem currentTDI = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); + TrackDropdownItem currentTDI = listCurrentTrack.getSelectedValue(); StratconCampaignState campaignState = currentTDI.contract.getStratconCampaignState(); objectivesCollapsed = !objectivesCollapsed; objectiveStatusText.setText(getStrategicObjectiveText(campaignState)); @@ -116,47 +118,54 @@ public void mousePressed(MouseEvent me) { * Worker function that sets up the layout of the right-side info panel. */ private void initializeInfoPanel() { - infoPanel = new JPanel(); - infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.PAGE_AXIS)); + int gridY = 0; + infoPanel = new JPanel(new GridBagLayout()); + GridBagConstraints constraints = new GridBagConstraints(); + constraints.anchor = GridBagConstraints.NORTHWEST; + constraints.gridx = gridY++; - infoPanel.add(new JLabel("Current Campaign Status:")); - infoPanel.add(campaignStatusText); + infoPanel.add(new JLabel("Current Campaign Status:"), constraints); + + constraints.gridy = gridY++; + infoPanel.add(campaignStatusText, constraints); JButton btnManageCampaignState = new JButton("Manage SP/CVP"); - btnManageCampaignState.setHorizontalAlignment(SwingConstants.LEFT); - btnManageCampaignState.setVerticalAlignment(SwingConstants.TOP); btnManageCampaignState.addActionListener(this::showCampaignStateManagement); - infoPanel.add(btnManageCampaignState); + constraints.gridy = gridY++; + infoPanel.add(btnManageCampaignState, constraints); expandedObjectivePanel = new JScrollPaneWithSpeed(objectiveStatusText); - expandedObjectivePanel.setMaximumSize(new Dimension(400, 300)); - expandedObjectivePanel.setAlignmentX(LEFT_ALIGNMENT); - infoPanel.add(expandedObjectivePanel); - - JLabel lblCurrentTrack = new JLabel("Current Sector:"); - infoPanel.add(lblCurrentTrack); - - cboCurrentTrack = new JComboBox<>(); - cboCurrentTrack.setAlignmentX(LEFT_ALIGNMENT); - cboCurrentTrack.setMaximumSize(new Dimension(320, 20)); + expandedObjectivePanel.setPreferredSize(new Dimension(400, 300)); + constraints.gridy = gridY++; + infoPanel.add(expandedObjectivePanel, constraints); + + JLabel lblCurrentTrack = new JLabel("Assigned Sectors:"); + constraints.gridy = gridY++; + infoPanel.add(lblCurrentTrack, constraints); + + listModel = new DefaultListModel<>(); + listCurrentTrack = new JList<>(listModel); + listCurrentTrack.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + listCurrentTrack.setFixedCellHeight(UIUtil.scaleForGUI(20)); repopulateTrackList(); - cboCurrentTrack.addItemListener(evt -> trackSelectionHandler()); + listCurrentTrack.addListSelectionListener(evt -> trackSelectionHandler()); - infoPanel.add(cboCurrentTrack); + JScrollPane scrollPane = new JScrollPane(listCurrentTrack); + scrollPane.setPreferredSize(new Dimension(UIUtil.scaleForGUI(400), + listCurrentTrack.getFixedCellHeight() * 10)); + constraints.gridy = gridY++; - // have a default selected - if (cboCurrentTrack.getItemCount() > 0) { - trackSelectionHandler(); - } - - infoPanel.add(infoPanelText); + infoPanel.add(scrollPane, constraints); + constraints.gridx = 2; + constraints.gridheight = 2; + infoPanel.add(infoPanelText, constraints); } /** * Worker that handles track selection. */ private void trackSelectionHandler() { - TrackDropdownItem tdi = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); + TrackDropdownItem tdi = listCurrentTrack.getSelectedValue(); if (tdi != null) { stratconPanel.selectTrack(tdi.contract.getStratconCampaignState(), tdi.track); updateCampaignState(); @@ -185,7 +194,7 @@ public MHQTabType tabType() { * with such info as current objective status, VP/SP totals, etc. */ public void updateCampaignState() { - if ((cboCurrentTrack == null) || (campaignStatusText == null)) { + if ((listCurrentTrack == null) || (campaignStatusText == null)) { return; } @@ -193,7 +202,7 @@ public void updateCampaignState() { // list of remaining objectives, percentage remaining // current VP // current support points - TrackDropdownItem currentTDI = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); + TrackDropdownItem currentTDI = listCurrentTrack.getSelectedValue(); if (currentTDI == null) { campaignStatusText.setText("No active contract selected, or contract has not started."); expandedObjectivePanel.setVisible(false); @@ -387,40 +396,38 @@ private String buildStrategicObjectiveText(StratconCampaignState campaignState) * Refreshes the list of tracks */ private void repopulateTrackList() { - TrackDropdownItem currentTDI = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); - cboCurrentTrack.removeAllItems(); + int currentTrackIndex = listCurrentTrack.getSelectedIndex(); + listModel.clear(); - // track dropdown is populated with all tracks across all active contracts for (AtBContract contract : getCampaignGui().getCampaign().getActiveAtBContracts(true)) { - if (contract.getStratconCampaignState() != null) { - for (StratconTrackState track : contract.getStratconCampaignState().getTracks()) { - TrackDropdownItem tdi = new TrackDropdownItem(contract, track); - cboCurrentTrack.addItem(tdi); - - if ((currentTDI != null) && currentTDI.equals(tdi)) { - currentTDI = tdi; - cboCurrentTrack.setSelectedItem(tdi); - } else if (currentTDI == null) { - currentTDI = tdi; - cboCurrentTrack.setSelectedItem(tdi); - } + StratconCampaignState campaignState = contract.getStratconCampaignState(); + if (campaignState != null) { + for (StratconTrackState track : campaignState.getTracks()) { + TrackDropdownItem trackItem = new TrackDropdownItem(contract, track); + listModel.addElement(trackItem); } } } - if ((cboCurrentTrack.getItemCount() > 0) && (currentTDI != null) && (currentTDI.contract != null)) { - TrackDropdownItem selectedTrack = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); + listCurrentTrack.setModel(listModel); + listCurrentTrack.setSelectedIndex(currentTrackIndex); + + if (listCurrentTrack.getSelectedValue() == null) { + listCurrentTrack.setSelectedIndex(0); + } - stratconPanel.selectTrack(selectedTrack.contract.getStratconCampaignState(), currentTDI.track); + if (listCurrentTrack.getSelectedValue() != null) { + TrackDropdownItem selectedTrack = listCurrentTrack.getSelectedValue(); + stratconPanel.selectTrack(selectedTrack.contract.getStratconCampaignState(), selectedTrack.track); stratconPanel.setVisible(true); } else { - infoPanelText.setText("No active sectors"); + infoPanelText.setText(""); stratconPanel.setVisible(false); } } private void showCampaignStateManagement(ActionEvent e) { - TrackDropdownItem selectedTrack = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); + TrackDropdownItem selectedTrack = listCurrentTrack.getSelectedValue(); if (selectedTrack == null) { return; } diff --git a/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java index 102cbff4b6..f15a1ec29a 100644 --- a/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java @@ -1082,6 +1082,11 @@ protected Optional createPopupMenu() { || !scenario.canDeployForces(forces, gui.getCampaign())) { continue; } + + if (scenario.getHasTrack()) { + continue; + } + menuItem = new JMenuItem(scenario.getName()); menuItem.setActionCommand( TOEMouseAdapter.COMMAND_DEPLOY_FORCE + scenario.getId() + '|' + forceIds); @@ -1097,10 +1102,12 @@ protected Optional createPopupMenu() { menuItem = new JMenuItem("Undeploy Force"); menuItem.setActionCommand(TOEMouseAdapter.COMMAND_UNDEPLOY_FORCE + forceIds); menuItem.addActionListener(this); + menuItem.setEnabled(!gui.getCampaign().getCampaignOptions().isUseStratCon()); popup.add(menuItem); } menuItem = new JMenuItem("Remove Force"); + menuItem.setActionCommand(TOEMouseAdapter.COMMAND_REMOVE_FORCE + forceIds); menuItem.addActionListener(this); menuItem.setEnabled( @@ -1444,6 +1451,10 @@ protected Optional createPopupMenu() { !scenario.canDeployUnits(units, gui.getCampaign())) { continue; } + + if (scenario.getHasTrack()) { + continue; + } menuItem = new JMenuItem(scenario.getName()); menuItem.setActionCommand(TOEMouseAdapter.COMMAND_DEPLOY_UNIT + scenario.getId() + '|' + unitIds); @@ -1593,11 +1604,11 @@ protected Optional createPopupMenu() { menuItem = new JMenuItem("Undeploy Unit"); menuItem.setActionCommand(TOEMouseAdapter.COMMAND_UNDEPLOY_UNIT + unitIds); menuItem.addActionListener(this); - menuItem.setEnabled(true); + menuItem.setEnabled(!gui.getCampaign().getCampaignOptions().isUseStratCon()); popup.add(menuItem); } - if (StaticChecks.areAllUnitsTransported(units)) { + if (StaticChecks.areAllUnitsTransported(units) && !StaticChecks.areAnyUnitsDeployed(units)) { menuItem = new JMenuItem("Unassign Unit from Transport Ship"); menuItem.setActionCommand(TOEMouseAdapter.COMMAND_UNASSIGN_FROM_SHIP + unitIds); menuItem.addActionListener(this); diff --git a/MekHQ/src/mekhq/gui/dialog/MissionTypeDialog.java b/MekHQ/src/mekhq/gui/dialog/MissionTypeDialog.java index 67609524ac..0f78e4669b 100644 --- a/MekHQ/src/mekhq/gui/dialog/MissionTypeDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/MissionTypeDialog.java @@ -39,6 +39,7 @@ public class MissionTypeDialog extends JDialog { private static final MMLogger logger = MMLogger.create(MissionTypeDialog.class); private boolean contract; + private boolean mission; public MissionTypeDialog(final JFrame frame, final boolean modal) { super(frame, modal); @@ -60,7 +61,7 @@ private void initComponents() { btnMission.setToolTipText(resourceMap.getString("btnMission.tooltip")); btnMission.setName("btnMission"); btnMission.addActionListener(evt -> { - contract = false; + mission = true; setVisible(false); }); getContentPane().add(btnMission); @@ -92,4 +93,8 @@ private void setUserPreferences() { public boolean isContract() { return contract; } + + public boolean isMission() { + return mission; + } } diff --git a/MekHQ/src/mekhq/gui/handler/TOETransferHandler.java b/MekHQ/src/mekhq/gui/handler/TOETransferHandler.java index b91fbcfbd3..8f20ba6143 100644 --- a/MekHQ/src/mekhq/gui/handler/TOETransferHandler.java +++ b/MekHQ/src/mekhq/gui/handler/TOETransferHandler.java @@ -18,6 +18,15 @@ */ package mekhq.gui.handler; +import megamek.logging.MMLogger; +import mekhq.MekHQ; +import mekhq.campaign.event.OrganizationChangedEvent; +import mekhq.campaign.force.Force; +import mekhq.campaign.unit.Unit; +import mekhq.gui.CampaignGUI; + +import javax.swing.*; +import javax.swing.tree.TreePath; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; @@ -26,22 +35,10 @@ import java.util.StringTokenizer; import java.util.UUID; -import javax.swing.JComponent; -import javax.swing.JTree; -import javax.swing.TransferHandler; -import javax.swing.tree.TreePath; - -import megamek.logging.MMLogger; -import mekhq.MekHQ; -import mekhq.campaign.event.OrganizationChangedEvent; -import mekhq.campaign.force.Force; -import mekhq.campaign.unit.Unit; -import mekhq.gui.CampaignGUI; - public class TOETransferHandler extends TransferHandler { private static final MMLogger logger = MMLogger.create(TOETransferHandler.class); - private CampaignGUI gui; + private final CampaignGUI gui; public TOETransferHandler(CampaignGUI gui) { super(); @@ -145,7 +142,7 @@ public boolean canImport(TransferHandler.TransferSupport support) { } @Override - public boolean importData(TransferHandler.TransferSupport support) { + public boolean importData(TransferSupport support) { if (!canImport(support)) { return false; } @@ -159,9 +156,15 @@ public boolean importData(TransferHandler.TransferSupport support) { String id = st.nextToken(); if (type.equals("UNIT")) { unit = gui.getCampaign().getUnit(UUID.fromString(id)); + if (unit == null || unit.isDeployed()) { + return false; + } } if (type.equals("FORCE")) { force = gui.getCampaign().getForce(Integer.parseInt(id)); + if (force == null || force.isDeployed()) { + return false; + } } } catch (UnsupportedFlavorException ufe) { logger.error("UnsupportedFlavor: " + ufe.getMessage()); diff --git a/MekHQ/src/mekhq/gui/model/ScenarioTableModel.java b/MekHQ/src/mekhq/gui/model/ScenarioTableModel.java index 62971a92ca..207e3490f8 100644 --- a/MekHQ/src/mekhq/gui/model/ScenarioTableModel.java +++ b/MekHQ/src/mekhq/gui/model/ScenarioTableModel.java @@ -20,13 +20,19 @@ import mekhq.MekHQ; import mekhq.campaign.Campaign; +import mekhq.campaign.mission.AtBContract; +import mekhq.campaign.mission.AtBScenario; import mekhq.campaign.mission.Scenario; import mekhq.campaign.mission.enums.ScenarioStatus; +import mekhq.campaign.stratcon.StratconCampaignState; +import mekhq.campaign.stratcon.StratconScenario; +import mekhq.campaign.stratcon.StratconTrackState; import mekhq.gui.utilities.MekHqTableCellRenderer; import javax.swing.*; import java.awt.*; import java.util.ArrayList; +import java.util.Objects; import java.util.ResourceBundle; /** @@ -34,13 +40,14 @@ */ public class ScenarioTableModel extends DataTableModel { //region Variable Declarations - private Campaign campaign; + private final Campaign campaign; public final static int COL_NAME = 0; public final static int COL_STATUS = 1; public final static int COL_DATE = 2; public final static int COL_ASSIGN = 3; - public final static int N_COL = 4; + public final static int COL_SECTOR = 4; + public final static int N_COL = 5; private final transient ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.ScenarioTableModel", MekHQ.getMHQOptions().getLocale()); @@ -60,29 +67,22 @@ public int getColumnCount() { @Override public String getColumnName(int column) { - switch (column) { - case COL_NAME: - return resources.getString("col_name.text"); - case COL_STATUS: - return resources.getString("col_status.text"); - case COL_DATE: - return resources.getString("col_date.text"); - case COL_ASSIGN: - return resources.getString("col_assign.text"); - default: - return resources.getString("col_unknown.text"); - } + return switch (column) { + case COL_NAME -> resources.getString("col_name.text"); + case COL_STATUS -> resources.getString("col_status.text"); + case COL_DATE -> resources.getString("col_date.text"); + case COL_ASSIGN -> resources.getString("col_assign.text"); + case COL_SECTOR -> resources.getString("col_sector.text"); + default -> resources.getString("col_unknown.text"); + }; } public int getColumnWidth(int c) { - switch (c) { - case COL_NAME: - return 100; - case COL_STATUS: - return 50; - default: - return 20; - } + return switch (c) { + case COL_NAME -> 100; + case COL_STATUS -> 50; + default -> 20; + }; } public int getAlignment(int col) { @@ -119,6 +119,23 @@ public Object getValueAt(int row, int col) { } } else if (col == COL_ASSIGN) { return scenario.getForces(getCampaign()).getAllUnits(true).size(); + } else if (col == COL_SECTOR) { + if (campaign.getCampaignOptions().isUseStratCon()) { + if (scenario instanceof AtBScenario) { + AtBContract contract = ((AtBScenario) scenario).getContract(campaign); + StratconCampaignState campaignState = contract.getStratconCampaignState(); + + for (StratconTrackState track : campaignState.getTracks()) { + for (StratconScenario stratconScenario : track.getScenarios().values()) { + if (Objects.equals(stratconScenario.getBackingScenario(), scenario)) { + return track.getDisplayableName(); + } + } + } + } + } + + return "-"; } else { return "?"; } diff --git a/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java b/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java index 3c7178331b..da0b596dab 100644 --- a/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java +++ b/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java @@ -34,8 +34,8 @@ */ public class CampaignManagementDialog extends JDialog { private StratconCampaignState currentCampaignState; - private StratconTab parent; - private JButton btnConvertVPToSP; + private final StratconTab parent; + private JButton btnRemoveCVP; private JButton btnConvertSPtoBonusPart; private JButton btnGMAddVP; private JButton btnGMAddSP; @@ -53,7 +53,7 @@ public CampaignManagementDialog(StratconTab parent) { public void display(StratconCampaignState campaignState, StratconTrackState currentTrack, boolean gmMode) { currentCampaignState = campaignState; - btnConvertVPToSP.setEnabled(currentCampaignState.getVictoryPoints() > 0); + btnRemoveCVP.setEnabled(currentCampaignState.getVictoryPoints() > 0); btnConvertSPtoBonusPart.setEnabled(currentCampaignState.getSupportPoints() > 0); btnGMAddVP.setEnabled(gmMode); btnGMAddSP.setEnabled(gmMode); @@ -78,10 +78,10 @@ private void initializeUI() { getContentPane().removeAll(); getContentPane().setLayout(layout); - btnConvertVPToSP = new JButton(); - btnConvertVPToSP.setText("Convert CVP to SP"); - btnConvertVPToSP.addActionListener(this::convertVPtoSPHandler); - getContentPane().add(btnConvertVPToSP); + btnRemoveCVP = new JButton(); + btnRemoveCVP.setText("Remove CVP (GM)"); + btnRemoveCVP.addActionListener(this::removeCVP); + getContentPane().add(btnRemoveCVP); btnConvertSPtoBonusPart = new JButton(); btnConvertSPtoBonusPart.setText("Convert SP to bonus part"); @@ -104,10 +104,9 @@ private void initializeUI() { pack(); } - private void convertVPtoSPHandler(ActionEvent e) { - currentCampaignState.convertVictoryToSupportPoint(); - btnConvertVPToSP.setEnabled(currentCampaignState.getVictoryPoints() > 0); - btnConvertSPtoBonusPart.setEnabled(currentCampaignState.getSupportPoints() > 0); + private void removeCVP(ActionEvent e) { + currentCampaignState.updateVictoryPoints(-1); + parent.updateCampaignState(); } @@ -120,7 +119,7 @@ private void convertSPtoBonusPartHandler(ActionEvent e) { private void gmAddVPHandler(ActionEvent e) { currentCampaignState.updateVictoryPoints(1); - btnConvertVPToSP.setEnabled(currentCampaignState.getVictoryPoints() > 0); + btnRemoveCVP.setEnabled(currentCampaignState.getVictoryPoints() > 0); parent.updateCampaignState(); } diff --git a/MekHQ/src/mekhq/gui/stratcon/ScenarioWizardUnitRenderer.java b/MekHQ/src/mekhq/gui/stratcon/ScenarioWizardUnitRenderer.java index ac695a3ede..8d64277269 100644 --- a/MekHQ/src/mekhq/gui/stratcon/ScenarioWizardUnitRenderer.java +++ b/MekHQ/src/mekhq/gui/stratcon/ScenarioWizardUnitRenderer.java @@ -13,6 +13,8 @@ */ package mekhq.gui.stratcon; +import mekhq.campaign.Campaign; +import mekhq.campaign.force.Force; import mekhq.campaign.unit.Unit; import javax.swing.*; @@ -30,8 +32,22 @@ public ScenarioWizardUnitRenderer() { @Override public Component getListCellRendererComponent(JList list, Unit value, int index, boolean isSelected, boolean cellHasFocus) { + Campaign campaign = value.getCampaign(); - setText(String.format("%s (BV: %d)", value.getName(), value.getEntity().calculateBattleValue())); + int valueForceId = value.getForceId(); + Force force = campaign.getForce(valueForceId); + + String forceName = ""; + if (force != null) { + forceName = force.getFullName(); + String originNodeName = ", " + campaign.getForce(0).getName(); + forceName = forceName.replaceAll(originNodeName, ""); + } + + setText(String.format("%s (%s/%s) - %s - Base BV: %d
%s", + value.getName(), value.getEntity().getCrew().getGunnery(), value.getEntity().getCrew().getPiloting(), + value.getCondition(), value.getEntity().calculateBattleValue(true, true), + forceName)); if (isSelected) { setBackground(list.getSelectionBackground()); @@ -44,4 +60,4 @@ public Component getListCellRendererComponent(JList list, Unit v return this; } -} \ No newline at end of file +} diff --git a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java index acfa670d95..c1f2ab8c3e 100644 --- a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java +++ b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java @@ -19,15 +19,16 @@ package mekhq.gui.stratcon; import megamek.common.Minefield; +import megamek.logging.MMLogger; import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.force.Force; import mekhq.campaign.mission.AtBDynamicScenarioFactory; import mekhq.campaign.mission.ScenarioForceTemplate; -import mekhq.campaign.personnel.SkillType; import mekhq.campaign.stratcon.StratconCampaignState; import mekhq.campaign.stratcon.StratconRulesManager; import mekhq.campaign.stratcon.StratconRulesManager.ReinforcementEligibilityType; +import mekhq.campaign.stratcon.StratconRulesManager.ReinforcementResultsType; import mekhq.campaign.stratcon.StratconScenario; import mekhq.campaign.stratcon.StratconScenario.ScenarioState; import mekhq.campaign.stratcon.StratconTrackState; @@ -41,7 +42,16 @@ import java.util.List; import java.util.*; -import static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor; +import static java.lang.Math.min; +import static mekhq.campaign.mission.AtBDynamicScenarioFactory.translateTemplateObjectives; +import static mekhq.campaign.personnel.SkillType.S_LEADER; +import static mekhq.campaign.stratcon.StratconRulesManager.BASE_LEADERSHIP_BUDGET; +import static mekhq.campaign.stratcon.StratconRulesManager.ReinforcementResultsType.DELAYED; +import static mekhq.campaign.stratcon.StratconRulesManager.ReinforcementResultsType.FAILED; +import static mekhq.campaign.stratcon.StratconRulesManager.getEligibleLeadershipUnits; +import static mekhq.campaign.stratcon.StratconRulesManager.processReinforcementDeployment; +import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; +import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor; /** * UI for managing force/unit assignments for individual StratCon scenarios. @@ -54,14 +64,16 @@ public class StratconScenarioWizard extends JDialog { private final transient ResourceBundle resourceMap = ResourceBundle.getBundle("mekhq.resources.AtBStratCon", MekHQ.getMHQOptions().getLocale()); - private Map> availableForceLists = new HashMap<>(); - private Map> availableUnitLists = new HashMap<>(); + private final Map> availableForceLists = new HashMap<>(); + private final Map> availableUnitLists = new HashMap<>(); private JList availableInfantryUnits = new JList<>(); private JList availableLeadershipUnits = new JList<>(); private JButton btnCommit; + private static final MMLogger logger = MMLogger.create(StratconScenarioWizard.class); + public StratconScenarioWizard(Campaign campaign) { this.campaign = campaign; this.setModalityType(ModalityType.APPLICATION_MODAL); @@ -96,43 +108,79 @@ private void setUI() { gbc.anchor = GridBagConstraints.WEST; setInstructions(gbc); - switch (currentScenario.getCurrentState()) { - case UNRESOLVED: - gbc.gridy++; - setAssignForcesUI(gbc, false); - break; - default: - gbc.gridy++; - setAssignForcesUI(gbc, true); - gbc.gridy++; - - List eligibleLeadershipUnits = StratconRulesManager.getEligibleLeadershipUnits(campaign, - currentScenario.getPrimaryForceIDs()); + if (Objects.requireNonNull(currentScenario.getCurrentState()) == ScenarioState.UNRESOLVED) { + gbc.gridy++; + setAssignForcesUI(gbc, false); + } else { + gbc.gridy++; + setAssignForcesUI(gbc, true); + gbc.gridy++; - eligibleLeadershipUnits.sort(Comparator.comparing(Unit::getName)); + int leadershipSkill = currentScenario.getBackingScenario().getLanceCommanderSkill( + S_LEADER, campaign); - int leadershipSkill = currentScenario.getBackingScenario().getLanceCommanderSkill(SkillType.S_LEADER, - campaign); + List eligibleLeadershipUnits = getEligibleLeadershipUnits(campaign, + currentScenario.getPrimaryForceIDs(), leadershipSkill); + eligibleLeadershipUnits.sort(Comparator.comparing(this::getForceNameReversed)); - if (!eligibleLeadershipUnits.isEmpty() && (leadershipSkill > 0)) { - setLeadershipUI(gbc, eligibleLeadershipUnits, leadershipSkill); - gbc.gridy++; - } + if (!eligibleLeadershipUnits.isEmpty() && (leadershipSkill > 0)) { + setLeadershipUI(gbc, eligibleLeadershipUnits, leadershipSkill); + gbc.gridy++; + } - if (currentScenario.getNumDefensivePoints() > 0) { - setDefensiveUI(gbc); - gbc.gridy++; - } - break; + if (currentScenario.getNumDefensivePoints() > 0) { + setDefensiveUI(gbc); + gbc.gridy++; + } } gbc.gridx = 0; gbc.gridy++; setNavigationButtons(gbc); + pack(); validate(); } + /** + * Returns a concatenated string of a unit's force hierarchy, in reversed order, + * starting from the highest parent Force going down to the given unit's direct Force. + *

+ * If the unit does not belong to any Force, an empty string is returned. + * + * @param unit The Unit whose Force hierarchy names are to be returned. + * @return A concatenated string of Force names in reversed order separated by a slash, + * or an empty string if the unit is not assigned to any Force. + */ + private String getForceNameReversed(Unit unit) { + List forceNames = new ArrayList<>(); + + Force force = campaign.getForce(unit.getForceId()); + + if (force == null) { + return ""; + } + + forceNames.add(force.getName()); + + Force parentForce = force.getParentForce(); + while (parentForce != null) { + forceNames.add(parentForce.getName()); + + parentForce = parentForce.getParentForce(); + } + + Collections.reverse(forceNames); + + StringBuilder forceNameReversed = new StringBuilder(); + + for (String forceName : forceNames) { + forceNameReversed.append(forceName); + } + + return forceNameReversed.toString(); + } + /** * Worker function that sets up the instructions for the currently selected * scenario. @@ -145,16 +193,13 @@ private void setInstructions(GridBagConstraints gbc) { if (currentTrackState.isGmRevealed() || currentTrackState.getRevealedCoords().contains(currentScenario.getCoords()) || (currentScenario.getDeploymentDate() != null)) { - labelBuilder.append(currentScenario.getInfo()); + labelBuilder.append(currentScenario.getInfo(campaign, true)); } - switch (currentScenario.getCurrentState()) { - case UNRESOLVED: - labelBuilder.append("primaryForceAssignmentInstructions.text"); - break; - default: - labelBuilder.append("reinforcementsAndSupportInstructions.text"); - break; + if (Objects.requireNonNull(currentScenario.getCurrentState()) == ScenarioState.UNRESOLVED) { + labelBuilder.append("primaryForceAssignmentInstructions.text"); + } else { + labelBuilder.append("reinforcementsAndSupportInstructions.text"); } labelBuilder.append("
"); @@ -182,9 +227,12 @@ private void setAssignForcesUI(GridBagConstraints gbc, boolean reinforcements) { localGbc.gridx = 0; localGbc.gridy = 0; - String labelText = reinforcements ? resourceMap.getString("selectReinforcementsForTemplate.Text") - : String.format(resourceMap.getString("selectForceForTemplate.Text"), - currentScenario.getRequiredPlayerLances()); + String reinforcementMessage = currentCampaignState.getSupportPoints() > 0 ? + resourceMap.getString("selectReinforcementsForTemplate.Text") : + resourceMap.getString("selectReinforcementsForTemplateNoSupportPoints.Text"); + + String labelText = reinforcements ? reinforcementMessage + : resourceMap.getString("selectForceForTemplate.Text"); JLabel assignForceListInstructions = new JLabel(labelText); forcePanel.add(assignForceListInstructions, localGbc); @@ -222,7 +270,7 @@ private void setDefensiveUI(GridBagConstraints gbc) { eligibleInfantryUnits.sort(Comparator.comparing(Unit::getName)); availableInfantryUnits = addIndividualUnitSelector(eligibleInfantryUnits, gbc, - currentScenario.getNumDefensivePoints()); + currentScenario.getNumDefensivePoints(), false); gbc.gridy++; gbc.anchor = GridBagConstraints.WEST; @@ -238,32 +286,19 @@ private void setDefensiveUI(GridBagConstraints gbc) { } private void setLeadershipUI(GridBagConstraints gbc, List eligibleUnits, int leadershipSkill) { - int maxSelectionSize = leadershipSkill - currentScenario.getLeadershipPointsUsed(); + // Leadership budget is capped at 5 levels + int leadershipBudget = min(BASE_LEADERSHIP_BUDGET * leadershipSkill, BASE_LEADERSHIP_BUDGET * 5); + int maxSelectionSize = leadershipBudget - currentScenario.getLeadershipPointsUsed(); gbc.anchor = GridBagConstraints.WEST; - if (maxSelectionSize <= 0) { - // either the full text or empty string - String leadershipUsedText = currentScenario.getLeadershipPointsUsed() > 0 - ? String.format(resourceMap.getString("lblLeaderUnitsUsed.Text"), - currentScenario.getLeadershipPointsUsed()) - : ""; - String leadershipUnavailable = resourceMap.getString("lblLeadershipReinforcementsUnavailable.Text"); - - JLabel lblLeadershipInstructions = new JLabel( - String.format(resourceMap.getString("lblFCLeadershipAvailable.Text"), - leadershipSkill, leadershipUsedText, leadershipUnavailable)); - getContentPane().add(lblLeadershipInstructions, gbc); - gbc.gridy++; - return; - } - - JLabel lblLeadershipInstructions = new JLabel(resourceMap.getString("lblLeadershipInstructions.Text")); + JLabel lblLeadershipInstructions = new JLabel(String.format(resourceMap.getString("lblLeadershipInstructions.Text"), + maxSelectionSize)); getContentPane().add(lblLeadershipInstructions, gbc); gbc.gridy++; - availableLeadershipUnits = addIndividualUnitSelector(eligibleUnits, gbc, maxSelectionSize); + availableLeadershipUnits = addIndividualUnitSelector(eligibleUnits, gbc, maxSelectionSize, true); } /** @@ -273,13 +308,10 @@ private JList addAvailableForceList(JPanel parent, GridBagConstraints gbc ScenarioForceTemplate forceTemplate) { JScrollPane forceListContainer = new JScrollPaneWithSpeed(); - ScenarioWizardLanceModel lanceModel; - - lanceModel = new ScenarioWizardLanceModel(campaign, - StratconRulesManager.getAvailableForceIDs(forceTemplate.getAllowedUnitType(), - campaign, currentTrackState, - (forceTemplate.getArrivalTurn() == ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS), - currentScenario, currentCampaignState)); + ScenarioWizardLanceModel lanceModel = new ScenarioWizardLanceModel(campaign, + StratconRulesManager.getAvailableForceIDs(forceTemplate.getAllowedUnitType(), campaign, currentTrackState, + (forceTemplate.getArrivalTurn() == ScenarioForceTemplate.ARRIVAL_TURN_AS_REINFORCEMENTS), + currentScenario, currentCampaignState)); JList availableForceList = new JList<>(); availableForceList.setModel(lanceModel); @@ -301,7 +333,7 @@ private JList addAvailableForceList(JPanel parent, GridBagConstraints gbc * @param maxSelectionSize Maximum number of units that can be selected */ private JList addIndividualUnitSelector(List units, GridBagConstraints gridBagConstraints, - int maxSelectionSize) { + int maxSelectionSize, boolean usesBV) { JPanel unitPanel = new JPanel(); unitPanel.setLayout(new GridBagLayout()); GridBagConstraints localGridBagConstraints = new GridBagConstraints(); @@ -331,7 +363,7 @@ private JList addIndividualUnitSelector(List units, GridBagConstrain availableUnits.setModel(availableModel); availableUnits.setCellRenderer(new ScenarioWizardUnitRenderer()); availableUnits.addListSelectionListener( - e -> availableUnitSelectorChanged(e, unitSelectionLabel, unitStatusLabel, maxSelectionSize)); + e -> availableUnitSelectorChanged(e, unitSelectionLabel, unitStatusLabel, maxSelectionSize, usesBV)); JScrollPane infantryContainer = new JScrollPaneWithSpeed(); infantryContainer.setViewportView(availableUnits); @@ -405,21 +437,13 @@ private String buildForceCost(int forceID) { costBuilder.append('('); switch (StratconRulesManager.getReinforcementType(forceID, currentTrackState, campaign, currentCampaignState)) { - case SupportPoint: - costBuilder.append(resourceMap.getString("supportPoint.text")); - - if (currentCampaignState.getSupportPoints() <= 0) { - costBuilder.append(", "); - - costBuilder.append(messageSurroundedBySpanWithColor( - MekHQ.getMHQOptions().getFontColorNegativeHexColor(), - resourceMap.getString("reinforcementRoll.Text"))); - } + case REGULAR: + costBuilder.append(resourceMap.getString("regular.text")); break; - case ChainedScenario: + case CHAINED_SCENARIO: costBuilder.append(resourceMap.getString("fromChainedScenario.text")); break; - case FightLance: + case FIGHT_LANCE: costBuilder.append(resourceMap.getString("lanceInFightRole.text")); break; default: @@ -459,19 +483,46 @@ private void btnCommitClicked(ActionEvent e) { for (Force force : availableForceLists.get(templateID).getSelectedValuesList()) { // if we are assigning reinforcements, pay the price if appropriate if (currentScenario.getCurrentState() == ScenarioState.PRIMARY_FORCES_COMMITTED) { + if (currentCampaignState.getSupportPoints() <= 0) { + campaign.addReport(String.format(resourceMap.getString("reinforcementsNoSupportPoints.text"), + currentScenario.getName(), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), + CLOSING_SPAN_TAG)); + continue; + } + ReinforcementEligibilityType reinforcementType = StratconRulesManager.getReinforcementType( force.getId(), currentTrackState, campaign, currentCampaignState); // if we failed to deploy as reinforcements, move on to the next force - if (!StratconRulesManager.processReinforcementDeployment(reinforcementType, currentCampaignState, - currentScenario, campaign)) { + ReinforcementResultsType reinforcementResults = processReinforcementDeployment( + force, reinforcementType, currentCampaignState, currentScenario, campaign); + + if (reinforcementResults.ordinal() >= FAILED.ordinal()) { currentScenario.addFailedReinforcements(force.getId()); continue; } - } - currentScenario.addForce(force, templateID, campaign); + currentScenario.addForce(force, templateID, campaign); + + if (reinforcementResults == DELAYED) { + List delayedReinforcements = currentScenario.getBackingScenario().getFriendlyDelayedReinforcements(); + + for (UUID unitId : force.getAllUnits(true)) { + try { + delayedReinforcements.add(unitId); + } catch (Exception ex) { + logger.error(ex.getMessage(), ex); + } + } + } + } else { + // In the event the player has selected multiple forces to act as the primary + // force, only commit the first force + currentScenario.addForce(force, templateID, campaign); + break; + } } } @@ -496,10 +547,8 @@ private void btnCommitClicked(ActionEvent e) { } // scenarios that haven't had primary forces committed yet get those committed - // now - // and the scenario gets published to the campaign and may be played immediately - // from the briefing room - // that being said, give the player a chance to commit reinforcements too + // now and the scenario gets published to the campaign and may be played immediately + // from the briefing room that being said, give the player a chance to commit reinforcements too if (currentScenario.getCurrentState() == ScenarioState.UNRESOLVED) { // if we've already generated forces and applied modifiers, no need to do it // twice @@ -518,6 +567,8 @@ private void btnCommitClicked(ActionEvent e) { setVisible(false); } + translateTemplateObjectives(currentScenario.getBackingScenario(), campaign); + this.getParent().repaint(); } @@ -535,16 +586,13 @@ private void availableForceSelectorChanged(ListSelectionEvent e, JLabel forceSta JList sourceList = (JList) e.getSource(); StringBuilder statusBuilder = new StringBuilder(); - StringBuilder costBuilder = new StringBuilder(); statusBuilder.append(""); - costBuilder.append(""); for (Force force : sourceList.getSelectedValuesList()) { statusBuilder.append(buildForceStatus(force, reinforcements)); } statusBuilder.append(""); - costBuilder.append(""); forceStatusLabel.setText(statusBuilder.toString()); @@ -556,27 +604,40 @@ private void availableForceSelectorChanged(ListSelectionEvent e, JLabel forceSta * Updates the "# units selected" label and the unit status label. * Also checks maximum selection size and disables commit button (TBD). * - * @param e + * @param event The triggering event * @param selectionCountLabel Which label to update with how many items are * selected * @param unitStatusLabel Which label to update with detailed unit info * @param maxSelectionSize How many items can be selected at most + * @param usesBV Whether we are tracking the BV of selected items, {@code true}, + * or simply the count of selected items, {@code false} */ - private void availableUnitSelectorChanged(ListSelectionEvent e, JLabel selectionCountLabel, JLabel unitStatusLabel, - int maxSelectionSize) { - if (!(e.getSource() instanceof JList)) { + private void availableUnitSelectorChanged(ListSelectionEvent event, JLabel selectionCountLabel, + JLabel unitStatusLabel, int maxSelectionSize, boolean usesBV) { + if (!(event.getSource() instanceof JList)) { return; } - JList changedList = (JList) e.getSource(); - selectionCountLabel.setText(String.format("%d selected", changedList.getSelectedIndices().length)); + JList changedList = (JList) event.getSource(); + + int selectedItems; + if (usesBV) { + selectedItems = 0; + for (Unit unit : changedList.getSelectedValuesList()) { + selectedItems += unit.getEntity().calculateBattleValue(true, true); + selectionCountLabel.setText(String.format("%d selected (ignores crew skill)", selectedItems)); + } + } else { + selectedItems = changedList.getSelectedIndices().length; + selectionCountLabel.setText(String.format("%d selected", selectedItems)); + } + // if we've selected too many units here, change the label and disable the // commit button - if (changedList.getSelectedIndices().length > maxSelectionSize) { - selectionCountLabel.setForeground(Color.RED); + if (selectedItems > maxSelectionSize) { + selectionCountLabel.setForeground(MekHQ.getMHQOptions().getFontColorNegative()); btnCommit.setEnabled(false); } else { - selectionCountLabel.setForeground(Color.BLACK); btnCommit.setEnabled(true); } @@ -599,8 +660,8 @@ private void availableUnitSelectorChanged(ListSelectionEvent e, JLabel selection StringBuilder sb = new StringBuilder(); sb.append(""); - for (Unit u : changedList.getSelectedValuesList()) { - sb.append(buildUnitStatus(u)); + for (Unit unit : changedList.getSelectedValuesList()) { + sb.append(buildUnitStatus(unit)); } sb.append(""); diff --git a/MekHQ/src/mekhq/gui/view/ContractSummaryPanel.java b/MekHQ/src/mekhq/gui/view/ContractSummaryPanel.java index cae55caeba..de3b6673bf 100644 --- a/MekHQ/src/mekhq/gui/view/ContractSummaryPanel.java +++ b/MekHQ/src/mekhq/gui/view/ContractSummaryPanel.java @@ -49,13 +49,13 @@ */ public class ContractSummaryPanel extends JPanel { //region Variable Declarations - private Campaign campaign; - private Contract contract; - private boolean allowRerolls; + private final Campaign campaign; + private final Contract contract; + private final boolean allowRerolls; private int cmdRerolls; private int logRerolls; private int tranRerolls; - private ContractMarketMethod method; + private final ContractMarketMethod method; private JPanel mainPanel; @@ -65,7 +65,7 @@ public class ContractSummaryPanel extends JPanel { private JLabel txtStraightSupport; private JLabel txtBattleLossComp; - private ResourceBundle resourceMap = ResourceBundle.getBundle("mekhq.resources.ContractMarketDialog", + private final ResourceBundle resourceMap = ResourceBundle.getBundle("mekhq.resources.ContractMarketDialog", MekHQ.getMHQOptions().getLocale()); private ContractPaymentBreakdown contractPaymentBreakdown; @@ -516,8 +516,7 @@ public void mouseClicked(MouseEvent e) { gridBagConstraintsLabels.gridy = ++y; mainPanel.add(lblRequiredLances, gridBagConstraintsLabels); - JLabel txtRequiredLances = new JLabel(((AtBContract) contract).getRequiredLances() - + " Lance(s)"); + JLabel txtRequiredLances = new JLabel(String.valueOf(((AtBContract) contract).getRequiredLances())); txtRequiredLances.setName("txtRequiredLances"); gridBagConstraintsText.gridy = y; mainPanel.add(txtRequiredLances, gridBagConstraintsText); diff --git a/MekHQ/src/mekhq/gui/view/LanceAssignmentView.java b/MekHQ/src/mekhq/gui/view/LanceAssignmentView.java index c1b10728e2..ed6a939a2c 100644 --- a/MekHQ/src/mekhq/gui/view/LanceAssignmentView.java +++ b/MekHQ/src/mekhq/gui/view/LanceAssignmentView.java @@ -140,7 +140,10 @@ public Component getTableCellRendererComponent(JTable table, Object value, switch (column) { case LanceAssignmentTableModel.COL_FORCE: if (null != value) { - setText((((Force) value)).getFullName()); + String forceName = (((Force) value)).getFullName(); + String originNodeName = ", " + campaign.getForce(0).getName(); + forceName = forceName.replaceAll(originNodeName, ""); + setText(forceName); } break; case LanceAssignmentTableModel.COL_CONTRACT: @@ -203,7 +206,7 @@ public boolean include(Entry