Skip to content

Commit bfd54df

Browse files
authored
Merge pull request #7844 from IllianiBird/attributeRandomizationImprovements
Improvement: Improved Randomization of Traits & Attribute Scores
2 parents d2b2c5f + d6c8b33 commit bfd54df

File tree

2 files changed

+146
-63
lines changed

2 files changed

+146
-63
lines changed

MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,23 +1495,28 @@ lblUseAttributes.tooltip=If checked, characters will be generated with <i>A Time
14951495
<br><b>Warning:</b> If enabled (or disabled) mid-campaign, you need to navigate to the personnel tab, and (while in\
14961496
\ GM Mode) you should use the right-click menu to reset attribute scores for all personnel.
14971497
lblRandomizeAttributes.text=Random Attributes
1498-
lblRandomizeAttributes.tooltip=If checked, a d6 is rolled for each of the character's ATOW Attributes.\
1498+
lblRandomizeAttributes.tooltip=If checked, a 2d6 is rolled for each of the character's ATOW Attributes.\
14991499
<br>\
1500-
<br>On a roll of a 6 the Attribute is increased by 1. On a roll of a 1 the Attribute is decreased\
1501-
\ by 1.
1500+
<br>- 2: score reduced by 2\
1501+
<br>- 3-5: score reduced by 1\
1502+
<br>- 6-8: score stays the same\
1503+
<br>- 9-11: score increased by 1\
1504+
<br>- 12: score increased by 2
15021505
lblDisplayAllAttributes.text=Display All Attributes <span style="color:#C344C3;">\u2605</span>
15031506
lblDisplayAllAttributes.tooltip=If checked, a character's profile will display all of their ATOW Attributes. \
15041507
Otherwise, only those attributes with a skill check modifier will be displayed. Even if this option is disabled, \
15051508
all attributes can be seen by selecting the 'attributes' filter in the personnel table.
15061509
lblRandomizeTraits.text=Randomize Traits
1507-
lblRandomizeTraits.tooltip=If checked, a newly created character's Connections, Wealth, Reputation, Unlucky, and \
1508-
Bloodmark scores are randomized.\
1509-
<br>\
1510-
<br>- For <b>Connections</b> there is a 1-in-6 chance the character has rank 1.\
1511-
<br>- For <b>Wealth</b> and <b>Reputation</b> there is a 1-in-6 chance the character has rank 1 and 1-in-6 they have \
1512-
rank -1.\
1513-
<br>- For <b>Unlucky</b> there is a 1-in-20 chance the character has rank 1\
1514-
<br>- For <b>Bloodmark</b> there is a 1-in-100 chance the character has rank 1. Increased to 1-in-50 for pirates.
1510+
lblRandomizeTraits.tooltip=If checked, a 2d6 is rolled for each of the character's ATOW Traits (within the confines \
1511+
of that Trait's minimum and maximum values).\
1512+
<br>\
1513+
<br>- 2: score reduced by 2\
1514+
<br>- 3-5: score reduced by 1\
1515+
<br>- 6-8: score stays the same\
1516+
<br>- 9-11: score increased by 1\
1517+
<br>- 12: score increased by 2\
1518+
<br>- For <b>Bloodmark</b> there is a 99% chance of no change. Increased to 11% for pirates. <b>Unlucky</b> is \
1519+
reduced to 5%.
15151520
lblAllowMonthlyReinvestment.text=Allow Monthly Reinvestment of Wealth
15161521
lblAllowMonthlyReinvestment.tooltip=Each month the campaign commander will use their Wealth trait to\
15171522
\ reinvest money back into the campaign.\

MekHQ/src/mekhq/campaign/personnel/generator/DefaultSkillGenerator.java

Lines changed: 130 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@
3232
*/
3333
package mekhq.campaign.personnel.generator;
3434

35+
import static megamek.codeUtilities.MathUtility.clamp;
3536
import static megamek.common.compute.Compute.d6;
3637
import static megamek.common.compute.Compute.randomInt;
38+
import static mekhq.campaign.personnel.Person.*;
3739
import static mekhq.campaign.personnel.skills.Attributes.DEFAULT_ATTRIBUTE_SCORE;
40+
import static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE;
41+
import static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE;
3842
import static mekhq.campaign.personnel.skills.InfantryGunnerySkills.INFANTRY_GUNNERY_SKILLS;
3943
import static mekhq.campaign.personnel.skills.SkillDeprecationTool.DEPRECATED_SKILLS;
4044
import static mekhq.campaign.personnel.skills.SkillType.EXP_ELITE;
@@ -55,6 +59,7 @@
5559
import mekhq.campaign.personnel.Person;
5660
import mekhq.campaign.personnel.enums.PersonnelRole;
5761
import mekhq.campaign.personnel.enums.Phenotype;
62+
import mekhq.campaign.personnel.skills.Attributes;
5863
import mekhq.campaign.personnel.skills.RandomSkillPreferences;
5964
import mekhq.campaign.personnel.skills.SkillType;
6065
import mekhq.campaign.personnel.skills.Skills;
@@ -183,6 +188,31 @@ private static void generateCommandUtilitySkills(Person person, int expLvl,
183188
}
184189
}
185190

