From 5fe0bf653389db9190d787842f92d70de1ee5bd0 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 2 Dec 2024 14:30:36 -0600 Subject: [PATCH 1/5] Add reinforcement interception scenarios Implemented new scenarios for reinforcement interception. Enhanced the campaign to select senior command administrators efficiently and handle delayed reinforcements deployment. --- ...-Atmosphere Reinforcements Intercepted.xml | 143 ++++++ .../Reinforcements Intercepted.xml | 143 ++++++ .../Space Reinforcements Intercepted.xml | 147 ++++++ .../mekhq/resources/AtBStratCon.properties | 53 ++- MekHQ/src/mekhq/campaign/Campaign.java | 36 +- .../campaign/mission/AtBDynamicScenario.java | 16 + .../mission/AtBDynamicScenarioFactory.java | 58 ++- .../stratcon/StratconRulesManager.java | 420 ++++++++++++++---- .../gui/stratcon/StratconScenarioWizard.java | 145 +++--- 9 files changed, 989 insertions(+), 172 deletions(-) create mode 100644 MekHQ/data/scenariotemplates/Low-Atmosphere Reinforcements Intercepted.xml create mode 100644 MekHQ/data/scenariotemplates/Reinforcements Intercepted.xml create mode 100644 MekHQ/data/scenariotemplates/Space Reinforcements Intercepted.xml 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..0b38a70819 100644 --- a/MekHQ/resources/mekhq/resources/AtBStratCon.properties +++ b/MekHQ/resources/mekhq/resources/AtBStratCon.properties @@ -1,17 +1,54 @@ -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. +lblDefensivePostureInstructions.Text=This lance is on a defensive deployment.\ +
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 -selectForceForTemplate.Text=Select at least %d forces from the list below -selectReinforcementsForTemplate.Text=Select reinforcements from the list below +selectForceForTemplate.Text=Select 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 Logistics Failure%s. Reinforcement attempt\ + \ fails and Support Point is lost. +reinforcementsSuccess.text= %sReinforcement Success%s +reinforcementsSuccessNoInterception.text= %sReinforcement Success%s. The enemy failed to\ + \ intercept your reinforcements. +reinforcementsInterceptionAttempt.text= Enemy forces attempted to %sIntercept%s your reinforcements. +reinforcementsErrorNoCommander.text= %sError%s. There is no commander assigned to this force.\ + \ Reinforcement attempt fails and Support Point is lost. +reinforcementsErrorUnableToFetchCommander.text= %sError%s. We were unable to fetch the\ + \ commander using the commander ID logged for this force. You should report this. 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 are unable to evade the 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 interception.\ + \ 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/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 89310fffbc..26759cf8ea 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -143,6 +143,7 @@ import java.util.stream.Collectors; 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; @@ -271,7 +272,7 @@ public class Campaign implements ITechManager { private final CampaignSummary campaignSummary; private final Quartermaster quartermaster; private StoryArc storyArc; - private FameAndInfamyController fameAndInfamy; + private final FameAndInfamyController fameAndInfamy; private BehaviorSettings autoResolveBehaviorSettings; private List automatedMothballUnits; @@ -2668,6 +2669,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. @@ -3864,10 +3892,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); @@ -3896,7 +3924,7 @@ 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) { diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java index f651f572f7..996ac96d3b 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java @@ -67,6 +67,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 @@ -92,6 +93,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 +314,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 +492,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 +536,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")) { diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java index 677bb23b50..7f287d1452 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java @@ -3418,8 +3418,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); @@ -3544,10 +3565,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; @@ -3568,7 +3620,7 @@ public static void setDeploymentTurnsForReinforcements(List entityList, // 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); + int actualArrivalTurn = Math.max(0, (arrivalScale / minimumSpeed) - turnModifier); for (Entity entity : entityList) { entity.setDeployRound(actualArrivalTurn); diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java index 47a4e44edd..1819cfe749 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -19,9 +19,7 @@ package mekhq.campaign.stratcon; import megamek.codeUtilities.ObjectUtility; -import megamek.common.Compute; -import megamek.common.Minefield; -import megamek.common.UnitType; +import megamek.common.*; import megamek.common.annotations.Nullable; import megamek.common.event.Subscribe; import megamek.logging.MMLogger; @@ -39,8 +37,8 @@ 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.personnel.Person; +import mekhq.campaign.personnel.Skill; import mekhq.campaign.personnel.SkillType; import mekhq.campaign.personnel.turnoverAndRetention.Fatigue; import mekhq.campaign.stratcon.StratconContractDefinition.StrategicObjectiveType; @@ -52,12 +50,26 @@ import java.util.*; import java.util.stream.Collectors; +import static java.lang.Math.max; +import static megamek.common.Compute.d6; import static mekhq.campaign.force.Force.FORCE_NONE; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Allied; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Opposing; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Player; 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.utilities.ReportingUtilities.CLOSING_SPAN_TAG; +import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor; /** * This class contains "rules" logic for the AtB-Stratcon state @@ -74,22 +86,47 @@ 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 + } + + /** + * What were the results of the reinforcement roll? + */ + 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 } /** @@ -300,7 +337,7 @@ public static void generateScenariosForTrack(Campaign campaign, AtBContract cont randomForceID = availableForceIDs.get(randomForceIndex); } - scenario = setupScenario(scenarioCoords, randomForceID, campaign, contract, track, template); + scenario = setupScenario(scenarioCoords, randomForceID, campaign, contract, track, template, false); } if (scenario == null) { @@ -313,6 +350,20 @@ public static void generateScenariosForTrack(Campaign campaign, AtBContract cont // We return the scenario in case we want to make specific changes. return scenario; } + public static @Nullable void generateReinforcementInterceptionScenario( + Campaign campaign, AtBContract contract, + StratconTrackState track, StratconCoords scenarioCoords, + ScenarioTemplate template, Force interceptedForce) { + 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); + } /** * Adds a {@link StratconScenario} to the specified contract. This scenario is cloaked so will @@ -605,7 +656,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) { @@ -688,7 +739,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); } /** @@ -707,14 +758,15 @@ 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; template = StratconScenarioFactory.getFacilityScenario(alliedFacility); @@ -887,78 +939,253 @@ private static void increaseFatigue(int forceID, Campaign campaign) { /** * Worker function that processes the effects of deploying a reinforcement force to a scenario * + * @param id * @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(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(); + + if (contract.getCommandRights().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 (contract.getCommandRights().isLiaison()) { + liaisonModifier = 2; + } + + reinforcementTargetNumber.addModifier(liaisonModifier, "Liaison 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())); + + 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; + } + + int interceptionOdds = calculateScenarioOdds(track, campaignState.getContract(), true); + int interceptionRoll = Compute.randomInt(100); + + // Was the reinforcement attempt successful, or did the enemy choose not to intercept? + 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; + } + + if (interceptionRoll >= interceptionOdds) { + reportStatus.append(' '); + reportStatus.append(String.format(resources.getString("reinforcementsSuccessNoInterception.text"), + spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorPositiveHexColor()), + CLOSING_SPAN_TAG)); + campaign.addReport(reportStatus.toString()); + return SUCCESS; + } + + 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()); + + StratconCoords scenarioCoords = scenario.getCoords(); + 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, scenarioCoords, scenarioTemplate, force); + + return INTERCEPTED; + } + + roll = d6(2); + int targetNumber = 12 - 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()); + + 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()); + + StratconCoords scenarioCoords = scenario.getCoords(); + 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, scenarioCoords, scenarioTemplate, force); + + return INTERCEPTED; } /** @@ -1461,7 +1688,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() @@ -1667,26 +1894,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.getStrategicFormations().containsKey(forceID) - && (campaign.getStrategicFormations().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.getStrategicFormations().containsKey(forceID)) { + Hashtable strategicFormations = campaign.getStrategicFormations(); + StrategicFormation formation = strategicFormations.get(forceID); + + if (formation == null) { + return ReinforcementEligibilityType.NONE; + } - // otherwise, the force requires support points to deploy - if (campaignState.getSupportPoints() > 0) { - return ReinforcementEligibilityType.SupportPoint; + 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; } /** @@ -1902,7 +2134,7 @@ public static void switchFacilityOwner(StratconFacility facility) { // captured // fall back to the default of just switching the owner if (facility.getOwner() == ForceAlignment.Allied) { - facility.setOwner(ForceAlignment.Opposing); + facility.setOwner(Opposing); } else { facility.setOwner(ForceAlignment.Allied); } @@ -1974,7 +2206,7 @@ public static boolean processIgnoredScenario(StratconScenario scenario, Stratcon // 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); + localFacility.setOwner(Opposing); } return true; @@ -1995,7 +2227,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)); diff --git a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java index acfa670d95..3a34768c78 100644 --- a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java +++ b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java @@ -19,6 +19,7 @@ package mekhq.gui.stratcon; import megamek.common.Minefield; +import megamek.logging.MMLogger; import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.force.Force; @@ -28,6 +29,7 @@ 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 +43,11 @@ import java.util.List; import java.util.*; -import static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor; +import static mekhq.campaign.stratcon.StratconRulesManager.ReinforcementResultsType.DELAYED; +import static mekhq.campaign.stratcon.StratconRulesManager.ReinforcementResultsType.FAILED; +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 +60,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,39 +104,36 @@ 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++; + if (Objects.requireNonNull(currentScenario.getCurrentState()) == ScenarioState.UNRESOLVED) { + gbc.gridy++; + setAssignForcesUI(gbc, false); + } else { + gbc.gridy++; + setAssignForcesUI(gbc, true); + gbc.gridy++; - List eligibleLeadershipUnits = StratconRulesManager.getEligibleLeadershipUnits(campaign, - currentScenario.getPrimaryForceIDs()); + List eligibleLeadershipUnits = StratconRulesManager.getEligibleLeadershipUnits(campaign, + currentScenario.getPrimaryForceIDs()); - eligibleLeadershipUnits.sort(Comparator.comparing(Unit::getName)); + eligibleLeadershipUnits.sort(Comparator.comparing(Unit::getName)); - int leadershipSkill = currentScenario.getBackingScenario().getLanceCommanderSkill(SkillType.S_LEADER, - campaign); + int leadershipSkill = currentScenario.getBackingScenario().getLanceCommanderSkill(SkillType.S_LEADER, campaign); - 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(); } @@ -148,13 +153,10 @@ private void setInstructions(GridBagConstraints gbc) { labelBuilder.append(currentScenario.getInfo()); } - 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 +184,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); @@ -273,13 +278,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); @@ -405,21 +407,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 +453,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 +517,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 From 490c4c6ed367b89b2247cf2515869b75abaa2aec Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 2 Dec 2024 14:37:41 -0600 Subject: [PATCH 2/5] Post-Merge correction --- .../stratcon/StratconRulesManager.java | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java index 3b3e314540..94eea49425 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -19,9 +19,7 @@ package mekhq.campaign.stratcon; import megamek.codeUtilities.ObjectUtility; -import megamek.common.Compute; -import megamek.common.Minefield; -import megamek.common.UnitType; +import megamek.common.*; import megamek.common.annotations.Nullable; import megamek.common.event.Subscribe; import megamek.logging.MMLogger; @@ -39,9 +37,9 @@ 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.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; @@ -52,12 +50,26 @@ import java.util.*; import java.util.stream.Collectors; +import static java.lang.Math.max; +import static java.lang.Math.round; +import static megamek.common.Compute.d6; import static mekhq.campaign.force.Force.FORCE_NONE; +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.utilities.ReportingUtilities.CLOSING_SPAN_TAG; +import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor; /** * This class contains "rules" logic for the AtB-Stratcon state @@ -709,7 +721,7 @@ 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); @@ -771,7 +783,7 @@ public static void deployForceToCoords(StratconCoords coords, int forceID, Campa 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); @@ -816,7 +828,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(); @@ -1013,7 +1025,7 @@ public static ReinforcementResultsType processReinforcementDeployment( int facilityModifier = 0; if (track != null) { for (StratconFacility facility : track.getFacilities().values()) { - if (facility.getOwner().equals(Player) || facility.getOwner().equals(Allied)) { + if (facility.getOwner().equals(ForceAlignment.Player) || facility.getOwner().equals(Allied)) { facilityModifier++; } else { facilityModifier--; @@ -1245,7 +1257,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. @@ -1900,7 +1912,7 @@ public static ReinforcementEligibilityType getReinforcementType(int forceID, Str // 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.getStrategicFormations(); + Hashtable strategicFormations = campaign.getStrategicFormationsTable(); StrategicFormation formation = strategicFormations.get(forceID); if (formation == null) { @@ -1939,7 +1951,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; } @@ -2131,10 +2143,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) { + if (facility.getOwner() == Allied) { facility.setOwner(Opposing); } else { - facility.setOwner(ForceAlignment.Allied); + facility.setOwner(Allied); } } @@ -2203,7 +2215,7 @@ 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) { + if (localFacility.getOwner() == Allied) { localFacility.setOwner(Opposing); } From b8e729a06b22a3bb31dfe337dc90c75f5a04b8e1 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 2 Dec 2024 15:18:30 -0600 Subject: [PATCH 3/5] Refactored reinforcement scenario logic. Removed redundant reinforcement success message and integrated interception logic for clarity. Adjusted modifiers for facility, skill, and liaison/command rights to ensure correct calculation. Enhanced the generateReinforcementInterceptionScenario method with additional details, moved scenarioCoords calculation inside, and ensured fatigue is accounted for when necessary. --- .../mekhq/resources/AtBStratCon.properties | 1 - .../stratcon/StratconRulesManager.java | 60 ++++++++++++------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/AtBStratCon.properties b/MekHQ/resources/mekhq/resources/AtBStratCon.properties index 0b38a70819..d64cd22f5c 100644 --- a/MekHQ/resources/mekhq/resources/AtBStratCon.properties +++ b/MekHQ/resources/mekhq/resources/AtBStratCon.properties @@ -32,7 +32,6 @@ reinforcementsNoAdminSkill.text=Attempting to reinforce scenario %s, %sERROR< reinforcementsAttempt.text=Attempting to reinforce scenario %s, roll %s%s vs. %s: reinforcementsCriticalFailure.text=%sCritical Logistics Failure%s. Reinforcement attempt\ \ fails and Support Point is lost. -reinforcementsSuccess.text= %sReinforcement Success%s reinforcementsSuccessNoInterception.text= %sReinforcement Success%s. The enemy failed to\ \ intercept your reinforcements. reinforcementsInterceptionAttempt.text= Enemy forces attempted to %sIntercept%s your reinforcements. diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java index 94eea49425..36c328759a 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -38,6 +38,7 @@ import mekhq.campaign.mission.ScenarioMapParameters.MapLocation; import mekhq.campaign.mission.atb.AtBScenarioModifier; import mekhq.campaign.mission.enums.AtBMoraleLevel; +import mekhq.campaign.mission.enums.ContractCommandRights; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.Skill; import mekhq.campaign.personnel.turnoverAndRetention.Fatigue; @@ -365,10 +366,24 @@ 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, StratconCoords scenarioCoords, - ScenarioTemplate template, Force interceptedForce) { + StratconTrackState track, ScenarioTemplate template, Force interceptedForce) { + StratconCoords scenarioCoords = getUnoccupiedCoords(track, false); + StratconScenario scenario = setupScenario(scenarioCoords, interceptedForce.getId(), campaign, contract, track, template, true); @@ -378,6 +393,7 @@ public static void generateScenariosForTrack(Campaign campaign, AtBContract cont } finalizeBackingScenario(campaign, contract, track, true, scenario); + scenario.setDeploymentDate(campaign.getLocalDate()); } /** @@ -1026,9 +1042,9 @@ public static ReinforcementResultsType processReinforcementDeployment( if (track != null) { for (StratconFacility facility : track.getFacilities().values()) { if (facility.getOwner().equals(ForceAlignment.Player) || facility.getOwner().equals(Allied)) { - facilityModifier++; - } else { facilityModifier--; + } else { + facilityModifier++; } } } @@ -1036,25 +1052,28 @@ public static ReinforcementResultsType processReinforcementDeployment( reinforcementTargetNumber.addModifier(facilityModifier, "Facilities"); // Skill Modifier - int skillModifier = contract.getAllySkill().getAdjustedValue(); + int skillModifier = -contract.getAllySkill().getAdjustedValue(); - if (contract.getCommandRights().isIndependent()) { + ContractCommandRights commandRights = contract.getCommandRights(); + if (commandRights.isIndependent()) { if (campaign.getCampaignOptions().getUnitRatingMethod().isCampaignOperations()) { skillModifier = campaign.getReputation().getAverageSkillLevel().getAdjustedValue(); } } - skillModifier -= contract.getEnemySkill().getAdjustedValue(); + skillModifier += contract.getEnemySkill().getAdjustedValue(); reinforcementTargetNumber.addModifier(skillModifier, "Skill"); // Liaison Modifier int liaisonModifier = 0; - if (contract.getCommandRights().isLiaison()) { - liaisonModifier = 2; + if (commandRights.isLiaison()) { + liaisonModifier -= 1; + } else if (commandRights.isHouse() || commandRights.isIntegrated()) { + liaisonModifier -= 2; } - reinforcementTargetNumber.addModifier(liaisonModifier, "Liaison Command Rights"); + reinforcementTargetNumber.addModifier(liaisonModifier, "Command Rights"); // Make the roll int roll = d6(2); @@ -1091,17 +1110,8 @@ public static ReinforcementResultsType processReinforcementDeployment( int interceptionOdds = calculateScenarioOdds(track, campaignState.getContract(), true); int interceptionRoll = Compute.randomInt(100); - // Was the reinforcement attempt successful, or did the enemy choose not to intercept? - 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; - } - - if (interceptionRoll >= interceptionOdds) { + // Was the reinforcement attempt successful? + if (roll >= reinforcementTargetNumber.getValue() && interceptionRoll >= interceptionOdds) { reportStatus.append(' '); reportStatus.append(String.format(resources.getString("reinforcementsSuccessNoInterception.text"), spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorPositiveHexColor()), @@ -1161,7 +1171,7 @@ public static ReinforcementResultsType processReinforcementDeployment( case LowAtmosphere -> ScenarioTemplate.Deserialize(String.format(templateString, "Low-Atmosphere ")); }; - generateReinforcementInterceptionScenario(campaign, contract, track, scenarioCoords, scenarioTemplate, force); + generateReinforcementInterceptionScenario(campaign, contract, track, scenarioTemplate, force); return INTERCEPTED; } @@ -1177,6 +1187,10 @@ public static ReinforcementResultsType processReinforcementDeployment( campaign.addReport(reportStatus.toString()); + if (campaign.getCampaignOptions().isUseFatigue()) { + increaseFatigue(force.getId(), campaign); + } + return DELAYED; } @@ -1197,7 +1211,7 @@ public static ReinforcementResultsType processReinforcementDeployment( case LowAtmosphere -> ScenarioTemplate.Deserialize(String.format(templateString, "Low-Atmosphere ")); }; - generateReinforcementInterceptionScenario(campaign, contract, track, scenarioCoords, scenarioTemplate, force); + generateReinforcementInterceptionScenario(campaign, contract, track, scenarioTemplate, force); return INTERCEPTED; } From 10fb8ec7d7b33ff434f0459c24758a7b7bcd10a8 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 2 Dec 2024 15:27:17 -0600 Subject: [PATCH 4/5] Initialize support points in Stratcon contracts Added a call to negotiate additional support points in the StratconContractInitializer to ensure initial support points are determined. This change ensures proper initialization of support resources at the start of a contract. --- MekHQ/src/mekhq/campaign/Campaign.java | 6 +++--- .../campaign/stratcon/StratconContractInitializer.java | 3 +++ MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java | 4 +--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 02206d0d24..c9b0117276 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -3932,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<>(); @@ -4565,7 +4565,7 @@ private void processReputationChanges() { public int getInitiativeBonus() { return initiativeBonus; } - + public void setInitiativeBonus(int bonus) { initiativeBonus = bonus; } @@ -4573,7 +4573,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/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 36c328759a..c7fc62d353 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -373,7 +373,7 @@ public static void generateScenariosForTrack(Campaign campaign, AtBContract cont * scenario is set as the current date. * * @param campaign the current campaign - * @param contract the {@link AtBContract for which the scenario is created + * @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 @@ -1160,7 +1160,6 @@ public static ReinforcementResultsType processReinforcementDeployment( CLOSING_SPAN_TAG)); campaign.addReport(reportStatus.toString()); - StratconCoords scenarioCoords = scenario.getCoords(); MapLocation mapLocation = scenario.getScenarioTemplate().mapParameters.getMapLocation(); String templateString = "data/scenariotemplates/%sReinforcements Intercepted.xml"; @@ -1200,7 +1199,6 @@ public static ReinforcementResultsType processReinforcementDeployment( CLOSING_SPAN_TAG, roll, targetNumber)); campaign.addReport(reportStatus.toString()); - StratconCoords scenarioCoords = scenario.getCoords(); MapLocation mapLocation = scenario.getScenarioTemplate().mapParameters.getMapLocation(); String templateString = "data/scenariotemplates/%sReinforcements Intercepted.xml"; From 156c5f962a9341f1f537406af10ae45a311371a9 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 2 Dec 2024 18:41:27 -0600 Subject: [PATCH 5/5] Adjust reinforcement dynamics and messaging. Reduced the reinforcement arrival scale from 30 to 15 to quicken reinforcements. Revised messaging for reinforcement events, including differentiating success, command failure, and enemy interception scenarios. Corrected skill modifier calculation in reinforcement logic for improved accuracy. --- .../mekhq/resources/AtBStratCon.properties | 34 ++++++++++-------- .../mission/AtBDynamicScenarioFactory.java | 18 +++++----- .../stratcon/StratconRulesManager.java | 36 +++++++++++++++---- 3 files changed, 57 insertions(+), 31 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/AtBStratCon.properties b/MekHQ/resources/mekhq/resources/AtBStratCon.properties index d64cd22f5c..f3493697c7 100644 --- a/MekHQ/resources/mekhq/resources/AtBStratCon.properties +++ b/MekHQ/resources/mekhq/resources/AtBStratCon.properties @@ -30,24 +30,28 @@ reinforcementsNoAdminSkill.text=Attempting to reinforce scenario %s, %sERROR< \ 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 Logistics Failure%s. Reinforcement attempt\ +reinforcementsCriticalFailure.text=%sCritical Command Failure%s. Reinforcement attempt\ \ fails and Support Point is lost. -reinforcementsSuccessNoInterception.text= %sReinforcement Success%s. The enemy failed to\ - \ intercept your reinforcements. -reinforcementsInterceptionAttempt.text= Enemy forces attempted to %sIntercept%s your reinforcements. -reinforcementsErrorNoCommander.text= %sError%s. There is no commander assigned to this force.\ - \ 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. 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 are unable to evade the enemy\ + \ 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 interception.\ - \ Reinforcements were delayed, but successful. -reinforcementEvasionUnsuccessful.text= %sEvasion Unsuccessful%s (%s vs. %s). The commander\ + \ 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/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java index 53502ea296..638a22e520 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java @@ -102,7 +102,7 @@ public class AtBDynamicScenarioFactory { 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", @@ -3696,17 +3696,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, (arrivalScale / 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); } } diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java index c7fc62d353..b7d428c7a0 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -1057,7 +1057,7 @@ public static ReinforcementResultsType processReinforcementDeployment( ContractCommandRights commandRights = contract.getCommandRights(); if (commandRights.isIndependent()) { if (campaign.getCampaignOptions().getUnitRatingMethod().isCampaignOperations()) { - skillModifier = campaign.getReputation().getAverageSkillLevel().getAdjustedValue(); + skillModifier = -campaign.getReputation().getAverageSkillLevel().getAdjustedValue(); } } @@ -1098,6 +1098,7 @@ public static ReinforcementResultsType processReinforcementDeployment( 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"), @@ -1107,19 +1108,41 @@ public static ReinforcementResultsType processReinforcementDeployment( 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 = Compute.randomInt(100); - // Was the reinforcement attempt successful? - if (roll >= reinforcementTargetNumber.getValue() && interceptionRoll >= interceptionOdds) { + // 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("reinforcementsSuccessNoInterception.text"), + 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()), @@ -1176,7 +1199,8 @@ public static ReinforcementResultsType processReinforcementDeployment( } roll = d6(2); - int targetNumber = 12 - tactics.getFinalSkillValue(); + int baseTargetNumber = 9; + int targetNumber = baseTargetNumber - tactics.getFinalSkillValue(); if (roll >= targetNumber) { reportStatus.append(' '); @@ -1976,7 +2000,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()) {