diff --git a/MekHQ/data/images/misc/skill_check_default.png b/MekHQ/data/images/misc/skill_check_default.png new file mode 100644 index 00000000000..4d463117d41 Binary files /dev/null and b/MekHQ/data/images/misc/skill_check_default.png differ diff --git a/MekHQ/data/images/misc/skill_check_fail.png b/MekHQ/data/images/misc/skill_check_fail.png new file mode 100644 index 00000000000..4d463117d41 Binary files /dev/null and b/MekHQ/data/images/misc/skill_check_fail.png differ diff --git a/MekHQ/data/images/misc/skill_check_pass.png b/MekHQ/data/images/misc/skill_check_pass.png new file mode 100644 index 00000000000..4d463117d41 Binary files /dev/null and b/MekHQ/data/images/misc/skill_check_pass.png differ diff --git a/MekHQ/resources/mekhq/resources/GUI.properties b/MekHQ/resources/mekhq/resources/GUI.properties index daa9dde4489..a4263a95b2d 100644 --- a/MekHQ/resources/mekhq/resources/GUI.properties +++ b/MekHQ/resources/mekhq/resources/GUI.properties @@ -87,6 +87,7 @@ setAsCommander.format=%s has been set as the overall unit commander. enterNewCallsign.text=Enter new callsign editCallsign.text=Edit Callsign changeSalary.text=Change Salary (-1 to remove custom salary) +makeSkillCheck.text=Perform Skill Check changeRank.text=Change Rank changeRankSystem.text=Change Rank System useCampaignRankSystem.text=Use Campaign Rank System diff --git a/MekHQ/resources/mekhq/resources/MarginOfSuccess.properties b/MekHQ/resources/mekhq/resources/MarginOfSuccess.properties index 63dcf2e0a3f..c516a4d2ec6 100644 --- a/MekHQ/resources/mekhq/resources/MarginOfSuccess.properties +++ b/MekHQ/resources/mekhq/resources/MarginOfSuccess.properties @@ -1,10 +1,10 @@ # suppress inspection "UnusedProperty" for the whole file -SPECTACULAR.label=Spectacular! +SPECTACULAR.label=Spectacular! EXTRAORDINARY.label=Extraordinary! -GOOD.label=Good -IT_WILL_DO.label=It'll do... +GOOD.label=Good. +IT_WILL_DO.label=It''ll do... BARELY_MADE_IT.label=Barely made it! ALMOST.label=Almost... -BAD.label=Bad +BAD.label=Bad. TERRIBLE.label=Terrible! -DISASTROUS.label=Disastrous +DISASTROUS.label=Disastrous! diff --git a/MekHQ/resources/mekhq/resources/PersonViewPanel.properties b/MekHQ/resources/mekhq/resources/PersonViewPanel.properties index ed8d6cf9830..d0116a8e874 100644 --- a/MekHQ/resources/mekhq/resources/PersonViewPanel.properties +++ b/MekHQ/resources/mekhq/resources/PersonViewPanel.properties @@ -67,7 +67,6 @@ lblPermanentInjury.text=permanent injury btnMedical.tooltip=View medical details format.itemHeader=%s: format.itemHeader.roleplay=%s: -format.itemHeader.roleplay.removal=\ (RP Only) format.kills=Kills: %d format.killDetail=%s with %s format.scenarios=Scenarios: %d diff --git a/MekHQ/resources/mekhq/resources/SkillCheckDialog.properties b/MekHQ/resources/mekhq/resources/SkillCheckDialog.properties new file mode 100644 index 00000000000..19575ce56e9 --- /dev/null +++ b/MekHQ/resources/mekhq/resources/SkillCheckDialog.properties @@ -0,0 +1,65 @@ +# suppress inspection "UnusedProperty" for the whole file +## Buttons +button.cancel=Cancel +button.attempt=Attempt +button.edge=Use Edge +## Out of Character +message.ooc=Select the skill you want to use and enter any modifiers, then click "Accept."\ +

If Edge is enabled in Campaign Options and the selected character has more than 0 Edge available, you can choose to\ + \ use Edge. This will perform the check as usual, but if the check fails, the character will spend 1 Edge to make a\ + \ second attempt.

\ +

The result of the second attempt will be kept, even if it is worse than the original.

