|
32 | 32 | */ |
33 | 33 | package mekhq.campaign.personnel.generator; |
34 | 34 |
|
| 35 | +import static megamek.codeUtilities.MathUtility.clamp; |
35 | 36 | import static megamek.common.compute.Compute.d6; |
36 | 37 | import static megamek.common.compute.Compute.randomInt; |
| 38 | +import static mekhq.campaign.personnel.Person.*; |
37 | 39 | 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; |
38 | 42 | import static mekhq.campaign.personnel.skills.InfantryGunnerySkills.INFANTRY_GUNNERY_SKILLS; |
39 | 43 | import static mekhq.campaign.personnel.skills.SkillDeprecationTool.DEPRECATED_SKILLS; |
40 | 44 | import static mekhq.campaign.personnel.skills.SkillType.EXP_ELITE; |
|
55 | 59 | import mekhq.campaign.personnel.Person; |
56 | 60 | import mekhq.campaign.personnel.enums.PersonnelRole; |
57 | 61 | import mekhq.campaign.personnel.enums.Phenotype; |
| 62 | +import mekhq.campaign.personnel.skills.Attributes; |
58 | 63 | import mekhq.campaign.personnel.skills.RandomSkillPreferences; |
59 | 64 | import mekhq.campaign.personnel.skills.SkillType; |
60 | 65 | import mekhq.campaign.personnel.skills.Skills; |
@@ -183,6 +188,31 @@ private static void generateCommandUtilitySkills(Person person, int expLvl, |
183 | 188 | } |
184 | 189 | } |
185 | 190 |
|
| 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 | + */ |
186 | 216 | @Override |
187 | 217 | public void generateAttributes(Person person) { |
188 | 218 | RandomSkillPreferences skillPreferences = getSkillPreferences(); |
@@ -216,79 +246,127 @@ public void generateAttributes(Person person) { |
216 | 246 | person.setAttributeScore(attribute, baseAttributeScore + attributeModifier); |
217 | 247 |
|
218 | 248 | // Attribute randomization |
219 | | - int roll = d6(); |
220 | 249 | 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); |
225 | 253 | } |
226 | 254 | } |
227 | 255 | } |
228 | 256 | } |
229 | 257 |
|
230 | 258 | /** |
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. |
232 | 260 | * |
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> |
235 | 263 | * |
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() |
238 | 285 | */ |
239 | 286 | @Override |
240 | 287 | public void generateTraits(Person person) { |
241 | 288 | if (!getSkillPreferences().isRandomizeTraits()) { |
242 | 289 | return; |
243 | 290 | } |
244 | 291 |
|
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)); |
259 | 296 |
|
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)); |
266 | 301 | } |
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)); |
274 | 309 | } |
| 310 | + } |
275 | 311 |
|
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 | + } |
283 | 342 |
|
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 | + }; |
292 | 370 | } |
293 | 371 |
|
294 | 372 | /** |
|
0 commit comments