191+
/**
192+
* Generates and assigns attribute scores for the specified person based on their profession, phenotype, and
193+
* randomization settings.
194+
*
195+
* <p>This method performs the following steps:</p>
196+
* <ol>
197+
* <li><b>Reset:</b> All attributes are reset to {@link Attributes#DEFAULT_ATTRIBUTE_SCORE}</li>
198+
* <li><b>Early Exit:</b> If attributes are disabled via {@link RandomSkillPreferences#isUseAttributes()},
199+
* the method returns immediately</li>
200+
* <li><b>Base Assignment:</b> Attribute scores are calculated by combining:
201+
* <ul>
202+
* <li>Profession-based modifiers from {@link PersonnelRole#getAttributeModifier(SkillAttribute)}</li>
203+
* <li>Phenotype-based modifiers from {@link Phenotype#getAttributeModifier(SkillAttribute)}</li>
204+
* </ul>
205+
* </li>
206+
* <li><b>Randomization:</b> If enabled via {@link RandomSkillPreferences#isRandomizeAttributes()}, each
207+
* attribute receives an additional random adjustment using {@link #performTraitRoll()}, which produces
208+
* values ranging from -2 to +2</li>
209+
* </ol>
210+
*
211+
* <p>All final attribute scores are clamped within the valid range defined by
212+
* {@link Attributes#MINIMUM_ATTRIBUTE_SCORE} and {@link Attributes#MAXIMUM_ATTRIBUTE_SCORE}.</p>
213+
*
214+
* @param person the {@link Person} whose attributes will be generated and assigned
215+
*/
186216
@Override
187217
public void generateAttributes(Person person) {
188218
RandomSkillPreferences skillPreferences = getSkillPreferences();
@@ -216,79 +246,127 @@ public void generateAttributes(Person person) {
216246
person.setAttributeScore(attribute, baseAttributeScore + attributeModifier);
217247

218248
// Attribute randomization
219-
int roll = d6();
220249
if (randomizeAttributes) {
221-
if (roll == 1) {
222-
person.changeAttributeScore(attribute, -1);
223-
} else if (roll == 6) {
224-
person.changeAttributeScore(attribute, 1);
250+
int delta = clamp(performTraitRoll(), MINIMUM_ATTRIBUTE_SCORE, MAXIMUM_ATTRIBUTE_SCORE);
251+
if (delta != 0) {
252+
person.changeAttributeScore(attribute, delta);
225253
}
226254
}
227255
}
228256
}
229257

230258
/**
231-
* Generates traits for the specified person based on random or pre-determined criteria.
259+
* Generates traits for the specified person based on random rolls.
232260
*
233-
* <p>When randomization is enabled, this method calculates and assigns specific traits such as connections,
234-
* reputation, wealth, and bad luck using random rolls. Each trait has its own set of rules for adjustment.</p>
261+
* <p>When randomization is enabled via {@link RandomSkillPreferences#isRandomizeTraits()}, this method
262+
* assigns the following traits using 2d6-based rolls that produce values ranging from -2 to +2:</p>
235263
*
236-
* @param person The person whose traits will be updated. Traits are adjusted based on random rolls when
237-
* randomization is enabled.
264+
* <ul>
265+
* <li><b>Connections</b>: Social network strength (clamped to valid range)</li>
266+
* <li><b>Reputation</b>: Public standing and renown (clamped to valid range)</li>
267+
* <li><b>Wealth</b>: Personal financial resources (clamped to valid range)</li>
268+
* <li><b>Unlucky</b>: Degree of bad fortune (clamped to valid range)</li>
269+
* <li><b>Bloodmark</b>: Clan honor debt (assigned only on rare occasions)</li>
270+
* </ul>
271+
*
272+
* <p><b>Bloodmark Assignment:</b></p>
273+
* <ul>
274+
* <li>Pirates: ~11.11% chance of receiving a bloodmark</li>
275+
* <li>Non-pirates: ~1.11% chance of receiving a bloodmark</li>
276+
* <li>Severity is determined by {@link #performBloodmarkRoll()}, producing values 0-2</li>
277+
* </ul>
278+
*
279+
* <p>If trait randomization is disabled, no traits are modified.</p>
280+
*
281+
* @param person the {@link Person} whose traits will be generated and assigned
282+
*
283+
* @see #performTraitRoll()
284+
* @see #performBloodmarkRoll()
238285
*/
239286
@Override
240287
public void generateTraits(Person person) {
241288
if (!getSkillPreferences().isRandomizeTraits()) {
242289
return;
243290
}
244291

245-
// Connections
246-
if (d6() == 6) {
247-
person.setConnections(1);
248-
} else {
249-
person.setConnections(0);
250-
}
251-
252-
// Reputation
253-
int roll = d6();
254-
if (roll == 6 || roll == 1) {
255-
person.setReputation(roll == 6 ? 1 : -1);
256-
} else {
257-
person.setReputation(0);
258-
}
292+
person.setConnections(clamp(performTraitRoll(), MINIMUM_CONNECTIONS, MAXIMUM_CONNECTIONS));
293+
person.setReputation(clamp(performTraitRoll(), MINIMUM_REPUTATION, MAXIMUM_REPUTATION));
294+
person.setWealth(clamp(performTraitRoll(), MINIMUM_WEALTH, MAXIMUM_WEALTH));
295+
person.setExtraIncomeFromTraitLevel(clamp(performTraitRoll(), MINIMUM_EXTRA_INCOME, MAXIMUM_EXTRA_INCOME));
259296

260-
// Wealth
261-
roll = d6();
262-
if (roll == 6 || roll == 1) {
263-
person.setWealth(roll == 6 ? 1 : -1);
264-
} else {
265-
person.setWealth(0);
297+
int baseUnluckyDiceSize = 5;
298+
int unluckyRoll = randomInt(baseUnluckyDiceSize);
299+
if (unluckyRoll == 0) { // 5% chance of positive value
300+
person.setUnlucky(clamp(performTraitRoll(), MINIMUM_UNLUCKY, MAXIMUM_UNLUCKY));
266301
}
267-
268-
// Extra Income
269-
roll = d6();
270-
if (roll == 6 || roll == 1) {
271-
person.setExtraIncomeFromTraitLevel(roll == 6 ? 1 : -1);
272-
} else {
273-
person.setExtraIncomeFromTraitLevel(0);
302+
// We want the chance of a Bloodmark to be low as it can be quite disruptive
303+
int baseBloodmarkDiceSize = person.getOriginFaction().isPirate() ? 5 : 50;
304+
// pirates = approx 11.11% chance of a bloodmark
305+
// non-pirates = approx 1.11% chance of a bloodmark
306+
int bloodmarkRoll = randomInt(baseBloodmarkDiceSize);
307+
if (bloodmarkRoll == 0) {
308+
person.setBloodmark(clamp(performBloodmarkRoll(), MINIMUM_BLOODMARK, MAXIMUM_BLOODMARK));
274309
}
310+
}
275311

276-
// Unlucky
277-
roll = randomInt(20);
278-
if (roll == 0) {
279-
person.setUnlucky(1);
280-
} else {
281-
person.setUnlucky(0);
282-
}
312+
/**
313+
* Performs a 2d6 roll to determine a trait modifier value.
314+
*
315+
* <p>This method rolls two six-sided dice and converts the result into a trait modifier
316+
* using the following distribution:</p>
317+
* <ul>
318+
* <li><b>2</b>: returns {@code -2} (exceptional negative trait)</li>
319+
* <li><b>3-5</b>: returns {@code -1} (below average trait)</li>
320+
* <li><b>6-8</b>: returns {@code 0} (average trait)</li>
321+
* <li><b>9-11</b>: returns {@code 1} (above average trait)</li>
322+
* <li><b>12</b>: returns {@code 2} (exceptional positive trait)</li>
323+
* </ul>
324+
*
325+
* <p>This creates a bell curve distribution centered on average (0), with exceptional results being rare.</p>
326+
*
327+
* @return a trait modifier value ranging from {@code -2} to {@code 2}
328+
*
329+
* @author Illiani
330+
* @since 0.50.10
331+
*/
332+
private static int performTraitRoll() {
333+
int roll = d6(2);
334+
return switch (roll) {
335+
case 2 -> -2;
336+
case 3, 4, 5 -> -1;
337+
case 9, 10, 11 -> 1;
338+
case 12 -> 2;
339+
default -> 0;
340+
};
341+
}
283342

284-
// Bloodmark
285-
// We want the chance of a Bloodmark to be low as it can be quite disruptive
286-
roll = randomInt(person.getOriginFaction().isPirate() ? 50 : 100);
287-
if (roll == 0) {
288-
person.setBloodmark(1);
289-
} else {
290-
person.setBloodmark(0);
291-
}
343+
/**
344+
* Performs a 2d6 roll to determine a bloodmark severity value.
345+
*
346+
* <p>This method rolls two six-sided dice and converts the result into a bloodmark value
347+
* using the following distribution:</p>
348+
*
349+
* <ul>
350+
* <li><b>2 or 12</b>: returns {@code 2} (~5.56% chance) - severe bloodmark</li>
351+
* <li><b>3-5 or 9-11</b>: returns {@code 1} (~50% chance) - moderate bloodmark</li>
352+
* <li><b>6-8</b>: returns {@code 0} (~44.44% chance) - no bloodmark assigned</li>
353+
* </ul>
354+
*
355+
* <p>This creates a bell curve distribution where most results produce a moderate bloodmark, with severe
356+
* bloodmarks being rare and no bloodmark being moderately common.</p>
357+
*
358+
* @return a bloodmark severity value: {@code 0} (none), {@code 1} (moderate), or {@code 2} (severe)
359+
*
360+
* @author Illiani
361+
* @since 0.50.10
362+
*/
363+
private static int performBloodmarkRoll() {
364+
int roll = d6(2);
365+
return switch (roll) {
366+
case 2, 12 -> 2;
367+
case 3, 4, 5, 9, 10, 11 -> 1;
368+
default -> 0;
369+
};
292370
}
293371

294372
/**

0 commit comments

Comments
 (0)