+## Supplemental panel +component.combo=Skill +component.spinner=Modifier +## In Character Variants +message.ic.0=Nothing ventured, nothing gained. Let''s see if luck''s still on our side. +message.ic.1=Nothing ventured, nothing gained. Let''s see if luck''s still on our side. +message.ic.2=Alright, focus up. One shot at this - make it count. +message.ic.3=I''ve pulled off crazier stunts before. Just gotta trust the gut. +message.ic.4=This could go sideways fast... but hey, fortune favors the bold. +message.ic.5=Alright, deep breath. One step at a time. We''ve got this. +message.ic.6=Alright, no guts, no galaxy. Let''s make it happen. +message.ic.7=Steady now... can''t afford any mistakes here. +message.ic.8=I''ve handled worse. Just need a bit of finesse. +message.ic.9=Let''s hope the universe is feeling generous today. +message.ic.10=Alright, no pressure... just gotta make it look easy. +message.ic.11=This is either gonna be genius or a total disaster. Here goes nothing. +message.ic.12=Can''t second-guess it now. Just commit and push through. +message.ic.13=If this works, I''m buying myself a drink. If not... well, I won''t worry about it. +message.ic.14=Eyes forward, stay calm. Just one more move. +message.ic.15=Alright, let''s do this. No point in overthinking it now. +message.ic.16=I''ve come too far to mess this up. Just gotta stay sharp. +message.ic.17=Luck''s gotta turn around sometime. Might as well be now. +message.ic.18=If this doesn''t work, we''ll just pretend it was the plan all along. +message.ic.19=Deep breath. Steady hands. No room for mistakes. +message.ic.20=Alright, here goes nothing. Hope the universe likes a risk-taker. +message.ic.21=I swear, if this works, I''m never letting anyone forget it. +message.ic.22=Sometimes you just gotta trust your gut and hope it''s not lying. +message.ic.23=If I pull this off, it''s gonna be one for the stories. +message.ic.24=No point worrying now. Just gotta see it through. +message.ic.25=Well, no sense in standing around. Let''s see what happens. +message.ic.26=It''s a long shot, but I''ve pulled off crazier moves. +message.ic.27=Can''t win ''em all, but I''ll be damned if I don''t give it a shot. +message.ic.28=If this works, I''m calling it skill. If it doesn''t... well, we''ll cross that bridge. +message.ic.29=Alright, let''s see if the stars are on my side today. +message.ic.30=Alright, let''s see if I''ve still got that lucky touch. +message.ic.31=Can''t back down now. One way or another, we''re seeing this through. +message.ic.32=No time to second-guess it. Just gotta go with the flow. +message.ic.33=I''ve got a feeling about this... let''s hope it''s a good one. +message.ic.34=Alright, universe. Show me what you''ve got. +message.ic.35=Alright, no turning back now. Let''s see how this plays out. +message.ic.36=Sometimes you just gotta throw caution to the wind and hope it sticks. +message.ic.37=If this doesn''t work, at least I''ll have a good story to tell. +message.ic.38=Here goes nothing... or everything. No in-between. +message.ic.39=Just gotta trust myself on this one. No room for doubt. +message.ic.40=Alright, no sense worrying now. Just gotta make it happen. +message.ic.41=Alright, time to see if I can pull a miracle out of thin air. +message.ic.42=No plan survives first contact... but let''s give it our best shot. +message.ic.43=If this doesn''t work, I''m blaming the universe. Just saying. +message.ic.44=One shot, one chance. Here goes everything. +message.ic.45=Sometimes you just gotta trust your instincts and hope for the best. +message.ic.46=If I get through this, I''m calling it pure skill. If not... well, oops. +message.ic.47=Here''s hoping I don''t make a complete fool of myself. +message.ic.48=You know what? I''ve survived worse. Let''s give it a shot. +message.ic.49=Whatever happens, I''m giving it everything I''ve got. diff --git a/MekHQ/resources/mekhq/resources/SkillCheckUtility.properties b/MekHQ/resources/mekhq/resources/SkillCheckUtility.properties index 20745993ee2..5de850fb0d7 100644 --- a/MekHQ/resources/mekhq/resources/SkillCheckUtility.properties +++ b/MekHQ/resources/mekhq/resources/SkillCheckUtility.properties @@ -1,4 +1,7 @@ -skillCheck.results={0} {1}failed{2} {3} {4} check with a roll of {5} vs. a target number of {6}. {7}.{8} -skillCheck.rerolled=\ {0} used a point of Edge when making this check. +# suppress inspection "UnusedProperty" for the whole file +skillCheck.results={0} {1}{2}{3} {4} {5} check with a roll of {6} vs. a target number of {7}. +skillCheck.results.success=Passed +skillCheck.results.failure=Failed +skillCheck.rerolled={0} used a point of Edge when making this check. skillCheck.nullSkillName=ERROR: Skill name is null. Please report this bug to the MegaMek team. skillCheck.nullPerson=ERROR: Person is null. Please report this bug to the MegaMek team. diff --git a/MekHQ/src/mekhq/campaign/personnel/skills/SkillCheckUtility.java b/MekHQ/src/mekhq/campaign/personnel/skills/SkillCheckUtility.java index df035ea52dc..11e64a5b347 100644 --- a/MekHQ/src/mekhq/campaign/personnel/skills/SkillCheckUtility.java +++ b/MekHQ/src/mekhq/campaign/personnel/skills/SkillCheckUtility.java @@ -36,6 +36,7 @@ import static mekhq.campaign.personnel.skills.Skill.COUNT_UP_MAX_VALUE; import static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.BARELY_MADE_IT; import static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.DISASTROUS; +import static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.getMarginOfSuccessObjectFromMarginValue; import static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.getMarginOfSuccessString; import static mekhq.campaign.personnel.skills.enums.MarginOfSuccess.getMarginValue; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; @@ -46,6 +47,7 @@ import megamek.logging.MMLogger; import mekhq.MekHQ; +import mekhq.campaign.event.PersonChangedEvent; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.skills.enums.MarginOfSuccess; import mekhq.campaign.personnel.skills.enums.SkillAttribute; @@ -77,6 +79,7 @@ public class SkillCheckUtility { private int marginOfSuccess; private String resultsText; private int targetNumber; + boolean isCountUp; private int roll; private boolean usedEdge; @@ -108,7 +111,9 @@ public SkillCheckUtility(final Person person, final String skillName, final int return; } - targetNumber = determineTargetNumber(person, skillName, miscModifier); + final SkillType skillType = SkillType.getType(skillName); + isCountUp = skillType.isCountUp(); + targetNumber = determineTargetNumber(person, skillType, miscModifier); performCheck(useEdge); } @@ -198,7 +203,6 @@ private String generateResultsText() { String fullTitle = person.getHyperlinkedFullTitle(); String firstName = person.getFirstName(); String genderedReferenced = HIS_HER_THEIR.getDescriptor(person.getGender()); - String marginOfSuccessText = getMarginOfSuccessString(marginOfSuccess); String colorOpen; int neutralMarginValue = getMarginValue(BARELY_MADE_IT); @@ -209,22 +213,29 @@ private String generateResultsText() { } else { colorOpen = spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorPositiveHexColor()); } - - String edgeUseText = !usedEdge ? "" : getFormattedTextAt(RESOURCE_BUNDLE, "skillCheck.rerolled", firstName); - - return getFormattedTextAt(RESOURCE_BUNDLE, + String status = getFormattedTextAt(RESOURCE_BUNDLE, + "skillCheck.results." + (isSuccess() ? "success" : "failure")); + String mainMessage = getFormattedTextAt(RESOURCE_BUNDLE, "skillCheck.results", fullTitle, colorOpen, - skillName, - colorOpen, + status, CLOSING_SPAN_TAG, genderedReferenced, skillName, roll, - targetNumber, - marginOfSuccessText, - edgeUseText); + targetNumber); + + String edgeUseText = !usedEdge ? "" : getFormattedTextAt(RESOURCE_BUNDLE, "skillCheck.rerolled", firstName); + + if (!edgeUseText.isBlank()) { + mainMessage = mainMessage + "

" + edgeUseText + "

"; + } + + MarginOfSuccess marginOfSuccessObject = getMarginOfSuccessObjectFromMarginValue(marginOfSuccess); + String marginOfSuccessText = getMarginOfSuccessString(marginOfSuccessObject); + + return mainMessage + "

" + marginOfSuccessText + "

"; } /** @@ -330,7 +341,7 @@ public boolean isUsedEdge() { * linked attributes. Otherwise, it is based on the final skill value and attribute modifiers.

