Skip to content

Commit 890a088

Browse files
authored
Merge pull request #7228 from IllianiBird/factionStandingMercenaryRelations
Improvement: Added Canon-Based Mercenary Relationships to the Faction Standing System
2 parents c71a49d + b6244c3 commit 890a088

File tree

3 files changed

+495
-0
lines changed

3 files changed

+495
-0
lines changed

MekHQ/src/mekhq/campaign/universe/factionStanding/FactionStandings.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,18 @@ public List<String> initializeStartingRegardValues(final Faction campaignFaction
351351
regardChangeReports.add(report);
352352
}
353353
}
354+
355+
if (campaignFaction.isMercenary()) {
356+
double mercenaryRelationsModifier = MercenaryRelations.getMercenaryRelationsModifier(otherFaction,
357+
today);
358+
359+
if (mercenaryRelationsModifier != DEFAULT_REGARD) {
360+
report = changeRegardForFaction(otherFactionCode, mercenaryRelationsModifier, gameYear);
361+
if (!report.isBlank()) {
362+
regardChangeReports.add(report);
363+
}
364+
}
365+
}
354366
}
355367

356368
return regardChangeReports;
@@ -587,6 +599,15 @@ public String updateClimateRegard(final Faction campaignFaction, final LocalDate
587599
if (factionHints.isRivalOf(campaignFaction, otherFaction, today)) {
588600
climateRegard.put(otherFactionCode, CLIMATE_REGARD_ENEMY_FACTION_RIVAL);
589601
}
602+
603+
if (campaignFaction.isMercenary()) {
604+
double mercenaryRelationsModifier = MercenaryRelations.getMercenaryRelationsModifier(otherFaction,
605+
today);
606+
607+
if (mercenaryRelationsModifier != DEFAULT_REGARD) {
608+
climateRegard.put(otherFactionCode, mercenaryRelationsModifier);
609+
}
610+
}
590611
}
591612

592613
// If we're not handling any climate modifiers, return an empty string
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
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

Comments
 (0)