diff --git a/MekHQ/src/mekhq/campaign/personnel/generator/SingleSpecialAbilityGenerator.java b/MekHQ/src/mekhq/campaign/personnel/generator/SingleSpecialAbilityGenerator.java index 191da1d79c8..5c293e958a3 100644 --- a/MekHQ/src/mekhq/campaign/personnel/generator/SingleSpecialAbilityGenerator.java +++ b/MekHQ/src/mekhq/campaign/personnel/generator/SingleSpecialAbilityGenerator.java @@ -32,6 +32,7 @@ */ package mekhq.campaign.personnel.generator; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; @@ -43,15 +44,21 @@ import megamek.common.options.IOption; import megamek.common.options.OptionsConstants; import megamek.common.units.Crew; +import megamek.logging.MMLogger; import mekhq.campaign.Campaign; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.PersonnelOptions; import mekhq.campaign.personnel.SpecialAbility; +import mekhq.campaign.universe.Faction; +import mekhq.campaign.universe.eras.Era; +import mekhq.campaign.universe.eras.Eras; /** * Generates a single special ability for a {@link Person}. */ public class SingleSpecialAbilityGenerator extends AbstractSpecialAbilityGenerator { + private static final MMLogger LOGGER = MMLogger.create(SingleSpecialAbilityGenerator.class); + @Override public boolean generateSpecialAbilities(final Campaign campaign, final Person person, final int expLvl) { @@ -97,6 +104,10 @@ public boolean generateSpecialAbilities(final Campaign campaign, final Person pe useAlternativeWeighting, ignoreEligibility, isVeterancyAward); + if (person.isClanPersonnel()) { + filterOutMeleeSPA(campaign.getLocalDate(), person.getOriginFaction(), abilityList); + } + if (abilityList.isEmpty()) { return null; } @@ -170,6 +181,43 @@ public boolean generateSpecialAbilities(final Campaign campaign, final Person pe return displayName; } + /** + * Filters out melee-related Special Personnel Abilities (SPAs) from the given list. + * + *
Melee SPAs ({@code "melee_specialist"} and {@code "melee_master"}) are removed if the origin faction is a + * Clan, and either:
+ *The 'end of the Republic era' date was picked, as by the Dark Ages it appears the Clan dislike of melee + * has faded. At least among clans that had settled the Inner Sphere.
+ * + * @param today the current date, used to determine era eligibility + * @param originFaction the faction whose rules govern SPA eligibility + * @param abilityList the mutable list of {@link SpecialAbility} objects to filter in-place + * + * @author Illiani + * @since 0.51.0 + */ + private static void filterOutMeleeSPA(LocalDate today, Faction originFaction, ListUsed by faction-restricted academy access (and any future eligibility check) for non-FedCom - * faction successions: Clan Ghost Bear / Free Rasalhague Republic into Rasalhague Dominion, ComStar - * into Word of Blake, and similar mergers/splits. The check is bidirectional — either side's - * {@code fallBackFactions} can carry the relationship — because the YAMLs only declare the - * successor's predecessors (e.g. {@code RD.fallBackFactions = [CGB, FRR]}), never the inverse. + * faction successions: Clan Ghost Bear / Free Rasalhague Republic into Rasalhague Dominion, ComStar into Word of + * Blake, and similar mergers/splits. The check is bidirectional — either side's {@code fallBackFactions} can carry + * the relationship — because the YAMLs only declare the successor's predecessors (e.g. + * {@code RD.fallBackFactions = [CGB, FRR]}), never the inverse. * *
Meta-faction codes are excluded as compatibility targets: a real faction is never - * considered "compatible" with the abstract meta-faction {@code IS} (or {@code CLAN.IS}, or any - * {@code Periphery.*} / {@code CLAN.*} code) just because the real faction's - * {@code fallBackFactions} list includes that meta code as a generic data-lookup fallback. Most - * playable Inner Sphere factions list {@code IS} as a fallback for the - * {@link mekhq.campaign.universe.RandomFactionGenerator} machinery, so without this exclusion - * any of them would erroneously test compatible with the abstract IS umbrella. + * considered "compatible" with the abstract meta-faction {@code IS} (or {@code CLAN.IS}, or any {@code Periphery.*} + * / {@code CLAN.*} code) just because the real faction's {@code fallBackFactions} list includes that meta code as a + * generic data-lookup fallback. Most playable Inner Sphere factions list {@code IS} as a fallback for the + * {@link mekhq.campaign.universe.RandomFactionGenerator} machinery, so without this exclusion any of them would + * erroneously test compatible with the abstract IS umbrella. * *
The exclusion does not change comparisons between two real factions: LA and FS, for - * example, are correctly considered incompatible by this method because neither lists the other's - * short code in its fallbacks, regardless of any shared meta entries. + * example, are correctly considered incompatible by this method because neither lists the other's short code in its + * fallbacks, regardless of any shared meta entries. * *
FedCom-specific era rules (LA seceding 3057, Yvonne reverting to Federated Suns 3067) are * handled separately at the call site; this method intentionally does not look at the date. * * @param other the other faction to test compatibility against * - * @return {@code true} if this and {@code other} are the same faction, or if either lists the - * other in its {@code fallBackFactions} (after meta-code exclusion); {@code false} otherwise + * @return {@code true} if this and {@code other} are the same faction, or if either lists the other in its + * {@code fallBackFactions} (after meta-code exclusion); {@code false} otherwise */ public boolean isLineageCompatible(final @Nullable Faction other) { if (other == null) { @@ -717,4 +716,28 @@ public boolean isAggregate() { public @Nullable FactionLeaderData getLeaderForYear(final int year) { return faction2 != null ? faction2.getFactionLeaderForYear(year) : null; } + + /** + * Determines whether this faction is a Homeworld Clan. + * + *
A faction qualifies as a Homeworld Clan if it is a Clan faction and one of its alternative faction codes
+ * matches {@code "Clan.HW"} (case-insensitive).
+ *
+ * @return {@code true} if this faction is a Clan and has {@code "Clan.HW"} among its alternative faction codes;
+ * {@code false} otherwise
+ *
+ * @author Illiani
+ * @since 0.51.0
+ */
+ public boolean isHomeworldClan() {
+ if (isClan()) {
+ for (String factionCode : alternativeFactionCodes) {
+ if (factionCode.equalsIgnoreCase("Clan.HW")) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
}
diff --git a/MekHQ/src/mekhq/campaign/universe/eras/Eras.java b/MekHQ/src/mekhq/campaign/universe/eras/Eras.java
index 381611bf6d0..33720c7b192 100644
--- a/MekHQ/src/mekhq/campaign/universe/eras/Eras.java
+++ b/MekHQ/src/mekhq/campaign/universe/eras/Eras.java
@@ -85,6 +85,26 @@ private void setEras(final TreeMap