Skip to content

Commit b558f9e

Browse files
authored
Merge pull request #6207 from IllianiCBT/leadershipAndReinforcementFixes
Fixed Unit Eligibility Logic For Frontline and Leadership Units
2 parents a5b0b69 + aa65003 commit b558f9e

File tree

4 files changed

+233
-66
lines changed

4 files changed

+233
-66
lines changed

MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java

Lines changed: 191 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
package mekhq.campaign.stratcon;
2929

3030
import megamek.codeUtilities.ObjectUtility;
31+
import megamek.common.Entity;
3132
import megamek.common.Minefield;
3233
import megamek.common.TargetRoll;
3334
import megamek.common.annotations.Nullable;
@@ -827,6 +828,7 @@ private static void swapInPlayerUnits(StratconScenario scenario, Campaign campai
827828
}
828829
}
829830
}
831+
830832
for (Unit unit : potentialUnits) {
831833
if ((sft.getAllowedUnitType() == 11) && (!campaign.getCampaignOptions().isUseDropShips())) {
832834
continue;
@@ -2305,119 +2307,245 @@ private static boolean subElementsOrSelfDeployed(Force force, Campaign campaign)
23052307
}
23062308

23072309
/**
2308-
* Returns a list of individual units eligible for deployment in scenarios run
2309-
* by "Defend" lances
2310+
* Retrieves a list of units that are eligible for deployment in support of a Frontline force.
2311+
*
2312+
* <p>A unit is considered eligible if:</p>
2313+
* <ul>
2314+
* <li>It is valid (available, properly deployed, and of a suitable type i.e., conventional
2315+
* infantry or battle armor).</li>
2316+
* <li>The force to which it belongs is valid (not deployed, part of a combat team, and not in
2317+
* reserve).</li>
2318+
* </ul>
23102319
*
2311-
* @return List of unit IDs.
2320+
* @param campaign The campaign instance holding the units and forces involved.
2321+
* @param currentScenario
2322+
* @return A list of {@code Unit} objects that meet the requirements for deployment in support
2323+
* of a Frontline force.
23122324
*/
2313-
public static List<Unit> getEligibleDefensiveUnits(Campaign campaign) {
2314-
List<Unit> retVal = new ArrayList<>();
2315-
2316-
for (Unit u : campaign.getUnits()) {
2317-
// "defensive" units are infantry, battle armor and (Weisman help you) gun
2318-
// emplacements
2319-
// and also said unit should be intact/alive/etc
2320-
boolean isEligibleInfantry = ((u.getEntity().getUnitType() == INFANTRY)
2321-
|| (u.getEntity().getUnitType() == BATTLE_ARMOR)) && !u.isUnmanned();
2322-
2323-
boolean isEligibleGunEmplacement = u.getEntity().getUnitType() == GUN_EMPLACEMENT;
2324-
2325-
if ((isEligibleInfantry || isEligibleGunEmplacement)
2326-
&& !u.isDeployed()
2327-
&& !u.isMothballed()
2328-
&& (u.checkDeployment() == null)
2329-
&& !isUnitDeployedToStratCon(u)) {
2330-
2331-
// this is a little inefficient, but probably there aren't too many active AtB
2332-
// contracts at a time
2333-
for (AtBContract contract : campaign.getActiveAtBContracts()) {
2334-
if (contract.getStratconCampaignState().isForceDeployedHere(u.getForceId())) {
2335-
continue;
2336-
}
2337-
}
2325+
public static List<Unit> getEligibleFrontlineUnits(Campaign campaign, StratconScenario currentScenario) {
2326+
List<Unit> defensiveUnits = new ArrayList<>();
2327+
2328+
// Retrieve the list of units from force 0
2329+
Vector<UUID> unitIDs = campaign.getForce(0).getAllUnits(true);
2330+
2331+
for (UUID unitId : unitIDs) {
2332+
Unit unit = campaign.getUnit(unitId);
2333+
2334+
// Validate the unit
2335+
if (!isUnitValidForFrontlineDeployment(unit)) {
2336+
continue;
2337+
}
23382338

2339-
retVal.add(u);
2339+
// Validate the force associated with the unit
2340+
if (!isForceEligible(unit, campaign, currentScenario)) {
2341+
continue;
23402342
}
2343+
2344+
defensiveUnits.add(unit);
23412345
}
23422346

2343-
return retVal;
2347+
return defensiveUnits;
2348+
}
2349+
2350+
/**
2351+
* Checks if a unit is valid for deployment in support of a Frontline force.
2352+
*
2353+
* <p>A unit is considered valid if:</p>
2354+
* <ul>
2355+
* <li>The unit is not null.</li>
2356+
* <li>The unit is available for deployment.</li>
2357+
* <li>The unit's deployment checks return no errors.</li>
2358+
* <li>The unit's entity is of type conventional infantry or battle armor.</li>
2359+
* </ul>
2360+
*
2361+
* @param unit The {@code Unit} object to validate.
2362+
* @return {@code true} if the unit is valid; {@code false} otherwise.
2363+
*/
2364+
private static boolean isUnitValidForFrontlineDeployment(@Nullable Unit unit) {
2365+
if (unit == null) {
2366+
return false;
2367+
}
2368+
2369+
if (!unit.isAvailable()) {
2370+
return false;
2371+
}
2372+
2373+
if (unit.checkDeployment() != null) {
2374+
return false;
2375+
}
2376+
2377+
Entity entity = unit.getEntity();
2378+
return entity != null && (unit.isConventionalInfantry() || unit.isBattleArmor());
23442379
}
23452380

23462381
/**
2347-
* Returns a list of individual units eligible for deployment in scenarios that
2348-
* result from the
2349-
* lance leader having a leadership score
2382+
* Checks if the force associated with a unit is eligible for deployment in support of a Frontline
2383+
* force.
23502384
*
2351-
* @return List of unit IDs.
2385+
* <p>A force is considered eligible if:</p>
2386+
* <ul>
2387+
* <li>The force is not null.</li>
2388+
* <li>The force has not already been deployed.</li>
2389+
* <li>The force is part of a combat team.</li>
2390+
* <li>The combat team is not assigned a reserve role.</li>
2391+
* </ul>
2392+
*
2393+
* @param unit The {@code Unit} whose associated force is being validated.
2394+
* @param campaign The {@code Campaign} object used to retrieve information about the force.
2395+
* @param currentScenario
2396+
* @return {@code true} if the associated force is eligible; {@code false} otherwise.
23522397
*/
2353-
public static List<Unit> getEligibleLeadershipUnits(Campaign campaign, ArrayList<Integer> forceIDs,
2398+
private static boolean isForceEligible(Unit unit, Campaign campaign, StratconScenario currentScenario) {
2399+
int forceId = unit.getForceId();
2400+
Force force = campaign.getForce(forceId);
2401+
2402+
// If the force is deployed, skip; added check for insurance
2403+
if (force == null || force.isDeployed()) {
2404+
return false;
2405+
}
2406+
2407+
// Check the associated combat team and its role
2408+
CombatTeam combatTeam = force.isCombatTeam() ? campaign.getCombatTeamsTable().get(forceId) : null;
2409+
2410+
if (combatTeam == null) {
2411+
return false;
2412+
}
2413+
2414+
if (combatTeam.getRole().isReserve()) {
2415+
return false;
2416+
}
2417+
2418+
AtBContract forceContract = combatTeam.getContract(campaign);
2419+
AtBContract scenarioContract = currentScenario.getBackingContract(campaign);
2420+
2421+
return forceContract.equals(scenarioContract);
2422+
}
2423+
2424+
/**
2425+
* Retrieves a list of units that are eligible for leadership deployment.
2426+
*
2427+
* <p>A unit is considered eligible for leadership deployment if:</p>
2428+
* <ul>
2429+
* <li>There is sufficient leadership skill to justify leadership deployment.</li>
2430+
* <li>The total leadership budget (based on leadership skill) is greater than 0.</li>
2431+
* <li>The unit matches the general unit type of the primary force in the scenario.</li>
2432+
* <li>The unit's battle value is within the computed budget.</li>
2433+
* <li>The unit and its associated force are valid for deployment.</li>
2434+
* </ul>
2435+
*
2436+
* @param campaign The campaign instance holding the units and forces involved.
2437+
* @param currentScenario The current StratCon scenario being processed.
2438+
* @param leadershipSkill The leadership skill value used to calculate budget.
2439+
* @return A list of {@code Unit} objects eligible for deployment as leadership units.
2440+
*/
2441+
public static List<Unit> getEligibleLeadershipUnits(Campaign campaign, StratconScenario currentScenario,
23542442
int leadershipSkill) {
2355-
List<Unit> eligibleUnits = new ArrayList<>();
2443+
List<Integer> forceIds = currentScenario.getPrimaryForceIDs();
2444+
List<Unit> leadershipUnits = new ArrayList<>();
23562445

23572446
// If there is no leadership skill, we shouldn't continue
23582447
if (leadershipSkill <= 0) {
2359-
return eligibleUnits;
2448+
return leadershipUnits;
23602449
}
23612450

2362-
// The criteria are as follows:
2363-
// - unit is eligible to be spawned on the scenario type
2364-
// - unit has a lower BV than the BV budget granted from Leadership
2365-
// Leadership budget is capped at 5 levels
23662451
int totalBudget = min(BASE_LEADERSHIP_BUDGET * leadershipSkill, BASE_LEADERSHIP_BUDGET * 5);
23672452

2368-
int primaryUnitType = getPrimaryUnitType(campaign, forceIDs);
2453+
int primaryUnitType = getPrimaryUnitType(campaign, forceIds);
23692454

23702455
// If there are no units (somehow), we've no reason to continue
23712456
if (primaryUnitType == -1) {
2372-
return eligibleUnits;
2457+
return leadershipUnits;
23732458
}
23742459

23752460
int generalUnitType = convertSpecificUnitTypeToGeneral(primaryUnitType);
23762461

2377-
for (UUID unitId : campaign.getForce(0).getAllUnits(true)) {
2462+
2463+
// Retrieve the list of units from force 0
2464+
Vector<UUID> unitIDs = campaign.getForce(0).getAllUnits(true);
2465+
2466+
for (UUID unitId : unitIDs) {
23782467
Unit unit = campaign.getUnit(unitId);
2379-
if (unit == null) {
2468+
2469+
// Validate the unit
2470+
if (!isUnitValidForLeadershipDeployment(unit, generalUnitType, totalBudget)) {
23802471
continue;
23812472
}
23822473

2383-
// the general idea is that we want something that can be deployed to the scenario -
2384-
// e.g., no infantry on air scenarios etc.
2385-
boolean validUnitType = (forceCompositionMatchesDeclaredUnitType(unit.getEntity().getUnitType(),
2386-
generalUnitType));
2387-
2388-
if (validUnitType
2389-
&& !unit.isDeployed()
2390-
&& !unit.isMothballed()
2391-
&& (unit.getEntity().calculateBattleValue(true, true) <= totalBudget)
2392-
&& (unit.checkDeployment() == null)
2393-
&& !isUnitDeployedToStratCon(unit)) {
2394-
eligibleUnits.add(unit);
2474+
// Validate the force associated with the unit
2475+
if (!isForceEligible(unit, campaign, currentScenario)) {
2476+
continue;
23952477
}
2478+
2479+
leadershipUnits.add(unit);
2480+
}
2481+
2482+
return leadershipUnits;
2483+
}
2484+
2485+
/**
2486+
* Checks if a unit is valid for leadership deployment.
2487+
*
2488+
* <p>A unit is considered valid for leadership deployment if:</p>
2489+
* <ul>
2490+
* <li>The unit is not null.</li>
2491+
* <li>The unit is available for deployment.</li>
2492+
* <li>The unit has no existing deployment records (i.e., not already deployed).</li>
2493+
* <li>The unit's entity is valid and has a battle value within the provided leadership budget.</li>
2494+
* <li>The unit matches the general unit type required for the deployment.</li>
2495+
* </ul>
2496+
*
2497+
* @param unit The {@code Unit} object to validate for leadership deployment.
2498+
* @param generalUnitType The general unit type required for this deployment.
2499+
* @param totalBudget The total battle value budget available for leadership deployment.
2500+
* @return {@code true} if the unit is valid for leadership deployment; {@code false} otherwise.
2501+
*/
2502+
private static boolean isUnitValidForLeadershipDeployment(@Nullable Unit unit, int generalUnitType,
2503+
int totalBudget) {
2504+
if (unit == null) {
2505+
return false;
2506+
}
2507+
2508+
if (!unit.isAvailable()) {
2509+
return false;
2510+
}
2511+
2512+
if (unit.checkDeployment() != null) {
2513+
return false;
2514+
}
2515+
2516+
Entity entity = unit.getEntity();
2517+
2518+
if (entity == null) {
2519+
return false;
2520+
}
2521+
2522+
if (entity.calculateBattleValue(true, true) > totalBudget) {
2523+
return false;
23962524
}
23972525

2398-
return eligibleUnits;
2526+
return forceCompositionMatchesDeclaredUnitType(entity.getUnitType(), generalUnitType);
23992527
}
24002528

24012529
/**
24022530
* Check if the unit's force (if one exists) has been deployed to a StratCon
24032531
* track
24042532
*/
2405-
public static boolean isUnitDeployedToStratCon(Unit u) {
2406-
if (!u.getCampaign().getCampaignOptions().isUseStratCon()) {
2533+
public static boolean isUnitDeployedToStratCon(Unit unit) {
2534+
if (!unit.getCampaign().getCampaignOptions().isUseStratCon()) {
24072535
return false;
24082536
}
24092537

24102538
// this is a little inefficient, but probably there aren't too many active AtB
24112539
// contracts at a time
2412-
return u.getCampaign().getActiveAtBContracts().stream()
2540+
return unit.getCampaign().getActiveAtBContracts().stream()
24132541
.anyMatch(contract -> (contract.getStratconCampaignState() != null) &&
2414-
contract.getStratconCampaignState().isForceDeployedHere(u.getForceId()));
2542+
contract.getStratconCampaignState().isForceDeployedHere(unit.getForceId()));
24152543
}
24162544

24172545
/**
24182546
* Calculates the majority unit type for the forces given the IDs.
24192547
*/
2420-
private static int getPrimaryUnitType(Campaign campaign, ArrayList<Integer> forceIDs) {
2548+
private static int getPrimaryUnitType(Campaign campaign, List<Integer> forceIDs) {
24212549
Map<Integer, Integer> unitTypeBuckets = new TreeMap<>();
24222550
int biggestBucketID = -1;
24232551
int biggestBucketCount = 0;

MekHQ/src/mekhq/campaign/stratcon/StratconScenario.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,24 @@ public AtBDynamicScenario getBackingScenario() {
299299
return backingScenario;
300300
}
301301

302+
/**
303+
* Retrieves the {@link AtBContract} associated with the backing scenario.
304+
*
305+
* <p>If the backing scenario is null, this method will return {@code null}. Otherwise, it
306+
* retrieves the associated contract through the provided campaign instance.
307+
*
308+
* @param campaign The {@code Campaign} instance used to obtain the contract.
309+
* @return The {@code AtBContract} associated with the current backing scenario, or {@code null}
310+
* if no backing scenario exists.
311+
*/
312+
public @Nullable AtBContract getBackingContract(Campaign campaign) {
313+
if (backingScenario == null) {
314+
return null;
315+
}
316+
317+
return backingScenario.getContract(campaign);
318+
}
319+
302320
@XmlJavaTypeAdapter(DateAdapter.class)
303321
public LocalDate getDeploymentDate() {
304322
return deploymentDate;

MekHQ/src/mekhq/campaign/unit/Unit.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1131,8 +1131,29 @@ public void undeploy() {
11311131
scenarioId = -1;
11321132
}
11331133

1134+
/**
1135+
* Validates the deployment readiness of the unit.
1136+
*
1137+
* <p>This method checks multiple conditions to determine whether the unit is deployable. If the
1138+
* unit is not deployable, a descriptive error message is returned indicating the reason for its
1139+
* ineligibility. If the unit passes all validations, {@code null} is returned, implying it is
1140+
* ready for deployment.<p>
1141+
*
1142+
* Deployment checks performed:
1143+
* <ul>
1144+
* <li>If the unit is not functional, it cannot be deployed.</li>
1145+
* <li>If the unit is unmanned and is not an unmanned trailer, it cannot be deployed.</li>
1146+
* <li>If the unit is in the process of being refitted, it cannot be deployed.</li>
1147+
* <li>If the unit is a tank and does not have the required crew size, it cannot be deployed.</li>
1148+
* <li>If the unit is a BattleArmor unit with empty suits, it cannot be deployed until these
1149+
* are filled or salvaged.</li>
1150+
* </ul>
1151+
*
1152+
* @return A descriptive {@code String} error message if the unit cannot be deployed, or {@code null}
1153+
* if the unit is deployable.
1154+
*/
11341155
// TODO: Add support for advanced medical
1135-
public String checkDeployment() {
1156+
public @Nullable String checkDeployment() {
11361157
if (!isFunctional()) {
11371158
return "unit is not functional";
11381159
}

MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ private void setUI(boolean isPrimaryForce) {
206206
gbc.gridy++;
207207
int leadershipSkill = currentScenario.getBackingScenario().getLanceCommanderSkill(S_LEADER, campaign);
208208
eligibleLeadershipUnits = getEligibleLeadershipUnits(
209-
campaign, currentScenario.getPrimaryForceIDs(), leadershipSkill);
209+
campaign, currentScenario, leadershipSkill);
210210
eligibleLeadershipUnits.sort(Comparator.comparing(this::getForceNameReversed));
211211

212212
setLeadershipUI(gbc, eligibleLeadershipUnits, leadershipSkill);
@@ -406,7 +406,7 @@ private void setDefensiveUI(GridBagConstraints gbc) {
406406
gbc.gridy++;
407407

408408
// Obtain eligible infantry units
409-
List<Unit> eligibleInfantryUnits = StratconRulesManager.getEligibleDefensiveUnits(campaign);
409+
List<Unit> eligibleInfantryUnits = StratconRulesManager.getEligibleFrontlineUnits(campaign, currentScenario);
410410
eligibleInfantryUnits.sort(Comparator.comparing(Unit::getName));
411411

412412
// Add a unit selector for infantry units

0 commit comments

Comments
 (0)