From a5aa317db6ad3a76c85b067ba09a28ea55bb9a1d Mon Sep 17 00:00:00 2001
From: IllianiCBT
Date: Mon, 7 Apr 2025 00:25:08 -0500
Subject: [PATCH 1/5] Added Skill Check Functionality And Refactored Skill
Check Utility Class
- Implemented new skill check feature, including UI updates and backend logic.
- Refactored `SkillType`, `SkillCheckUtility` `MarginOfSuccess` enums to remove a handful of bugs or to add new functionality.
---
.../resources/mekhq/resources/GUI.properties | 1 +
.../resources/MarginOfSuccess.properties | 10 +-
.../resources/PersonViewPanel.properties | 1 -
.../resources/SkillCheckDialog.properties | 65 ++++
.../resources/SkillCheckUtility.properties | 7 +-
MekHQ/src/mekhq/campaign/Campaign.java | 3 +-
.../personnel/skills/SkillCheckUtility.java | 50 +++-
.../campaign/personnel/skills/SkillType.java | 96 +++---
.../skills/enums/MarginOfSuccess.java | 85 ++++--
.../adapter/PersonnelTableMouseAdapter.java | 17 +-
.../immersiveDialogs/ImmersiveDialogCore.java | 97 ++++--
.../mekhq/gui/dialog/SkillCheckDialog.java | 282 ++++++++++++++++++
MekHQ/src/mekhq/gui/view/PersonViewPanel.java | 7 +-
.../skills/SkillCheckUtilityTest.java | 44 +--
14 files changed, 628 insertions(+), 137 deletions(-)
create mode 100644 MekHQ/resources/mekhq/resources/SkillCheckDialog.properties
create mode 100644 MekHQ/src/mekhq/gui/dialog/SkillCheckDialog.java
diff --git a/MekHQ/resources/mekhq/resources/GUI.properties b/MekHQ/resources/mekhq/resources/GUI.properties
index daa9dde448..a4263a95b2 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 63dcf2e0a3..c516a4d2ec 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 ed8d6cf983..d0116a8e87 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 0000000000..19575ce56e
--- /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 20745993ee..5de850fb0d 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/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java
index d6587b9818..265a12e8ae 100644
--- a/MekHQ/src/mekhq/campaign/Campaign.java
+++ b/MekHQ/src/mekhq/campaign/Campaign.java
@@ -6407,7 +6407,8 @@ public int getTemporaryPrisonerCapacity() {
}
public void setTemporaryPrisonerCapacity(int temporaryPrisonerCapacity) {
- this.temporaryPrisonerCapacity = max(PrisonerEventManager.MINIMUM_TEMPORARY_CAPACITY, temporaryPrisonerCapacity);
+ this.temporaryPrisonerCapacity = max(PrisonerEventManager.MINIMUM_TEMPORARY_CAPACITY,
+ temporaryPrisonerCapacity);
}
public RandomEventLibraries getRandomEventLibraries() {
diff --git a/MekHQ/src/mekhq/campaign/personnel/skills/SkillCheckUtility.java b/MekHQ/src/mekhq/campaign/personnel/skills/SkillCheckUtility.java
index df035ea52d..de4c71c94f 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;
@@ -77,6 +78,7 @@ public class SkillCheckUtility {
private int marginOfSuccess;
private String resultsText;
private int targetNumber;
+ boolean isCountUp;
private int roll;
private boolean usedEdge;
@@ -108,7 +110,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 +202,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 +212,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 +340,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 +350,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 +429,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;
}
@@ -441,7 +459,7 @@ private void rollWithEdge() {
person.changeCurrentEdge(-1);
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 a4b48ad35f..fa33dbae99 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 e1ebc5219b..984874d254 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 5e02956fd2..1250e8dd7c 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 fd54260a82..7471ccd875 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();
@@ -442,7 +487,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 +495,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 +563,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 +626,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 0000000000..8cf39d4a42
--- /dev/null
+++ b/MekHQ/src/mekhq/gui/dialog/SkillCheckDialog.java
@@ -0,0 +1,282 @@
+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.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+
+import megamek.client.ui.baseComponents.MMComboBox;
+import megamek.logging.MMLogger;
+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 int DIALOG_CANCEL_INDEX = 0;
+ final int DIALOG_USE_EDGE_INDEX = 2;
+
+ private final Campaign campaign;
+ private final Person character;
+ 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(),
+ null,
+ 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);
+ MMLogger logger = MMLogger.create(SkillCheckDialog.class);
+ logger.info("Performing skill check for " + skillName + " with modifier " + selectedModifier);
+ boolean useEdge = choiceIndex == DIALOG_USE_EDGE_INDEX;
+ SkillCheckUtility utility = new SkillCheckUtility(character, skillName, selectedModifier, useEdge);
+
+ 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, null, 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 0481f0f880..d2e3c1cf74 100644
--- a/MekHQ/src/mekhq/gui/view/PersonViewPanel.java
+++ b/MekHQ/src/mekhq/gui/view/PersonViewPanel.java
@@ -32,6 +32,7 @@
import static megamek.client.ui.WrapLayout.wordWrap;
import static megamek.common.EntityWeightClass.WEIGHT_ULTRA_LIGHT;
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 mekhq.utilities.ImageUtilities.addTintToImageIcon;
import static org.jfree.chart.ChartColor.DARK_BLUE;
@@ -94,8 +95,6 @@
import mekhq.gui.utilities.MarkdownRenderer;
import mekhq.gui.utilities.WrapLayout;
-import static megamek.client.ui.WrapLayout.wordWrap;
-
/**
* A custom panel that gets filled in with goodies from a Person record
*
@@ -1510,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 6d00a8a2e3..11f357ea23 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 -
From b89d344f30143c47629809df709c6e5c74c2d9b9 Mon Sep 17 00:00:00 2001
From: IllianiCBT
Date: Mon, 7 Apr 2025 01:03:49 -0500
Subject: [PATCH 2/5] - Introduced `ImageIcon` to `SkillCheckDialog` for
enhanced visuals.
---
MekHQ/data/images/misc/skill_check.png | Bin 0 -> 106086 bytes
.../src/mekhq/gui/dialog/SkillCheckDialog.java | 12 +++++++-----
2 files changed, 7 insertions(+), 5 deletions(-)
create mode 100644 MekHQ/data/images/misc/skill_check.png
diff --git a/MekHQ/data/images/misc/skill_check.png b/MekHQ/data/images/misc/skill_check.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d463117d4142f1ba86efd5a7cc7fdf795923ed0
GIT binary patch
literal 106086
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 |