* * @param person the {@link Person} performing the skill check - * @param skillName the name of the skill being used + * @param skillType the associated {@link SkillType} for the {@link Skill} being used. * @param miscModifier any special modifiers, as an {@link Integer}. These values are subtracted from the target * number, if the associated skill is classified as 'count up', otherwise they are added to the * target number. This means negative values are bonuses, positive values are penalties. @@ -340,8 +351,8 @@ public boolean isUsedEdge() { * @author Illiani * @since 0.50.5 */ - static int determineTargetNumber(Person person, String skillName, int miscModifier) { - final SkillType skillType = SkillType.getType(skillName); + public static int determineTargetNumber(Person person, SkillType skillType, int miscModifier) { + final String skillName = skillType.getName(); final Attributes characterAttributes = person.getATOWAttributes(); boolean isUntrained = !person.hasSkill(skillName); @@ -419,7 +430,15 @@ boolean performInitialRoll(boolean useEdge) { int availableEdge = person.getCurrentEdge(); if (roll >= targetNumber || !useEdge || availableEdge < 1) { - marginOfSuccess = MarginOfSuccess.getMarginOfSuccess(roll); + int difference = isCountUp ? targetNumber - roll : roll - targetNumber; + + logger.info("Initial roll for skill check {}: {} vs {}. Margin of success: {}.", + skillName, + roll, + targetNumber, + difference); + + marginOfSuccess = MarginOfSuccess.getMarginOfSuccess(difference); resultsText = generateResultsText(); return true; } @@ -439,9 +458,10 @@ boolean performInitialRoll(boolean useEdge) { */ private void rollWithEdge() { person.changeCurrentEdge(-1); + MekHQ.triggerEvent(new PersonChangedEvent(person)); usedEdge = true; - marginOfSuccess = MarginOfSuccess.getMarginOfSuccess(roll); + marginOfSuccess = MarginOfSuccess.getMarginOfSuccess(roll - targetNumber); resultsText = generateResultsText(); } diff --git a/MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java b/MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java index a4b48ad35f8..fa33dbae99e 100644 --- a/MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java +++ b/MekHQ/src/mekhq/campaign/personnel/skills/SkillType.java @@ -84,6 +84,14 @@ public class SkillType { private static final MMLogger logger = MMLogger.create(SkillType.class); + /** + * A constant string value representing the suffix " (RP Only)". + * + *

Usage: This is used to denote a skill that has no mechanical benefits. This tag should be + * progressively removed as mechanics are expanded to use these skills.

+ */ + public static final String RP_ONLY_TAG = " (RP Only)"; + // combat skills public static final String S_PILOT_MEK = "Piloting/Mek"; public static final String S_PILOT_AERO = "Piloting/Aerospace"; @@ -121,46 +129,46 @@ public class SkillType { public static final String S_TACTICS = "Tactics"; // roleplay skills - public static final String S_ACROBATICS = "Acrobatics (RP Only)"; - public static final String S_ACTING = "Acting (RP Only)"; - public static final String S_ANIMAL_HANDLING = "Animal Handling (RP Only)"; - public static final String S_APPRAISAL = "Appraisal (RP Only)"; - public static final String S_ARCHERY = "Archery (RP Only)"; - public static final String S_ART_DANCING = "Art/Dancing (RP Only)"; - public static final String S_ART_DRAWING = "Art/Drawing (RP Only)"; - public static final String S_ART_PAINTING = "Art/Painting (RP Only)"; - public static final String S_ART_WRITING = "Art/Writing (RP Only)"; - public static final String S_CLIMBING = "Climbing (RP Only)"; - public static final String S_COMMUNICATIONS = "Communications (RP Only)"; - public static final String S_COMPUTERS = "Computers (RP Only)"; - public static final String S_CRYPTOGRAPHY = "Cryptography (RP Only)"; - public static final String S_DEMOLITIONS = "Demolitions (RP Only)"; - public static final String S_DISGUISE = "Disguise (RP Only)"; - public static final String S_ESCAPE_ARTIST = "Escape Artist (RP Only)"; - public static final String S_FORGERY = "Forgery (RP Only)"; - public static final String S_INTEREST_HISTORY = "Interest/History (RP Only)"; - public static final String S_INTEREST_LITERATURE = "Interest/Literature (RP Only)"; - public static final String S_INTEREST_HOLO_GAMES = "Interest/Holo-Games (RP Only)"; - public static final String S_INTEREST_SPORTS = "Interest/Sports (RP Only)"; - public static final String S_INTERROGATION = "Interrogation (RP Only)"; - public static final String S_INVESTIGATION = "Investigation (RP Only)"; - public static final String S_LANGUAGES = "Languages (RP Only)"; - public static final String S_MARTIAL_ARTS = "Martial Arts (RP Only)"; - public static final String S_PERCEPTION = "Perception (RP Only)"; - public static final String S_SLEIGHT_OF_HAND = "Sleight of Hand (RP Only)"; - public static final String S_PROTOCOLS = "Protocols (RP Only)"; - public static final String S_SCIENCE_BIOLOGY = "Science/Biology (RP Only)"; - public static final String S_SCIENCE_CHEMISTRY = "Science/Chemistry (RP Only)"; - public static final String S_SCIENCE_MATHEMATICS = "Science/Mathematics (RP Only)"; - public static final String S_SCIENCE_PHYSICS = "Science/Physics (RP Only)"; - public static final String S_SECURITY_SYSTEMS_ELECTRONIC = "Security Systems/Electronic (RP Only)"; - public static final String S_SCIENCE_SYSTEMS_MECHANICAL = "Security Systems/Mechanical (RP Only)"; - public static final String S_SENSOR_OPERATIONS = "Sensor Operations (RP Only)"; - public static final String S_STEALTH = "Stealth (RP Only)"; - public static final String S_STREETWISE = "Streetwise (RP Only)"; - public static final String S_SURVIVAL = "Survival (RP Only)"; - public static final String S_TRACKING = "Tracking (RP Only)"; - public static final String S_TRAINING = "Training (RP Only)"; + public static final String S_ACROBATICS = "Acrobatics" + RP_ONLY_TAG; + public static final String S_ACTING = "Acting" + RP_ONLY_TAG; + public static final String S_ANIMAL_HANDLING = "Animal Handling" + RP_ONLY_TAG; + public static final String S_APPRAISAL = "Appraisal" + RP_ONLY_TAG; + public static final String S_ARCHERY = "Archery" + RP_ONLY_TAG; + public static final String S_ART_DANCING = "Art/Dancing" + RP_ONLY_TAG; + public static final String S_ART_DRAWING = "Art/Drawing" + RP_ONLY_TAG; + public static final String S_ART_PAINTING = "Art/Painting" + RP_ONLY_TAG; + public static final String S_ART_WRITING = "Art/Writing" + RP_ONLY_TAG; + public static final String S_CLIMBING = "Climbing" + RP_ONLY_TAG; + public static final String S_COMMUNICATIONS = "Communications" + RP_ONLY_TAG; + public static final String S_COMPUTERS = "Computers" + RP_ONLY_TAG; + public static final String S_CRYPTOGRAPHY = "Cryptography" + RP_ONLY_TAG; + public static final String S_DEMOLITIONS = "Demolitions" + RP_ONLY_TAG; + public static final String S_DISGUISE = "Disguise" + RP_ONLY_TAG; + public static final String S_ESCAPE_ARTIST = "Escape Artist" + RP_ONLY_TAG; + public static final String S_FORGERY = "Forgery" + RP_ONLY_TAG; + public static final String S_INTEREST_HISTORY = "Interest/History" + RP_ONLY_TAG; + public static final String S_INTEREST_LITERATURE = "Interest/Literature" + RP_ONLY_TAG; + public static final String S_INTEREST_HOLO_GAMES = "Interest/Holo-Games" + RP_ONLY_TAG; + public static final String S_INTEREST_SPORTS = "Interest/Sports" + RP_ONLY_TAG; + public static final String S_INTERROGATION = "Interrogation" + RP_ONLY_TAG; + public static final String S_INVESTIGATION = "Investigation" + RP_ONLY_TAG; + public static final String S_LANGUAGES = "Languages" + RP_ONLY_TAG; + public static final String S_MARTIAL_ARTS = "Martial Arts" + RP_ONLY_TAG; + public static final String S_PERCEPTION = "Perception" + RP_ONLY_TAG; + public static final String S_SLEIGHT_OF_HAND = "Sleight of Hand" + RP_ONLY_TAG; + public static final String S_PROTOCOLS = "Protocols" + RP_ONLY_TAG; + public static final String S_SCIENCE_BIOLOGY = "Science/Biology" + RP_ONLY_TAG; + public static final String S_SCIENCE_CHEMISTRY = "Science/Chemistry" + RP_ONLY_TAG; + public static final String S_SCIENCE_MATHEMATICS = "Science/Mathematics" + RP_ONLY_TAG; + public static final String S_SCIENCE_PHYSICS = "Science/Physics" + RP_ONLY_TAG; + public static final String S_SECURITY_SYSTEMS_ELECTRONIC = "Security Systems/Electronic" + RP_ONLY_TAG; + public static final String S_SCIENCE_SYSTEMS_MECHANICAL = "Security Systems/Mechanical" + RP_ONLY_TAG; + public static final String S_SENSOR_OPERATIONS = "Sensor Operations" + RP_ONLY_TAG; + public static final String S_STEALTH = "Stealth" + RP_ONLY_TAG; + public static final String S_STREETWISE = "Streetwise" + RP_ONLY_TAG; + public static final String S_SURVIVAL = "Survival" + RP_ONLY_TAG; + public static final String S_TRACKING = "Tracking" + RP_ONLY_TAG; + public static final String S_TRAINING = "Training" + RP_ONLY_TAG; public static final int NUM_LEVELS = 11; @@ -378,10 +386,10 @@ public SkillType() { * *

For example:

*
-     *                                                                                                                                                                                                                                                                                     Integer[] costs = new Integer[] {8, 4, 4, 4, 4, 4, 4, 4, 4, -1, -1};
-     *                                                                                                                                                                                                                                                                                     SkillType skillType = new SkillType("Example Skill", 7, false, SkillSubType.COMBAT,
-     *                                                                                                                                                                                                                                                                                            SkillAttribute.DEXTERITY, SkillAttribute.INTELLIGENCE, 1, 3, 4, 5, costs);
-     *                                                                                                                                                                                                                                                                                     
+ * Integer[] costs = new Integer[] {8, 4, 4, 4, 4, 4, 4, 4, 4, -1, -1}; + * SkillType skillType = new SkillType("Example Skill", 7, false, SkillSubType.COMBAT, + * SkillAttribute.DEXTERITY, SkillAttribute.INTELLIGENCE, 1, 3, 4, 5, costs); + * * * @author Illiani * @since 0.50.05 diff --git a/MekHQ/src/mekhq/campaign/personnel/skills/enums/MarginOfSuccess.java b/MekHQ/src/mekhq/campaign/personnel/skills/enums/MarginOfSuccess.java index e1ebc5219b5..984874d2549 100644 --- a/MekHQ/src/mekhq/campaign/personnel/skills/enums/MarginOfSuccess.java +++ b/MekHQ/src/mekhq/campaign/personnel/skills/enums/MarginOfSuccess.java @@ -49,7 +49,7 @@ * while {@link #DISASTROUS} represents a margin of success in the range of {@link Integer#MIN_VALUE} to -7.

* * @author Illiani - * @since 0.50.5 + * @since 0.50.05 */ public enum MarginOfSuccess { SPECTACULAR(7, Integer.MAX_VALUE, 4), @@ -77,7 +77,7 @@ public enum MarginOfSuccess { * @param margin the margin value associated with this range * * @author Illiani - * @since 0.50.5 + * @since 0.50.05 */ MarginOfSuccess(int lowerBound, int upperBound, int margin) { this.lowerBound = lowerBound; @@ -96,36 +96,85 @@ public enum MarginOfSuccess { * @return the margin value associated with the given {@link MarginOfSuccess} * * @author Illiani - * @since 0.50.5 + * @since 0.50.05 */ public static int getMarginValue(MarginOfSuccess marginOfSuccess) { return marginOfSuccess.margin; } /** - * Retrieves the margin of success for the specified roll value. + * Determines the margin of success as an integer based on the difference between the roll and the target. * - *

This method matches the provided roll value against the bounds of each {@link MarginOfSuccess} constant to - * determine the appropriate range and calculate the roll's margin.

+ *

This method calculates the margin of success using the given difference and returns the associated + * margin as an integer. Internally, it utilizes {@link #getMarginOfSuccessObject(int)} to determine the relevant + * margin category.

* - * @param roll the roll value to evaluate + * @param differenceBetweenRollAndTarget The difference between the roll result and the target value. * - * @return the margin (calculated as {@code roll - lowerBound}) corresponding to the matching - * {@link MarginOfSuccess} range, or the margin for {@link #DISASTROUS} if no matching range is found + * @return The margin of success as an integer. * * @author Illiani - * @since 0.50.5 + * @since 0.50.05 */ - public static int getMarginOfSuccess(int roll) { - for (MarginOfSuccess mos : MarginOfSuccess.values()) { - if (roll >= mos.lowerBound && roll <= mos.upperBound) { - return roll - mos.lowerBound; + public static int getMarginOfSuccess(int differenceBetweenRollAndTarget) { + return getMarginOfSuccessObject(differenceBetweenRollAndTarget).margin; + } + + /** + * Determines the {@link MarginOfSuccess} category based on the difference between the roll and the target. + * + *

This method iterates through all possible {@link MarginOfSuccess} values and compares the provided + * difference to their defined bounds ({@code lowerBound} and {@code upperBound}). If a matching range is found, it + * returns the corresponding {@link MarginOfSuccess} object.

+ * + *

If no matching category is found, an error message is logged, and the method returns the + * {@link MarginOfSuccess#DISASTROUS} category as a fallback.

+ * + * @param differenceBetweenRollAndTarget The difference between the roll result and the target value. + * + * @return The {@link MarginOfSuccess} object that corresponds to the provided difference. + * + * @author Illiani + * @since 0.50.05 + */ + public static MarginOfSuccess getMarginOfSuccessObject(int differenceBetweenRollAndTarget) { + for (MarginOfSuccess margin : MarginOfSuccess.values()) { + if ((differenceBetweenRollAndTarget >= margin.lowerBound) && + (differenceBetweenRollAndTarget <= margin.upperBound)) { + return margin; } } + logger.error("No valid MarginOfSuccess found for roll: {}. Returning DISASTROUS", + differenceBetweenRollAndTarget); + return DISASTROUS; + } - logger.error("Unknown MarginOfSuccess value: {} - returning {}.", roll, DISASTROUS); + /** + * Retrieves the {@link MarginOfSuccess} object corresponding to the specified margin value. + * + *

This method iterates through all possible {@link MarginOfSuccess} values and returns the one + * whose associated margin matches the provided {@code marginValue}.

+ * + *

If no matching {@link MarginOfSuccess} is found, an error is logged, and the method + * defaults to returning {@link MarginOfSuccess#DISASTROUS}.

+ * + * @param marginValue The integer margin value to look up. + * + * @return The {@link MarginOfSuccess} object corresponding to the given margin value, or + * {@link MarginOfSuccess#DISASTROUS} if no match is found. + * + * @author Illiani + * @since 0.50.05 + */ + public static MarginOfSuccess getMarginOfSuccessObjectFromMarginValue(int marginValue) { + for (MarginOfSuccess marginOfSuccess : MarginOfSuccess.values()) { + if (marginOfSuccess.margin == marginValue) { + return marginOfSuccess; + } + } - return DISASTROUS.margin; + logger.error("No valid MarginOfSuccess found for marginValue: {}. Returning DISASTROUS", marginValue); + return DISASTROUS; } /** @@ -139,9 +188,9 @@ public static int getMarginOfSuccess(int roll) { * @return the localized string representing the given margin of success * * @author Illiani - * @since 0.50.5 + * @since 0.50.05 */ - public static String getMarginOfSuccessString(int marginOfSuccess) { + public static String getMarginOfSuccessString(MarginOfSuccess marginOfSuccess) { return getFormattedTextAt(RESOURCE_BUNDLE, marginOfSuccess + ".label"); } } diff --git a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java index 5e02956fd21..1250e8dd7ca 100644 --- a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java @@ -46,6 +46,7 @@ import static mekhq.campaign.personnel.enums.education.EducationLevel.DOCTORATE; import static mekhq.campaign.personnel.skills.Attributes.ATTRIBUTE_IMPROVEMENT_COST; import static mekhq.campaign.personnel.skills.Attributes.MAXIMUM_ATTRIBUTE_SCORE; +import static mekhq.campaign.personnel.skills.Attributes.MINIMUM_ATTRIBUTE_SCORE; import static mekhq.campaign.personnel.skills.SkillType.S_DOCTOR; import static mekhq.campaign.randomEvents.personalities.PersonalityController.writePersonalityDescription; import static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.processAdHocExecution; @@ -144,6 +145,7 @@ public class PersonnelTableMouseAdapter extends JPopupMenuAdapter { private static final MMLogger logger = MMLogger.create(PersonnelTableMouseAdapter.class); // region Variable Declarations + private static final String CMD_SKILL_CHECK = "SKILL_CHECK"; private static final String CMD_RANKSYSTEM = "RANKSYSTEM"; private static final String CMD_RANK = "RANK"; private static final String CMD_MANEI_DOMINI_RANK = "MD_RANK"; @@ -325,6 +327,12 @@ public void actionPerformed(ActionEvent action) { String[] data = action.getActionCommand().split(SEPARATOR, -1); switch (data[0]) { + case CMD_SKILL_CHECK: { + for (final Person person : people) { + new SkillCheckDialog(getCampaign(), person); + } + break; + } case CMD_RANKSYSTEM: { final RankSystem rankSystem = Ranks.getRankSystemFromCode(data[1]); final RankValidator rankValidator = new RankValidator(); @@ -640,11 +648,11 @@ public void actionPerformed(ActionEvent action) { true, resources.getString("spendOnAttributes.score"), selectedPerson.getAttributeScore(attribute), - 0); + MINIMUM_ATTRIBUTE_SCORE); choiceDialog.setVisible(true); int choice = choiceDialog.getValue(); - if (choice <= 0) { + if (choice < 0) { // <0 indicates Cancellation return; } @@ -1664,6 +1672,11 @@ protected Optional createPopupMenu() { Person[] selected = getSelectedPeople(); // lets fill the pop up menu + menuItem = new JMenuItem(resources.getString("makeSkillCheck.text")); + menuItem.setActionCommand(makeCommand(CMD_SKILL_CHECK)); + menuItem.addActionListener(this); + popup.add(menuItem); + if (StaticChecks.areAllEligible(true, selected)) { menu = new JMenu(resources.getString("changeRank.text")); final Profession initialProfession = Profession.getProfessionFromPersonnelRole(person.getPrimaryRole()); diff --git a/MekHQ/src/mekhq/gui/baseComponents/immersiveDialogs/ImmersiveDialogCore.java b/MekHQ/src/mekhq/gui/baseComponents/immersiveDialogs/ImmersiveDialogCore.java index 65630c707ec..a111ba4ee3b 100644 --- a/MekHQ/src/mekhq/gui/baseComponents/immersiveDialogs/ImmersiveDialogCore.java +++ b/MekHQ/src/mekhq/gui/baseComponents/immersiveDialogs/ImmersiveDialogCore.java @@ -56,6 +56,7 @@ import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkEvent.EventType; +import megamek.client.ui.baseComponents.MMComboBox; import megamek.common.annotations.Nullable; import megamek.common.icons.Portrait; import megamek.logging.MMLogger; @@ -96,6 +97,8 @@ public class ImmersiveDialogCore extends JDialog { private JSpinner spinner; private int spinnerValue; + private MMComboBox comboBox; // can be null + private int comboBoxChoiceIndex; private int dialogChoice = 0; @@ -116,18 +119,61 @@ public int getDialogChoice() { return dialogChoice; } + /** + * Sets the dialog choice for the current object. + * + * @param dialogChoice The integer value representing the dialog choice to set. + */ public void setDialogChoice(int dialogChoice) { this.dialogChoice = dialogChoice; } + /** + * Retrieves the current value of the spinner. + * + *

Note: will return 0 if the dialog does not contain a {@link JSpinner} in the supplemental panel.

+ * + * @return The integer value of the spinner. + */ public int getSpinnerValue() { return spinnerValue; } + /** + * Sets a new value for the spinner. + * + *

Note: will return 0 if the dialog does not contain a {@link MMComboBox} in the supplemental panel.

+ * + * @param spinnerValue The integer value to set for the spinner. + */ public void setSpinnerValue(int spinnerValue) { this.spinnerValue = spinnerValue; } + + /** + * Retrieves the current index of the combo box choice. + * + * @return The integer value representing the current selected index of the combo box. + */ + public int getComboBoxChoiceIndex() { + return comboBoxChoiceIndex; + } + + /** + * Sets a new index for the combo box choice. + * + * @param comboBoxChoiceIndex The integer value to set as the combo box's selected index. + */ + public void setComboBoxChoiceIndex(int comboBoxChoiceIndex) { + this.comboBoxChoiceIndex = comboBoxChoiceIndex; + } + + /** + * Retrieves the padding value defined in this object. + * + * @return The padding value as an integer. + */ protected int getPADDING() { return PADDING; } @@ -152,18 +198,17 @@ protected int getPADDING() { * @param centerWidth An optional width for the center panel; uses the default value if {@code null}. * @param isVerticalLayout A {@code boolean} determining the button layout: {@code true} for vertical stacking, * {@code false} for horizontal layout. - * @param spinnerPanel An optional {@link JPanel} containing a spinner widget to be displayed in the center - * panel; use {@code null} if not applicable. + * @param supplementalPanel An optional {@link JPanel} containing a {@link JSpinner} and/or a {@link MMComboBox} + * to be displayed in the center panel; use {@code null} if not applicable. */ public ImmersiveDialogCore(Campaign campaign, @Nullable Person leftSpeaker, @Nullable Person rightSpeaker, String centerMessage, List buttons, @Nullable String outOfCharacterMessage, - @Nullable Integer centerWidth, boolean isVerticalLayout, @Nullable JPanel spinnerPanel, + @Nullable Integer centerWidth, boolean isVerticalLayout, @Nullable JPanel supplementalPanel, @Nullable ImageIcon imageIcon, boolean isModal) { // Initialize this.campaign = campaign; this.leftSpeaker = leftSpeaker; this.rightSpeaker = rightSpeaker; - spinner = new JSpinner(); CENTER_WIDTH = (centerWidth != null) ? centerWidth : CENTER_WIDTH; @@ -193,7 +238,7 @@ public ImmersiveDialogCore(Campaign campaign, @Nullable Person leftSpeaker, @Nul } // Center box for the message - JPanel pnlCenter = createCenterBox(centerMessage, buttons, isVerticalLayout, spinnerPanel, imageIcon); + JPanel pnlCenter = createCenterBox(centerMessage, buttons, isVerticalLayout, supplementalPanel, imageIcon); constraints.gridx = gridx; constraints.gridy = 0; constraints.weightx = 2; @@ -265,11 +310,11 @@ protected void setTitle() { * @return A {@link JPanel} with the message displayed in the center and buttons at the bottom. */ private JPanel createCenterBox(String centerMessage, List buttons, boolean isVerticalLayout, - @Nullable JPanel spinnerPanel, @Nullable ImageIcon imageIcon) { + @Nullable JPanel supplementalPanel, @Nullable ImageIcon imageIcon) { northPanel = new JPanel(new BorderLayout()); // Buttons panel - JPanel buttonPanel = populateButtonPanel(buttons, isVerticalLayout, spinnerPanel); + JPanel buttonPanel = populateButtonPanel(buttons, isVerticalLayout, supplementalPanel); // Create a JEditorPane for the center message JEditorPane editorPane = new JEditorPane(); @@ -303,15 +348,6 @@ private JPanel createCenterBox(String centerMessage, List CENTER_WIDTH) { - imageIcon = scaleImageIcon(imageIcon, CENTER_WIDTH, true); - } - - int heightLimit = max(1, CENTER_WIDTH / 3); // I went with 3 because that provided the best feel - if (imageIcon.getIconHeight() > heightLimit) { - imageIcon = scaleImageIcon(imageIcon, heightLimit, false); - } - imageLabel.setIcon(imageIcon); imageLabel.setHorizontalAlignment(SwingConstants.CENTER); } @@ -442,7 +478,7 @@ protected void hyperlinkEventListenerActions(HyperlinkEvent evt) { * {@code false} for horizontal arrangement. */ protected JPanel populateButtonPanel(List buttons, boolean isVerticalLayout, - @Nullable JPanel spinnerPanel) { + @Nullable JPanel supplementalPanel) { final int padding = getPADDING(); // Main container panel to hold the spinner and button panel @@ -450,9 +486,10 @@ protected JPanel populateButtonPanel(List buttons, boole containerPanel.setLayout(new BorderLayout(padding, padding)); // Add the spinner panel to the top of the container - if (spinnerPanel != null) { - containerPanel.add(spinnerPanel, BorderLayout.NORTH); - fetchSpinnerFromPanel(spinnerPanel); + if (supplementalPanel != null) { + containerPanel.add(supplementalPanel, BorderLayout.NORTH); + fetchSpinnerFromPanel(supplementalPanel); + fetchComboBoxFromPanel(supplementalPanel); } // Create button panel @@ -517,7 +554,15 @@ protected JPanel populateButtonPanel(List buttons, boole // Add action listener button.addActionListener(evt -> { setDialogChoice(buttons.indexOf(buttonStrings)); - setSpinnerValue((int) spinner.getValue()); + + if (spinner != null) { + setSpinnerValue((int) spinner.getValue()); + } + + if (comboBox != null) { + setComboBoxChoiceIndex(comboBox.getSelectedIndex()); + } + dispose(); }); @@ -572,21 +617,22 @@ protected JPanel populateButtonPanel(List buttons, boole * fallback. *

* - * @param spinnerPanel The {@link JPanel} to search for a {@link JSpinner}. Must not be {@code null}. - * - * @return The {@link JSpinner} instance found in the panel; if no {@link JSpinner} is found, a new, default - * {@link JSpinner} is returned. + * @param supplementalPanel The {@link JPanel} to search for a {@link JSpinner}. Must not be {@code null}. */ - private JSpinner fetchSpinnerFromPanel(JPanel spinnerPanel) { - for (Component component : spinnerPanel.getComponents()) { + private void fetchSpinnerFromPanel(JPanel supplementalPanel) { + for (Component component : supplementalPanel.getComponents()) { if (component instanceof JSpinner) { spinner = (JSpinner) component; } } + } - // Return an empty JSpinner if one isn't found and log the error - logger.error("No JSpinner found in the provided panel."); - return new JSpinner(); + private void fetchComboBoxFromPanel(JPanel supplementalPanel) { + for (Component component : supplementalPanel.getComponents()) { + if (component instanceof MMComboBox) { + comboBox = (MMComboBox) component; + } + } } diff --git a/MekHQ/src/mekhq/gui/dialog/SkillCheckDialog.java b/MekHQ/src/mekhq/gui/dialog/SkillCheckDialog.java new file mode 100644 index 00000000000..c721314748d --- /dev/null +++ b/MekHQ/src/mekhq/gui/dialog/SkillCheckDialog.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2025 The MegaMek Team. All Rights Reserved. + * + * This file is part of MekHQ. + * + * MekHQ is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License (GPL), + * version 3 or (at your option) any later version, + * as published by the Free Software Foundation. + * + * MekHQ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * A copy of the GPL should have been included with this project; + * if not, see . + * + * NOTICE: The MegaMek organization is a non-profit group of volunteers + * creating free software for the BattleTech community. + * + * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks + * of The Topps Company, Inc. All Rights Reserved. + * + * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of + * InMediaRes Productions, LLC. + */ +package mekhq.gui.dialog; + +import static megamek.common.Compute.randomInt; +import static mekhq.campaign.personnel.skills.SkillCheckUtility.determineTargetNumber; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; + +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.util.ArrayList; +import java.util.List; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; + +import megamek.client.ui.baseComponents.MMComboBox; +import mekhq.campaign.Campaign; +import mekhq.campaign.personnel.Person; +import mekhq.campaign.personnel.skills.SkillCheckUtility; +import mekhq.campaign.personnel.skills.SkillType; +import mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore; +import mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogCore.ButtonLabelTooltipPair; +import mekhq.gui.baseComponents.immersiveDialogs.ImmersiveDialogSimple; + +/** + * A dialog that facilitates skill checks for a character. + * + *

This dialog allows the user to perform skill checks for a specific skill by selecting the skill, applying + * modifiers, and choosing whether to use Edge. It consists of an initial dialog to gather input, executes the skill + * check, and then presents the result in a results dialog.

+ * + * @author Illiani + * @since 0.50.05 + */ +public class SkillCheckDialog { + final String RESOURCE_BUNDLE = "mekhq.resources." + SkillCheckDialog.class.getSimpleName(); + + final String DIALOG_IMAGE_FILENAME_DEFAULT = "data/images/misc/skill_check_default.png"; + final String DIALOG_IMAGE_FILENAME_PASS = "data/images/misc/skill_check_pass.png"; + final String DIALOG_IMAGE_FILENAME_FAIL = "data/images/misc/skill_check_fail.png"; + + final int DIALOG_CANCEL_INDEX = 0; + final int DIALOG_USE_EDGE_INDEX = 2; + + private final Campaign campaign; + private final Person character; + boolean isSuccess = false; + private List skillNames = new ArrayList<>(); + + + /** + * Constructs a {@code SkillCheckDialog} for the specified campaign and character. + * + *

This constructor initializes the dialog, processes the selected skill check, and displays the results. If + * the user cancels the skill check, no further action is taken.

+ * + * @param campaign the {@link Campaign} containing the current game state + * @param character the {@link Person} performing the skill check + * + * @author Illiani + * @since 0.50.05 + */ + public SkillCheckDialog(Campaign campaign, Person character) { + this.campaign = campaign; + this.character = character; + + // Initial Dialog + ImmersiveDialogCore dialog = getInitialDialog(); + int choiceIndex = dialog.getDialogChoice(); + + if (choiceIndex == DIALOG_CANCEL_INDEX) { + return; + } + + // Perform Check + String results = performSkillCheck(dialog.getComboBoxChoiceIndex(), dialog.getSpinnerValue(), choiceIndex); + + // Results Dialog + showResultsDialog(results); + } + + + /** + * Creates and returns the initial dialog for skill check configuration. + * + *

This dialog gathers user input for the skill, modifier, and whether to use Edge or not.

+ * + * @return an {@link ImmersiveDialogCore} instance for the initial dialog + * + * @author Illiani + * @since 0.50.05 + */ + private ImmersiveDialogCore getInitialDialog() { + return new ImmersiveDialogCore(campaign, + character, + null, + getInCharacterMessage(), + getButtons(character.getCurrentEdge() > 0, campaign.getCampaignOptions().isUseEdge()), + getFormattedTextAt(RESOURCE_BUNDLE, "message.ooc"), + null, + false, + getSupplementalPanel(), + new ImageIcon(DIALOG_IMAGE_FILENAME_DEFAULT), + true); + } + + /** + * Performs the skill check and returns the result as a string. + * + * @param selectedSkill the index of the skill selected in the ComboBox + * @param selectedModifier the modifier applied to the roll + * @param choiceIndex the user's choice (e.g., whether to use Edge or not) + * + * @return a {@code String} containing the result of the skill check + * + * @author Illiani + * @since 0.50.05 + */ + private String performSkillCheck(int selectedSkill, int selectedModifier, int choiceIndex) { + String skillName = skillNames.get(selectedSkill); + boolean useEdge = choiceIndex == DIALOG_USE_EDGE_INDEX; + SkillCheckUtility utility = new SkillCheckUtility(character, skillName, selectedModifier, useEdge); + isSuccess = utility.isSuccess(); + + return utility.getResultsText(); + } + + + /** + * Displays the results of the skill check in a results dialog. + * + * @param results the results text to display + * + * @author Illiani + * @since 0.50.05 + */ + private void showResultsDialog(String results) { + new ImmersiveDialogSimple(campaign, + character, + null, + results, + null, + null, + new ImageIcon(isSuccess ? DIALOG_IMAGE_FILENAME_PASS : DIALOG_IMAGE_FILENAME_FAIL), + false); + } + + /** + * Retrieves the in-character message to display in the dialog. + * + * @return a {@code String} containing the in-character message + * + * @author Illiani + * @since 0.50.05 + */ + private String getInCharacterMessage() { + int variant = randomInt(50); + return getFormattedTextAt(RESOURCE_BUNDLE, "message.ic." + variant); + } + + /** + * Retrieves the list of buttons for the dialog. + * + *

The buttons include Cancel, Attempt, and optionally Use Edge (if applicable).

+ * + * @param hasEdge whether the character has any Edge points available + * @param allowsEdge whether the campaign allows Edge usage + * + * @return a {@code List} of {@link ButtonLabelTooltipPair} instances for dialog buttons + * + * @author Illiani + * @since 0.50.05 + */ + private List getButtons(boolean hasEdge, boolean allowsEdge) { + List buttons = new ArrayList<>(); + buttons.add(new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE, "button.cancel"), null)); + buttons.add(new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE, "button.attempt"), null)); + + if (hasEdge && allowsEdge) { + buttons.add(new ButtonLabelTooltipPair(getFormattedTextAt(RESOURCE_BUNDLE, "button.edge"), null)); + } + + return buttons; + } + + + /** + * Creates and returns the supplemental panel for the dialog. + * + *

This panel includes a {@link MMComboBox} for selecting skills and a {@link JSpinner} for adding + * modifiers.

+ * + * @return a {@link JPanel} with additional input fields + * + * @author Illiani + * @since 0.50.05 + */ + private JPanel getSupplementalPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + GridBagConstraints constraints = createBaseConstraints(); + + // Add label for ComboBox + JLabel lblSkills = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE, "component.combo")); + addComponent(panel, lblSkills, constraints, 0, 0, 1, GridBagConstraints.NONE); + + // Add ComboBox + MMComboBox cboSkills = new MMComboBox<>("cboSkills", getComboListItems()); + addComponent(panel, cboSkills, constraints, 1, 0, 2, GridBagConstraints.HORIZONTAL); + + // Add label for spinner + JLabel lblModifiers = new JLabel(getFormattedTextAt(RESOURCE_BUNDLE, "component.spinner")); + addComponent(panel, lblModifiers, constraints, 0, 1, 1, GridBagConstraints.NONE); + + // Add spinner + JSpinner spnModifiers = new JSpinner(new SpinnerNumberModel(0, -30, 10, 1)); + addComponent(panel, spnModifiers, constraints, 1, 1, 1, GridBagConstraints.NONE); + + return panel; + } + + + /** + * Creates and returns the base {@link GridBagConstraints} for use in laying out the supplemental panel. + * + * @return a {@link GridBagConstraints} object with pre-configured values + * + * @author Illiani + * @since 0.50.05 + */ + private GridBagConstraints createBaseConstraints() { + GridBagConstraints constraints = new GridBagConstraints(); + constraints.insets = new Insets(5, 5, 5, 5); + constraints.anchor = GridBagConstraints.WEST; + return constraints; + } + + + /** + * Adds a component to the supplemental panel with specified layout constraints. + * + * @param panel the {@link JPanel} to add the component to + * @param component the {@link JComponent} to add + * @param constraints the {@link GridBagConstraints} to control layout + * @param gridX the grid X-coordinate + * @param gridY the grid Y-coordinate + * @param gridWidth the width of the component in terms of grid cells + * @param fill the fill style (e.g., {@link GridBagConstraints#HORIZONTAL}) + * + * @author Illiani + * @since 0.50.05 + */ + private void addComponent(JPanel panel, JComponent component, GridBagConstraints constraints, int gridX, int gridY, + int gridWidth, int fill) { + constraints.gridx = gridX; + constraints.gridy = gridY; + constraints.gridwidth = gridWidth; + constraints.fill = fill; + panel.add(component, constraints); + } + + /** + * Generates a list of skills with formatted labels for display in the ComboBox. + * + *

Each label includes the skill name (bolded), target number, and any relevant modifiers.

+ * + * @return a {@code String[]} containing the formatted skill labels + * + * @author Illiani + * @since 0.50.05 + */ + private String[] getComboListItems() { + List skills = new ArrayList<>(); + + for (String skillName : SkillType.getSkillList()) { + SkillType skillType = SkillType.getType(skillName); + int targetNumber = determineTargetNumber(character, skillType, 0); + boolean isCountsUp = SkillType.getType(skillName).isCountUp(); + + // Build the label with the target number + String formattedSkillName = "" + skillName.replace(" (RP Only)", "") + ""; + String label = formattedSkillName + " (" + targetNumber + (isCountsUp ? '-' : '+') + ")"; + + skills.add(label); + skillNames.add(skillName); + } + + // Convert the list to a String array and return it + return skills.toArray(new String[0]); + } +} diff --git a/MekHQ/src/mekhq/gui/view/PersonViewPanel.java b/MekHQ/src/mekhq/gui/view/PersonViewPanel.java index 0595e2b4ca5..c5b094f9c40 100644 --- a/MekHQ/src/mekhq/gui/view/PersonViewPanel.java +++ b/MekHQ/src/mekhq/gui/view/PersonViewPanel.java @@ -33,6 +33,7 @@ import static megamek.common.EntityWeightClass.WEIGHT_ULTRA_LIGHT; import static megamek.utilities.ImageUtilities.addTintToImageIcon; import static mekhq.campaign.personnel.Person.getLoyaltyName; +import static mekhq.campaign.personnel.skills.SkillType.RP_ONLY_TAG; import static mekhq.campaign.personnel.turnoverAndRetention.Fatigue.getEffectiveFatigue; import static org.jfree.chart.ChartColor.DARK_BLUE; import static org.jfree.chart.ChartColor.DARK_RED; @@ -1508,9 +1509,7 @@ private JPanel fillSkills() { } if (type.isRoleplaySkill()) { lblName = new JLabel(String.format(resourceMap.getString("format.itemHeader.roleplay"), - skillName.replaceAll(' ' + - Pattern.quote(resourceMap.getString("format.itemHeader.roleplay" + - ".removal")), ""))); + skillName.replaceAll(Pattern.quote(RP_ONLY_TAG), ""))); } else { lblName = new JLabel(String.format(resourceMap.getString("format.itemHeader"), skillName)); } diff --git a/MekHQ/unittests/mekhq/campaign/personnel/skills/SkillCheckUtilityTest.java b/MekHQ/unittests/mekhq/campaign/personnel/skills/SkillCheckUtilityTest.java index 6d00a8a2e3c..11f357ea23c 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/skills/SkillCheckUtilityTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/skills/SkillCheckUtilityTest.java @@ -302,10 +302,10 @@ void testDetermineTargetNumber_UntrainedWithOneLinkedAttribute() { SkillType testSkillType = new SkillType(); testSkillType.setSecondAttribute(NONE); - mockSkillType.when(() -> SkillType.getType(S_GUN_MEK)).thenReturn(testSkillType); + mockSkillType.when(() -> SkillType.getType("MISSING_NAME")).thenReturn(testSkillType); // Act - int targetNumber = SkillCheckUtility.determineTargetNumber(person, S_GUN_MEK, 0); + int targetNumber = SkillCheckUtility.determineTargetNumber(person, testSkillType, 0); // Assert int expectedTargetNumber = UNTRAINED_TARGET_NUMBER_ONE_LINKED_ATTRIBUTE - @@ -323,10 +323,10 @@ void testDetermineTargetNumber_UntrainedWithTwoLinkedAttributes() { try (MockedStatic mockSkillType = Mockito.mockStatic(SkillType.class)) { SkillType testSkillType = new SkillType(); - mockSkillType.when(() -> SkillType.getType(S_GUN_MEK)).thenReturn(testSkillType); + mockSkillType.when(() -> SkillType.getType("MISSING_NAME")).thenReturn(testSkillType); // Act - int targetNumber = SkillCheckUtility.determineTargetNumber(person, S_GUN_MEK, 0); + int targetNumber = SkillCheckUtility.determineTargetNumber(person, testSkillType, 0); // Assert int expectedTargetNumber = UNTRAINED_TARGET_NUMBER_TWO_LINKED_ATTRIBUTES - @@ -354,17 +354,17 @@ void testDetermineTargetNumber_TrainedWithOneLinkedAttribute() { DEFAULT_ATTRIBUTE_SCORE); Person mockPerson = mock(Person.class); - when(mockPerson.hasSkill(S_GUN_MEK)).thenReturn(true); - when(mockPerson.getSkill(S_GUN_MEK)).thenReturn(skill); + when(mockPerson.hasSkill("MISSING_NAME")).thenReturn(true); + when(mockPerson.getSkill("MISSING_NAME")).thenReturn(skill); when(mockPerson.getATOWAttributes()).thenReturn(characterAttributes); when(mockPerson.getOptions()).thenReturn(new PersonnelOptions()); when(mockPerson.getReputation()).thenReturn(0); try (MockedStatic mockSkillType = Mockito.mockStatic(SkillType.class)) { - mockSkillType.when(() -> SkillType.getType(S_GUN_MEK)).thenReturn(testSkillType); + mockSkillType.when(() -> SkillType.getType("MISSING_NAME")).thenReturn(testSkillType); // Act - int targetNumber = SkillCheckUtility.determineTargetNumber(mockPerson, S_GUN_MEK, 0); + int targetNumber = SkillCheckUtility.determineTargetNumber(mockPerson, testSkillType, 0); // Assert int skillTargetNumber = skill.getFinalSkillValue(new PersonnelOptions(), 0); @@ -393,17 +393,17 @@ void testDetermineTargetNumber_TrainedWithOneLinkedAttribute_AboveNormalAttribut DEFAULT_ATTRIBUTE_SCORE); Person mockPerson = mock(Person.class); - when(mockPerson.hasSkill(S_GUN_MEK)).thenReturn(true); - when(mockPerson.getSkill(S_GUN_MEK)).thenReturn(skill); + when(mockPerson.hasSkill("MISSING_NAME")).thenReturn(true); + when(mockPerson.getSkill("MISSING_NAME")).thenReturn(skill); when(mockPerson.getATOWAttributes()).thenReturn(characterAttributes); when(mockPerson.getOptions()).thenReturn(new PersonnelOptions()); when(mockPerson.getReputation()).thenReturn(0); try (MockedStatic mockSkillType = Mockito.mockStatic(SkillType.class)) { - mockSkillType.when(() -> SkillType.getType(S_GUN_MEK)).thenReturn(testSkillType); + mockSkillType.when(() -> SkillType.getType("MISSING_NAME")).thenReturn(testSkillType); // Act - int targetNumber = SkillCheckUtility.determineTargetNumber(mockPerson, S_GUN_MEK, 0); + int targetNumber = SkillCheckUtility.determineTargetNumber(mockPerson, testSkillType, 0); // Assert int skillTargetNumber = skill.getFinalSkillValue(new PersonnelOptions(), 0); @@ -429,17 +429,17 @@ void testDetermineTargetNumber_TrainedWithTwoLinkedAttributes() { DEFAULT_ATTRIBUTE_SCORE); Person mockPerson = mock(Person.class); - when(mockPerson.hasSkill(S_GUN_MEK)).thenReturn(true); - when(mockPerson.getSkill(S_GUN_MEK)).thenReturn(skill); + when(mockPerson.hasSkill("MISSING_NAME")).thenReturn(true); + when(mockPerson.getSkill("MISSING_NAME")).thenReturn(skill); when(mockPerson.getATOWAttributes()).thenReturn(characterAttributes); when(mockPerson.getOptions()).thenReturn(new PersonnelOptions()); when(mockPerson.getReputation()).thenReturn(0); try (MockedStatic mockSkillType = Mockito.mockStatic(SkillType.class)) { - mockSkillType.when(() -> SkillType.getType(S_GUN_MEK)).thenReturn(testSkillType); + mockSkillType.when(() -> SkillType.getType("MISSING_NAME")).thenReturn(testSkillType); // Act - int targetNumber = SkillCheckUtility.determineTargetNumber(mockPerson, S_GUN_MEK, 0); + int targetNumber = SkillCheckUtility.determineTargetNumber(mockPerson, testSkillType, 0); // Assert int skillTargetNumber = skill.getFinalSkillValue(new PersonnelOptions(), 0); @@ -465,10 +465,10 @@ void testDetermineTargetNumber_InvalidAttributes() { testSkillType.setSecondAttribute(DEXTERITY); try (MockedStatic mockSkillType = Mockito.mockStatic(SkillType.class)) { - mockSkillType.when(() -> SkillType.getType(S_GUN_MEK)).thenReturn(testSkillType); + mockSkillType.when(() -> SkillType.getType("MISSING_NAME")).thenReturn(testSkillType); // Act - int targetNumber = SkillCheckUtility.determineTargetNumber(person, S_GUN_MEK, 0); + int targetNumber = SkillCheckUtility.determineTargetNumber(person, testSkillType, 0); // Assert int expectedTargetNumber = UNTRAINED_TARGET_NUMBER_TWO_LINKED_ATTRIBUTES - @@ -491,10 +491,10 @@ void testDetermineTargetNumber_EdgeCaseSkillType() { person.setATOWAttributes(new Attributes()); try (MockedStatic mockSkillType = Mockito.mockStatic(SkillType.class)) { - mockSkillType.when(() -> SkillType.getType(S_GUN_MEK)).thenReturn(edgeCaseSkillType); + mockSkillType.when(() -> SkillType.getType("MISSING_NAME")).thenReturn(edgeCaseSkillType); // Act - int targetNumber = SkillCheckUtility.determineTargetNumber(person, S_GUN_MEK, 0); + int targetNumber = SkillCheckUtility.determineTargetNumber(person, edgeCaseSkillType, 0); // Assert assertEquals(UNTRAINED_TARGET_NUMBER_ONE_LINKED_ATTRIBUTE, targetNumber); @@ -520,10 +520,10 @@ void testDetermineTargetNumber_NegativeAttributeModifier() { testSkillType.setSecondAttribute(REFLEXES); try (MockedStatic mockSkillType = Mockito.mockStatic(SkillType.class)) { - mockSkillType.when(() -> SkillType.getType(S_GUN_MEK)).thenReturn(testSkillType); + mockSkillType.when(() -> SkillType.getType("MISSING_NAME")).thenReturn(testSkillType); // Act - int targetNumber = SkillCheckUtility.determineTargetNumber(person, S_GUN_MEK, 0); + int targetNumber = SkillCheckUtility.determineTargetNumber(person, testSkillType, 0); // Assert int expectedTargetNumber = UNTRAINED_TARGET_NUMBER_TWO_LINKED_ATTRIBUTES -