|
| 1 | +/* |
| 2 | + * Copyright (C) 2025 The MegaMek Team. All Rights Reserved. |
| 3 | + * |
| 4 | + * This file is part of MekHQ. |
| 5 | + * |
| 6 | + * MekHQ is free software: you can redistribute it and/or modify |
| 7 | + * it under the terms of the GNU General Public License (GPL), |
| 8 | + * version 3 or (at your option) any later version, |
| 9 | + * as published by the Free Software Foundation. |
| 10 | + * |
| 11 | + * MekHQ is distributed in the hope that it will be useful, |
| 12 | + * but WITHOUT ANY WARRANTY; without even the implied warranty |
| 13 | + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| 14 | + * See the GNU General Public License for more details. |
| 15 | + * |
| 16 | + * A copy of the GPL should have been included with this project; |
| 17 | + * if not, see <https://www.gnu.org/licenses/>. |
| 18 | + * |
| 19 | + * NOTICE: The MegaMek organization is a non-profit group of volunteers |
| 20 | + * creating free software for the BattleTech community. |
| 21 | + * |
| 22 | + * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks |
| 23 | + * of The Topps Company, Inc. All Rights Reserved. |
| 24 | + * |
| 25 | + * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of |
| 26 | + * InMediaRes Productions, LLC. |
| 27 | + * |
| 28 | + * MechWarrior Copyright Microsoft Corporation. MekHQ was created under |
| 29 | + * Microsoft's "Game Content Usage Rules" |
| 30 | + * <https://www.xbox.com/en-US/developers/rules> and it is not endorsed by or |
| 31 | + * affiliated with Microsoft. |
| 32 | + */ |
| 33 | +package mekhq.campaign.universe.factionStanding; |
| 34 | + |
| 35 | +import static mekhq.campaign.universe.factionStanding.FactionStandings.DEFAULT_REGARD; |
| 36 | +import static mekhq.campaign.universe.factionStanding.FactionStandings.STARTING_REGARD_ALLIED_FACTION; |
| 37 | +import static mekhq.campaign.universe.factionStanding.FactionStandings.STARTING_REGARD_ENEMY_FACTION_AT_WAR; |
| 38 | +import static mekhq.campaign.universe.factionStanding.FactionStandings.STARTING_REGARD_ENEMY_FACTION_RIVAL; |
| 39 | +import static mekhq.campaign.universe.factionStanding.FactionStandings.STARTING_REGARD_SAME_FACTION; |
| 40 | +import static mekhq.campaign.universe.factionStanding.MercenaryRelations.StandingModifier.ABOVE_AVERAGE; |
| 41 | +import static mekhq.campaign.universe.factionStanding.MercenaryRelations.StandingModifier.AVERAGE; |
| 42 | +import static mekhq.campaign.universe.factionStanding.MercenaryRelations.StandingModifier.AWFUL; |
| 43 | +import static mekhq.campaign.universe.factionStanding.MercenaryRelations.StandingModifier.BELOW_AVERAGE; |
| 44 | + |
| 45 | +import java.time.LocalDate; |
| 46 | +import java.util.List; |
| 47 | +import java.util.Map; |
| 48 | + |
| 49 | +import megamek.common.annotations.Nullable; |
| 50 | +import mekhq.campaign.universe.Faction; |
| 51 | + |
| 52 | +/** |
| 53 | + * Provides historical and contextual modifiers that represent how various factions perceive mercenary forces. The |
| 54 | + * modifiers are organized as a mapping from faction codes to chronologically ordered lists of {@link MercenaryRelation} |
| 55 | + * instances, each describing a standing modifier for a time period. |
| 56 | + * |
| 57 | + * <p>This class supplies query methods for retrieving the current relationship modifier based on a faction and year. |
| 58 | + * It also defines convenience constants for representing default or fallback values for the Inner Sphere and Clan |
| 59 | + * factions.</p> |
| 60 | + * |
| 61 | + * @author Illiani |
| 62 | + * @since 0.50.07 |
| 63 | + */ |
| 64 | +public class MercenaryRelations { |
| 65 | + static final LocalDate NO_STARTING_DATE = LocalDate.of(2000, 1, 1); |
| 66 | + private static final LocalDate NO_ENDING_DATE = LocalDate.MAX; |
| 67 | + static final double INNER_SPHERE_FALLBACK_VALUE = AVERAGE.getModifier(); |
| 68 | + static final double CLAN_FALLBACK_VALUE = AWFUL.getModifier(); |
| 69 | + |
| 70 | + /** |
| 71 | + * A mapping of faction codes to their respective lists of {@link MercenaryRelation} objects, describing how each |
| 72 | + * faction's relationship with mercenaries changes over various historical periods. |
| 73 | + * |
| 74 | + * <p>Each key is a faction code (e.g., "FS" for Federated Suns), and the value is a list detailing the standing |
| 75 | + * modifiers effective for specific date ranges, including explanatory context.</p> |
| 76 | + * |
| 77 | + * <p>If a faction code is not present in this map, lookups using {@link Map#get(Object)} will return |
| 78 | + * {@code null}.</p> |
| 79 | + * |
| 80 | + * <p><b>Usage:</b> Enter the faction code followed by a list of {@link MercenaryRelation} objects. These |
| 81 | + * objects should be ordered in chronological order oldest -> newest as the first valid modifier will be the |
| 82 | + * modifier returned. Ideally, an explanation of each modifier should be included so that future developers know the |
| 83 | + * logic behind the change.</p> |
| 84 | + * |
| 85 | + * @since 0.50.07 |
| 86 | + */ |
| 87 | + static final Map<String, List<MercenaryRelation>> CLIMATE_FACTION_STANDING_MODIFIERS = Map.ofEntries( |
| 88 | + // Federated Suns |
| 89 | + Map.entry("FS", List.of( |
| 90 | + // Professional, pragmatic approach via Department of Mercenary Relations (default standing) |
| 91 | + new MercenaryRelation(NO_STARTING_DATE, LocalDate.of(3062, 11, 15), ABOVE_AVERAGE), |
| 92 | + // FedCom Civil War Period: Internal conflict due to Civil War causing disruption |
| 93 | + new MercenaryRelation(LocalDate.of(3062, 11, 16), LocalDate.of(3067, 4, 20), BELOW_AVERAGE), |
| 94 | + // Slow regaining of stability following the Civil War |
| 95 | + new MercenaryRelation(LocalDate.of(3067, 4, 21), LocalDate.of(3069, 12, 31), AVERAGE), |
| 96 | + // Professional, pragmatic approach via Department of Mercenary Relations (default standing) |
| 97 | + new MercenaryRelation(LocalDate.of(3070, 1, 1), NO_ENDING_DATE, ABOVE_AVERAGE) |
| 98 | + )), |
| 99 | + // Draconis Combine |
| 100 | + Map.entry("DC", List.of( |
| 101 | + // Distrustful due to bushido culture viewing mercenaries as dishonorable (default standing) |
| 102 | + new MercenaryRelation(NO_STARTING_DATE, LocalDate.of(3029, 9, 3), BELOW_AVERAGE), |
| 103 | + // Death to Mercenaries edict |
| 104 | + new MercenaryRelation(LocalDate.of(3029, 9, 4), LocalDate.of(3039, 4, 15), AWFUL), |
| 105 | + // Death to Mercenaries starts to be relaxed, but not wholly removed |
| 106 | + new MercenaryRelation(LocalDate.of(3039, 4, 16), LocalDate.of(3054, 9, 14), BELOW_AVERAGE), |
| 107 | + // Theodore Kurita's reforms (new default) |
| 108 | + new MercenaryRelation(LocalDate.of(3054, 9, 15), NO_ENDING_DATE, AVERAGE) |
| 109 | + )), |
| 110 | + // Capellan Confederation |
| 111 | + Map.entry("CC", List.of( |
| 112 | + // Pragmatic but cautious (default standing) |
| 113 | + new MercenaryRelation(NO_STARTING_DATE, LocalDate.of(3036, 5, 11), BELOW_AVERAGE), |
| 114 | + // Romano Liao's Paranoid Purges: Extended suspicion and harsh treatment to mercenary contractors |
| 115 | + new MercenaryRelation(LocalDate.of(3036, 5, 12), LocalDate.of(3052, 5, 8), AWFUL), |
| 116 | + // Pragmatic but cautious (default standing) |
| 117 | + new MercenaryRelation(LocalDate.of(3052, 5, 9), NO_ENDING_DATE, BELOW_AVERAGE) |
| 118 | + )), |
| 119 | + // Lyran Commonwealth |
| 120 | + Map.entry("LA", List.of( |
| 121 | + // Bureaucratic but welcoming major employer (default standing) |
| 122 | + new MercenaryRelation(NO_STARTING_DATE, LocalDate.of(3062, 11, 15), ABOVE_AVERAGE), |
| 123 | + // FedCom Civil War Period: Internal conflict due to Civil War causing disruption |
| 124 | + new MercenaryRelation(LocalDate.of(3062, 11, 16), LocalDate.of(3067, 4, 20), BELOW_AVERAGE), |
| 125 | + // Slow regaining of stability following the Civil War |
| 126 | + new MercenaryRelation(LocalDate.of(3067, 4, 21), LocalDate.of(3069, 12, 31), AVERAGE), |
| 127 | + // Bureaucratic but welcoming major employer (default standing) |
| 128 | + new MercenaryRelation(LocalDate.of(3070, 1, 1), NO_ENDING_DATE, ABOVE_AVERAGE) |
| 129 | + )), |
| 130 | + // Free Worlds League (included so future developers don't think it was forgotten) |
| 131 | + Map.entry("FWL", List.of( |
| 132 | + // Utilitarian, often for internal balance (default standing) |
| 133 | + new MercenaryRelation(NO_STARTING_DATE, NO_ENDING_DATE, AVERAGE) |
| 134 | + )), |
| 135 | + // Free Rasalhague Republic |
| 136 | + Map.entry("FRR", List.of( |
| 137 | + // Ronin War mercenary betrayals created cultural bias against mercenaries |
| 138 | + new MercenaryRelation(LocalDate.of(3034, 5, 23), LocalDate.of(3035, 5, 23), AWFUL), |
| 139 | + // Lingering fallout from the betrayals (new default) |
| 140 | + new MercenaryRelation(LocalDate.of(3035, 5, 24), NO_ENDING_DATE, BELOW_AVERAGE) |
| 141 | + )), |
| 142 | + // Clan Diamond Shark |
| 143 | + Map.entry("CDS", List.of( |
| 144 | + // As the Merchant Clan they become more pragmatic about business relationships with mercenaries once |
| 145 | + // isolated from the Homeworlds |
| 146 | + new MercenaryRelation(LocalDate.of(3075, 12, 1), NO_ENDING_DATE, BELOW_AVERAGE) |
| 147 | + )), |
| 148 | + // Taurian Concordat |
| 149 | + Map.entry("TC", List.of( |
| 150 | + // Generally suspicious of mercenaries due to anti-Inner Sphere paranoia (default standing) |
| 151 | + new MercenaryRelation(NO_STARTING_DATE, LocalDate.of(3058, 8, 1), BELOW_AVERAGE), |
| 152 | + // Trinity Alliance Period: Cooperation with CC and MoC led to more professional mercenary relations |
| 153 | + new MercenaryRelation(LocalDate.of(3058, 8, 2), LocalDate.of(3067, 12, 31), AVERAGE), |
| 154 | + // Generally suspicious of mercenaries due to anti-Inner Sphere paranoia (default standing) |
| 155 | + new MercenaryRelation(LocalDate.of(3068, 1, 1), NO_ENDING_DATE, BELOW_AVERAGE) |
| 156 | + )), |
| 157 | + // Magistracy of Canopus |
| 158 | + Map.entry("MOC", List.of( |
| 159 | + // Pragmatic and welcoming due to open society values |
| 160 | + new MercenaryRelation(NO_STARTING_DATE, NO_ENDING_DATE, ABOVE_AVERAGE) |
| 161 | + )), |
| 162 | + // Outworlds Alliance |
| 163 | + Map.entry("OA", List.of( |
| 164 | + // Practical necessity due to weak military |
| 165 | + new MercenaryRelation(NO_STARTING_DATE, NO_ENDING_DATE, ABOVE_AVERAGE) |
| 166 | + )) |
| 167 | + ); |
| 168 | + |
| 169 | + /** |
| 170 | + * Enumerates the possible standing modifiers for a faction's attitude toward mercenaries, with internal numeric |
| 171 | + * values representing their severity. |
| 172 | + * |
| 173 | + * @author Illiani |
| 174 | + * @since 0.50.07 |
| 175 | + */ |
| 176 | + enum StandingModifier { |
| 177 | + AWFUL(STARTING_REGARD_ENEMY_FACTION_AT_WAR), |
| 178 | + BELOW_AVERAGE(STARTING_REGARD_ENEMY_FACTION_RIVAL), |
| 179 | + AVERAGE(DEFAULT_REGARD), |
| 180 | + ABOVE_AVERAGE(STARTING_REGARD_ALLIED_FACTION), |
| 181 | + AMAZING(STARTING_REGARD_SAME_FACTION); |
| 182 | + |
| 183 | + private final double modifier; |
| 184 | + |
| 185 | + /** |
| 186 | + * Constructs a standing modifier with its associated numeric value. |
| 187 | + * |
| 188 | + * @param modifier the floating-point value representing the modifier. |
| 189 | + * |
| 190 | + * @author Illiani |
| 191 | + * @since 0.50.07 |
| 192 | + */ |
| 193 | + StandingModifier(double modifier) { |
| 194 | + this.modifier = modifier; |
| 195 | + } |
| 196 | + |
| 197 | + /** |
| 198 | + * Gets the numeric value assigned to this standing modifier. |
| 199 | + * |
| 200 | + * @return the standing modifier as a floating-point value. |
| 201 | + * |
| 202 | + * @author Illiani |
| 203 | + * @since 0.50.07 |
| 204 | + */ |
| 205 | + public double getModifier() { |
| 206 | + return modifier; |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + /** |
| 211 | + * Represents the relationship between a faction and mercenaries during a specific historical period. |
| 212 | + * |
| 213 | + * @param startingDate The first date this relationship applies (inclusive), or 01/01/2000 for no defined |
| 214 | + * start. |
| 215 | + * @param endingDate The last date this relationship applies (inclusive), or {@link LocalDate#MAX} for no |
| 216 | + * defined end. |
| 217 | + * @param standingModifier The {@link StandingModifier} indicating the faction's attitude in this period. |
| 218 | + * |
| 219 | + * @author Illiani |
| 220 | + * @since 0.50.07 |
| 221 | + */ |
| 222 | + public record MercenaryRelation(LocalDate startingDate, LocalDate endingDate, StandingModifier standingModifier) { |
| 223 | + } |
| 224 | + |
| 225 | + /** |
| 226 | + * Returns the standing modifier value for the specified faction and year, representing how strongly the faction |
| 227 | + * favors or disfavors mercenaries. |
| 228 | + * |
| 229 | + * <p>If no specific modifier is found for the faction and year, a fallback value is returned depending on |
| 230 | + * whether the faction is considered Inner Sphere or Clan.</p> |
| 231 | + * |
| 232 | + * @param faction the faction whose relationship to mercenaries is to be retrieved, or {@code null} to use fallback |
| 233 | + * @param today the date to check for the relevant relationship |
| 234 | + * |
| 235 | + * @return the numeric standing modifier for how the faction treats mercenaries |
| 236 | + * |
| 237 | + * @author Illiani |
| 238 | + * @since 0.50.07 |
| 239 | + */ |
| 240 | + public static double getMercenaryRelationsModifier(@Nullable Faction faction, LocalDate today) { |
| 241 | + if (faction == null) { |
| 242 | + return INNER_SPHERE_FALLBACK_VALUE; |
| 243 | + } |
| 244 | + |
| 245 | + double defaultModifier = faction.isClan() ? CLAN_FALLBACK_VALUE : INNER_SPHERE_FALLBACK_VALUE; |
| 246 | + |
| 247 | + String factionCode = faction.getShortName(); |
| 248 | + List<MercenaryRelation> mercenaryRelations = CLIMATE_FACTION_STANDING_MODIFIERS.get(factionCode); |
| 249 | + if (mercenaryRelations == null) { |
| 250 | + return defaultModifier; |
| 251 | + } |
| 252 | + |
| 253 | + for (MercenaryRelation relation : mercenaryRelations) { |
| 254 | + if (isDateEqualOrBefore(relation.startingDate, today) |
| 255 | + && |
| 256 | + (relation.endingDate.equals(NO_ENDING_DATE) || isDateEqualOrAfter(relation.endingDate, today))) { |
| 257 | + return relation.standingModifier.getModifier(); |
| 258 | + } |
| 259 | + } |
| 260 | + |
| 261 | + return defaultModifier; |
| 262 | + } |
| 263 | + |
| 264 | + /** |
| 265 | + * Determines whether the first date is equal to or comes before the second date. |
| 266 | + * |
| 267 | + * @param firstDate the first date to compare |
| 268 | + * @param secondDate the second date to compare against |
| 269 | + * |
| 270 | + * @return {@link true} if the first date is equal to or comes before the second date; {@link false} otherwise |
| 271 | + * |
| 272 | + * @author Illiani |
| 273 | + * @since 0.50.07 |
| 274 | + */ |
| 275 | + private static boolean isDateEqualOrBefore(LocalDate firstDate, LocalDate secondDate) { |
| 276 | + return firstDate.isBefore(secondDate) || firstDate.isEqual(secondDate); |
| 277 | + } |
| 278 | + |
| 279 | + /** |
| 280 | + * Determines whether the first date is equal to or comes after the second date. |
| 281 | + * |
| 282 | + * @param firstDate the first date to compare |
| 283 | + * @param secondDate the second date to compare against |
| 284 | + * |
| 285 | + * @return {@link true} if the first date is equal to or comes after the second date; {@link false} otherwise |
| 286 | + * |
| 287 | + * @author Illiani |
| 288 | + * @since 0.50.07 |
| 289 | + */ |
| 290 | + private static boolean isDateEqualOrAfter(LocalDate firstDate, LocalDate secondDate) { |
| 291 | + return firstDate.isAfter(secondDate) || firstDate.isEqual(secondDate); |
| 292 | + } |
| 293 | +} |
0 commit comments