From 7312bf0d6a4e909b376c8e13ee4501909a10f945 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 6 Apr 2025 04:32:30 -0500 Subject: [PATCH 1/2] Added Discretionary Spending System Based on Wealth Trait - Implemented a discretionary spending system using the "Wealth" trait, with rules inspired by A Time of War: Companion. - Added configuration option to enable monthly wealth reinvestment via Campaign Options. - Introduced "Extreme Expenditure" action for personnel, affecting wealth, loyalty, and generating reports. - Expanded finances logic and resources to include "Wealth" as a new transaction type. - Integrated spending calculations and options into the campaign process and GUI.OnWealth.tooltip` property to clarify fund reinvestment by campaign commanders. - Removed extraneous whitespace from `DiscretionarySpending.properties`. ``` --- .../CampaignOptionsDialog.properties | 5 + .../DiscretionarySpending.properties | 7 + .../mekhq/resources/Finances.properties | 14 +- .../resources/mekhq/resources/GUI.properties | 5 +- MekHQ/src/mekhq/campaign/Campaign.java | 14 + MekHQ/src/mekhq/campaign/CampaignOptions.java | 17 +- .../finances/enums/TransactionType.java | 17 +- .../personnel/DiscretionarySpending.java | 324 ++++++++++++++++++ .../src/mekhq/campaign/personnel/Person.java | 16 + .../adapter/PersonnelTableMouseAdapter.java | 42 +++ .../campaignOptions/contents/FinancesTab.java | 298 ++++++++-------- 11 files changed, 593 insertions(+), 166 deletions(-) create mode 100644 MekHQ/resources/mekhq/resources/DiscretionarySpending.properties create mode 100644 MekHQ/src/mekhq/campaign/personnel/DiscretionarySpending.java diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index efaccbeb82f..7b102645fe1 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -1298,6 +1298,11 @@ lblNewFinancialYearFinancesToCSVExportBox.tooltip=This writes the finance table \ the first day of a new financial term, right before the table is carried over to the next period. lblSimulateGrayMonday.text=Simulate Gray Monday \u2728 lblSimulateGrayMonday.tooltip=Simulate the economic and social upheaval of Gray Monday. +lblAllowMonthlyReinvestment.text=Allow Monthly Reinvestment of Wealth \u2728 +lblAllowMonthlyReinvestment.tooltip=Each month the campaign commander will use their Wealth trait to\ + \ reinvest money back into the campaign.\ +
\ +
This is based on the Wealth Trait Check rules from the A Time of War:Companion. # createSalesPanel lblSalesPanel.text=Sales lblSellUnitsBox.text=Enable the Sale of Units diff --git a/MekHQ/resources/mekhq/resources/DiscretionarySpending.properties b/MekHQ/resources/mekhq/resources/DiscretionarySpending.properties new file mode 100644 index 00000000000..0574e9730a6 --- /dev/null +++ b/MekHQ/resources/mekhq/resources/DiscretionarySpending.properties @@ -0,0 +1,7 @@ +# suppress inspection "UnusedProperty" for the whole file +report.format.monthly={0} has used {1}Wealth {2}{3} to transfer {4} C-Bills into the\ + \ unit''s coffers. The funds have cleared and are available for spending. +report.format.extreme={0} has {1}exhausted{2} their Wealth to transfer {3} C-Bills into the unit''s coffers. The\ + \ funds have cleared and are available for spending. {4} has {5} Wealth remaining. +report.format.exhausted={0} has already {1}exhausted{2} their Wealth for this month and cannot reinvest into the unit. +finance.format=Transfer from {0} diff --git a/MekHQ/resources/mekhq/resources/Finances.properties b/MekHQ/resources/mekhq/resources/Finances.properties index ce3af86d451..e5b95b81a76 100644 --- a/MekHQ/resources/mekhq/resources/Finances.properties +++ b/MekHQ/resources/mekhq/resources/Finances.properties @@ -1,7 +1,6 @@ # This is used to store any Finances Resources ## Generic Finances Resources Error.text=Error - ## Finances Enums # FinancialTerm Enum FinancialTerm.BIWEEKLY.text=Biweekly @@ -14,7 +13,6 @@ FinancialTerm.SEMIANNUALLY.text=Semiannually FinancialTerm.SEMIANNUALLY.toolTipText=The financial term is once every six months. FinancialTerm.ANNUALLY.text=Annually FinancialTerm.ANNUALLY.toolTipText=The financial term is one per year. - # FinancialYearDuration Enum FinancialYearDuration.SEMIANNUAL.text=Semiannual FinancialYearDuration.SEMIANNUAL.toolTipText=The financial term lasts six months @@ -28,7 +26,6 @@ FinancialYearDuration.DECENNIAL.text=Decennial FinancialYearDuration.DECENNIAL.toolTipText=The financial term lasts ten years FinancialYearDuration.FOREVER.text=Forever (Not Recommended) FinancialYearDuration.FOREVER.toolTipText=The financial term lasts forever. This option is not recommended and can cause issues in lengthy campaigns. - # TransactionType Enum TransactionType.BATTLE_LOSS_COMPENSATION.text=Battle Loss Compensation TransactionType.BATTLE_LOSS_COMPENSATION.toolTipText=A financial transaction where battle losses are partially to completely paid back by the employer, as per the current negotiated contract. @@ -88,7 +85,8 @@ TransactionType.UNIT_SALE.text=Unit Sale(s) TransactionType.UNIT_SALE.toolTipText=A financial transaction where a unit was or multiple units were sold. TransactionType.BONUS_EXCHANGE.text=Bonus Exchange TransactionType.BONUS_EXCHANGE.toolTipText=A financial transaction where Bonus Parts were exchanged for money. - +TransactionType.WEALTH.text=Reinvestment +TransactionType.WEALTH.toolTipText=A financial transaction where the commander reinvested funds back into the unit. ## Finances Files # Peacetime Operating Costs PeacetimeCosts.title=Monthly Peacetime Operating Costs @@ -99,24 +97,18 @@ PeacetimeCostsAmmunition.title=Monthly Ammunition PeacetimeCostsAmmunition.text=Your account has been debited %s for training munitions PeacetimeCostsFuel.title=Monthly Fuel bill PeacetimeCostsFuel.text=Your account has been debited %s for fuel - # Salaries and other Overhead Salaries.title=Monthly salaries Salaries.text=Payday! Your account has been debited for %s in personnel salaries Overhead.title=Monthly overhead Overhead.text=Your account has been debited for %s in overhead expenses - # Loans Loan.title=loan payment to %s Loan.text=Your account has been debited for %s in loan payment to %s Loan.insufficient.report=You have insufficient funds to service the debt on loan %s!%s Funds required: %s Loan.paid.report=You have fully paid off loan %s - # File Export FinanceExport.format=%s financial transactions written to file. - - - ## Unsorted General Finances FinancialTermEndCarryover.finances=Carryover from previous financial term Taxes.finances=Taxes @@ -135,4 +127,4 @@ OverheadCosts.text=overhead costs Shares.text=shares AssetPayment.finances=Income from %s AssetPayment.report=Your account has been credited for %s from %s -loyaltyChangeGroup.text=Loyalty has changed across the unit. \ No newline at end of file +loyaltyChangeGroup.text=Loyalty has changed across the unit. diff --git a/MekHQ/resources/mekhq/resources/GUI.properties b/MekHQ/resources/mekhq/resources/GUI.properties index daa9dde4489..5799d8f844f 100644 --- a/MekHQ/resources/mekhq/resources/GUI.properties +++ b/MekHQ/resources/mekhq/resources/GUI.properties @@ -154,7 +154,8 @@ spendOnReputation.tooltip=If this character is the campaign commander unit Reput \ modifies all Negotiation, Protocols, and Streetwise skills by %s. spendOnWealth.text=Wealth -> %s (%s xp) spendOnWealth.tooltip=Raising this trait to at least rank 7 will improve unit Reputation by 1 if this character is the\ - \ campaign commander. + \ campaign commander. If this character is the campaign commander, they will reinvest funds back into the unit at the\ + \ beginning of each month. spendOnUnlucky.text=Unlucky -> %s (%s xp) spendOnUnlucky.tooltip=Decreases the character's available Edge by %s. spendOnAttributes.increase=Increase Attribute Scores @@ -197,6 +198,8 @@ addScenarioEntry.text=Add Single Scenario Entry assignKill.text=Add Single Kill Entry exportPersonnel.text=Export Personnel sack.text=Sack +wealth.extreme.single=Perform Extreme Expenditure (+%s C-Bills, -1 Wealth) +wealth.extreme.multiple=Perform Extreme Expenditure (-1 Wealth) eduEducation.text=Education eduCivilian.text=Civilian eduMilitary.text=Military diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 04622963ae5..1e952f3ed6e 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -46,6 +46,7 @@ import static mekhq.campaign.mission.resupplyAndCaches.Resupply.isProhibitedUnitType; import static mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities.processAbandonedConvoy; import static mekhq.campaign.parts.enums.PartQuality.QUALITY_A; +import static mekhq.campaign.personnel.DiscretionarySpending.performDiscretionarySpending; import static mekhq.campaign.personnel.backgrounds.BackgroundsController.randomMercenaryCompanyNameGenerator; import static mekhq.campaign.personnel.education.EducationController.getAcademy; import static mekhq.campaign.personnel.education.TrainingCombatTeams.processTrainingCombatTeams; @@ -4930,6 +4931,19 @@ public void processNewDayPersonnel() { personnelWhoAdvancedInXP.add(person); } } + + if (person.isCommander() && + campaignOptions.isAllowMonthlyReinvestment() && + !person.isHasPerformedExtremeExpenditure()) { + String reportString = performDiscretionarySpending(person, finances, currentDay); + if (reportString != null) { + addReport(reportString); + } else { + logger.error("Unable to process discretionary spending for {}", person.getFullTitle()); + } + } + + person.setHasPerformedExtremeExpenditure(false); } if (isCommandersDay && !faction.isClan() && (peopleWhoCelebrateCommandersDay < commanderDayTargetNumber)) { diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 4e7b4991a53..0e289c68de9 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -466,6 +466,7 @@ public static String getTechLevelName(final int techLevel) { private FinancialYearDuration financialYearDuration; private boolean newFinancialYearFinancesToCSVExport; private boolean simulateGrayMonday; + private boolean allowMonthlyReinvestment; // Price Multipliers private double commonPartPriceMultiplier; @@ -1068,6 +1069,7 @@ public CampaignOptions() { setFinancialYearDuration(FinancialYearDuration.ANNUAL); newFinancialYearFinancesToCSVExport = false; simulateGrayMonday = false; + allowMonthlyReinvestment = false; // Price Multipliers setCommonPartPriceMultiplier(1.0); @@ -3382,6 +3384,14 @@ public void setSimulateGrayMonday(final boolean simulateGrayMonday) { this.simulateGrayMonday = simulateGrayMonday; } + public boolean isAllowMonthlyReinvestment() { + return allowMonthlyReinvestment; + } + + public void setAllowMonthlyReinvestment(final boolean allowMonthlyReinvestment) { + this.allowMonthlyReinvestment = allowMonthlyReinvestment; + } + // region Price Multipliers public double getCommonPartPriceMultiplier() { return commonPartPriceMultiplier; @@ -5278,6 +5288,7 @@ public void writeToXml(final PrintWriter pw, int indent) { "newFinancialYearFinancesToCSVExport", newFinancialYearFinancesToCSVExport); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "simulateGrayMonday", simulateGrayMonday); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "allowMonthlyReinvestment", allowMonthlyReinvestment); // region Price Multipliers MHQXMLUtility.writeSimpleXMLTag(pw, indent, "commonPartPriceMultiplier", getCommonPartPriceMultiplier()); @@ -6208,6 +6219,8 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve retVal.newFinancialYearFinancesToCSVExport = Boolean.parseBoolean(wn2.getTextContent().trim()); } else if (nodeName.equalsIgnoreCase("simulateGrayMonday")) { retVal.simulateGrayMonday = Boolean.parseBoolean(wn2.getTextContent().trim()); + } else if (nodeName.equalsIgnoreCase("allowMonthlyReinvestment")) { + retVal.allowMonthlyReinvestment = Boolean.parseBoolean(wn2.getTextContent().trim()); // region Price Multipliers } else if (nodeName.equalsIgnoreCase("commonPartPriceMultiplier")) { @@ -6482,8 +6495,8 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve retVal.getRandomOriginOptions() .setExtraRandomOrigin(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (nodeName.equalsIgnoreCase("originDistanceScale")) { // Legacy, 0.49.7 Removal - retVal.getRandomOriginOptions() - .setOriginDistanceScale(Double.parseDouble(wn2.getTextContent().trim())); + retVal.getRandomOriginOptions().setOriginDistanceScale(Double.parseDouble(wn2.getTextContent() + .trim())); } else if (nodeName.equalsIgnoreCase("dependentsNeverLeave")) { // Legacy - 0.49.7 Removal retVal.setUseRandomDependentRemoval(!Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (nodeName.equalsIgnoreCase("marriageAgeRange")) { // Legacy - 0.49.6 Removal diff --git a/MekHQ/src/mekhq/campaign/finances/enums/TransactionType.java b/MekHQ/src/mekhq/campaign/finances/enums/TransactionType.java index 40e16ed0ec5..99f06bc2c0a 100644 --- a/MekHQ/src/mekhq/campaign/finances/enums/TransactionType.java +++ b/MekHQ/src/mekhq/campaign/finances/enums/TransactionType.java @@ -35,14 +35,14 @@ public enum TransactionType { // region Enum Declarations BATTLE_LOSS_COMPENSATION("TransactionType.BATTLE_LOSS_COMPENSATION.text", - "TransactionType.BATTLE_LOSS_COMPENSATION.toolTipText"), + "TransactionType.BATTLE_LOSS_COMPENSATION.toolTipText"), CONSTRUCTION("TransactionType.CONSTRUCTION.text", "TransactionType.CONSTRUCTION.toolTipText"), CONTRACT_PAYMENT("TransactionType.CONTRACT_PAYMENT.text", "TransactionType.CONTRACT_PAYMENT.toolTipText"), EDUCATION("TransactionType.EDUCATION.text", "TransactionType.EDUCATION.toolTipText"), EQUIPMENT_PURCHASE("TransactionType.EQUIPMENT_PURCHASE.text", "TransactionType.EQUIPMENT_PURCHASE.toolTipText"), EQUIPMENT_SALE("TransactionType.EQUIPMENT_SALE.text", "TransactionType.EQUIPMENT_SALE.toolTipText"), FINANCIAL_TERM_END_CARRYOVER("TransactionType.FINANCIAL_TERM_END_CARRYOVER.text", - "TransactionType.FINANCIAL_TERM_END_CARRYOVER.toolTipText"), + "TransactionType.FINANCIAL_TERM_END_CARRYOVER.toolTipText"), FINE("TransactionType.FINE.text", "TransactionType.FINE.toolTipText"), LOAN_PAYMENT("TransactionType.LOAN_PAYMENT.text", "TransactionType.LOAN_PAYMENT.toolTipText"), LOAN_PRINCIPAL("TransactionType.LOAN_PRINCIPAL.text", "TransactionType.LOAN_PRINCIPAL.toolTipText"), @@ -64,7 +64,8 @@ public enum TransactionType { TRANSPORTATION("TransactionType.TRANSPORTATION.text", "TransactionType.TRANSPORTATION.toolTipText"), UNIT_PURCHASE("TransactionType.UNIT_PURCHASE.text", "TransactionType.UNIT_PURCHASE.toolTipText"), UNIT_SALE("TransactionType.UNIT_SALE.text", "TransactionType.UNIT_SALE.toolTipText"), - BONUS_EXCHANGE("TransactionType.BONUS_EXCHANGE.text", "TransactionType.BONUS_EXCHANGE.toolTipText"); + BONUS_EXCHANGE("TransactionType.BONUS_EXCHANGE.text", "TransactionType.BONUS_EXCHANGE.toolTipText"), + WEALTH("TransactionType.WEALTH.text", "TransactionType.WEALTH.toolTipText"); // endregion Enum Declarations // region Variable Declarations @@ -75,7 +76,7 @@ public enum TransactionType { // region Constructors TransactionType(final String name, final String toolTipText) { final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Finances", - MekHQ.getMHQOptions().getLocale()); + MekHQ.getMHQOptions().getLocale()); this.name = resources.getString(name); this.toolTipText = resources.getString(toolTipText); } @@ -203,6 +204,10 @@ public boolean isUnitSale() { public boolean isBonusExchange() { return this == BONUS_EXCHANGE; } + + public boolean isWealth() { + return this == WEALTH; + } // endregion Boolean Comparison Methods // region File I/O @@ -264,6 +269,8 @@ public static TransactionType parseFromString(final String text) { return TAXES; case 21: return BONUS_EXCHANGE; + case 22: + return WEALTH; default: break; } @@ -272,7 +279,7 @@ public static TransactionType parseFromString(final String text) { } MMLogger.create(TransactionType.class) - .error("Unable to parse " + text + " into a TransactionType. Returning MISCELLANEOUS."); + .error("Unable to parse " + text + " into a TransactionType. Returning MISCELLANEOUS."); return MISCELLANEOUS; } // endregion File I/O diff --git a/MekHQ/src/mekhq/campaign/personnel/DiscretionarySpending.java b/MekHQ/src/mekhq/campaign/personnel/DiscretionarySpending.java new file mode 100644 index 00000000000..d812bd185f4 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/personnel/DiscretionarySpending.java @@ -0,0 +1,324 @@ +/* + * 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.campaign.personnel; + +import static megamek.common.Compute.d6; +import static mekhq.campaign.finances.enums.TransactionType.WEALTH; +import static mekhq.campaign.personnel.Person.MINIMUM_WEALTH; +import static mekhq.campaign.personnel.skills.enums.SkillAttribute.WILLPOWER; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; +import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; +import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor; + +import java.time.LocalDate; +import java.util.Map; + +import mekhq.MekHQ; +import mekhq.campaign.finances.Finances; +import mekhq.campaign.finances.Money; + +/** + * The {@code DiscretionarySpending} class manages the simulation of discretionary spending for a person based on their + * wealth and spending limits defined by wealth levels. + * + *

It calculates total spending for major, moderate, and minor purchases based on dice rolls, + * spending rules, and wealth modifiers.

+ * + *

The rules in this class are based on those found in the ATOW: Companion, pages 53-54. Some liberties were + * taken. We don't model increasing or degrading Wealth (for an explanation as to why, see the comments in + * {@link #calculateSpending(int, int, int)}). Furthermore, the player is always assumed to be spending up to their + * monthly limit. Any funds accumulated in this fashion are 'reinvested' back into the campaign, which is why only the + * campaign commander uses their Wealth in this manner.

+ */ +public class DiscretionarySpending { + final private static String RESOURCE_BUNDLE = "mekhq.resources." + DiscretionarySpending.class.getSimpleName(); + + /** + * The maximum number of major purchases a person can make in a single discretionary spending calculation. + */ + private static final int MAXIMUM_MAJOR_PURCHASES = 1; + + /** + * The multiplier for calculating the number of moderate purchases. This is derived from the person's wealth. + */ + private static final int MODERATE_PURCHASES_MULTIPLIER = 1; + + /** + * The multiplier for calculating the number of minor purchases. This is derived from the person's wealth. + */ + private static final int MINOR_PURCHASES_MULTIPLIER = 3; + + /** + * The base target number for wealth checks. This value is used as the foundation before applying modifiers for + * major, moderate, or minor purchases. + */ + private static final int WEALTH_CHECK_TARGET_NUMBER = 12; + + /** + * The modifier applied to the wealth check target number for major purchases. This represents the difficulty or + * ease of passing the check. + */ + private static final int WEALTH_CHECK_MAJOR_MODIFIER = -6; + + /** + * The modifier applied to the wealth check target number for moderate purchases. This represents the difficulty or + * ease of passing the check. + */ + private static final int WEALTH_CHECK_MODERATE_MODIFIER = -4; + + /** + * The modifier applied to the wealth check target number for minor purchases. This represents the difficulty or + * ease of passing the check. + */ + private static final int WEALTH_CHECK_MINOR_MODIFIER = -2; + + /** + * A mapping of wealth levels to the spending limits for major, moderate, and minor purchases. + * + *

Source: ATOW: Companion, pg 54

+ */ + private static final Map discretionarySpendingTable = Map.ofEntries(Map.entry(-1, + new SpendingLimits(21, 9, 4)), + Map.entry(0, new SpendingLimits(200, 80, 33)), + Map.entry(1, new SpendingLimits(469, 188, 75)), + Map.entry(2, new SpendingLimits(875, 350, 138)), + Map.entry(3, new SpendingLimits(1625, 600, 250)), + Map.entry(4, new SpendingLimits(3750, 1500, 563)), + Map.entry(5, new SpendingLimits(6875, 2750, 1000)), + Map.entry(6, new SpendingLimits(12500, 5000, 1750)), + Map.entry(7, new SpendingLimits(28125, 11250, 3750)), + Map.entry(8, new SpendingLimits(50000, 20000, 6250)), + Map.entry(9, new SpendingLimits(87500, 35000, 10000)), + Map.entry(10, new SpendingLimits(150000, 60000, 15000))); + + private String reportMessage = ""; + + /** + * Constructs a {@code DiscretionarySpending} instance for the given {@code Person} and calculates their + * discretionary spending based on their wealth level. + * + * @param person The person performing discretionary spending, used to determine wealth. + * @param finances The finances object managing transactions. + * @param today The date of the transaction. + */ + public DiscretionarySpending(Person person, Finances finances, LocalDate today) { + final String fullTitle = person.getHyperlinkedFullTitle(); + if (person.isHasPerformedExtremeExpenditure()) { + final String openingSpan = spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()); + reportMessage = getFormattedTextAt(RESOURCE_BUNDLE, + "report.format.monthly", + fullTitle, + openingSpan, + CLOSING_SPAN_TAG); + + return; + } + + int totalSpending = 0; + final int wealth = person.getWealth(); + final SpendingLimits spendingLimits = discretionarySpendingTable.get(wealth); + + // Calculate total spending for major, moderate, and minor purchases + totalSpending += calculateSpending(spendingLimits.major(), + MAXIMUM_MAJOR_PURCHASES, + WEALTH_CHECK_MAJOR_MODIFIER); + totalSpending += calculateSpending(spendingLimits.moderate(), + wealth * MODERATE_PURCHASES_MULTIPLIER, + WEALTH_CHECK_MODERATE_MODIFIER); + totalSpending += calculateSpending(spendingLimits.minor(), + wealth * MINOR_PURCHASES_MULTIPLIER, + WEALTH_CHECK_MINOR_MODIFIER); + + Money money = Money.of(totalSpending); + + // Generate the report message + final String fullName = person.getFullName(); + final String openingSpan = spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorPositiveHexColor()); + reportMessage = getFormattedTextAt(RESOURCE_BUNDLE, + "report.format.monthly", + fullTitle, + openingSpan, + wealth, + CLOSING_SPAN_TAG, + money.toAmountString()); + + // Credit finances with the calculated total spending + String reason = getFormattedTextAt(RESOURCE_BUNDLE, "finance.format", fullName); + finances.credit(WEALTH, today, money, reason); + } + + /** + * Calculates the total spending for a specified type of purchase (major, moderate, or minor). + * + * @param spendingLimit The spending limit for this type of purchase. + * @param purchaseCount The number of purchases to consider for this type. + * @param wealthModifier The wealth modifier applied to the target number. + * + * @return The total spending for this type of purchase. + */ + private int calculateSpending(int spendingLimit, int purchaseCount, int wealthModifier) { + int total = 0; + for (int purchase = 0; purchase < purchaseCount; purchase++) { + int targetNumber = WEALTH_CHECK_TARGET_NUMBER + wealthModifier; + + // ATOW Companion states that on a fumble, the character's Wealth decreases by 1. As we're not giving the + // player a choice whether they perform discretionary spending, we're not going to implement that rule. + // Similarly, we're not going to implement the rule that passing the check by 6 margins of success + // increases the character's Wealth score - for the same reason. If we implemented those rules Wealth + // would quickly spike to either extreme end of the spectrum. + if (d6(2) >= targetNumber) { + total += spendingLimit; + } + } + return total; + } + + /** + * Retrieves the report message summarizing the discretionary spending. + * + * @return The report message. + */ + public String getReportMessage() { + return reportMessage; + } + + /** + * Performs discretionary spending for a given {@code Person} and records the transactions, then returns the + * generated report message. + * + * @param person The person performing discretionary spending. + * @param finances The finances object managing transactions. + * @param today The date of the transaction. + * + * @return A formatted report message summarizing the discretionary spending. + */ + public static String performDiscretionarySpending(Person person, Finances finances, LocalDate today) { + DiscretionarySpending spending = new DiscretionarySpending(person, finances, today); + return spending.getReportMessage(); + } + + /** + * Simulates and processes an extreme expenditure event for the given person. + * + *

During an extreme expenditure, the person's wealth decreases by one point. The spending is based on their + * current wealth level, major spending limits, and their willpower attribute. The result is a formatted report + * summarizing the transaction and its impact on the person's finances.

+ * + *

Implementation: according to ATOW: Companion, if a player does this, they cannot make any other + * discretionary purchases for the rest of the month.

+ * + * @param person The person performing the extreme expenditure, whose wealth and attributes are used in + * calculations. + * @param finances The finances object that records the monetary transaction for the extreme expenditure. + * @param today The date the expenditure takes place. + * + * @return A formatted string summarizing the extreme expenditure report. Returns an empty string if the person has + * the minimum wealth level. + */ + public static String performExtremeExpenditure(Person person, Finances finances, LocalDate today) { + final int wealth = person.getWealth(); + if (wealth == MINIMUM_WEALTH) { + return ""; + } + + person.setWealth(wealth - 1); + person.setHasPerformedExtremeExpenditure(true); + + int totalSpending = getExpenditure(person.getAttributeScore(WILLPOWER), wealth); + + Money money = Money.of(totalSpending); + + // Generate the report message + final String fullTitle = person.getHyperlinkedFullTitle(); + final String fullName = person.getFullName(); + final String givenName = person.getGivenName(); + final String openingSpan = spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorWarningHexColor()); + String reportMessage = getFormattedTextAt(RESOURCE_BUNDLE, + "report.format.extreme", + fullTitle, + openingSpan, + CLOSING_SPAN_TAG, + money.toAmountString(), + givenName, + wealth - 1); + + // Credit finances with the calculated total spending + String reason = getFormattedTextAt(RESOURCE_BUNDLE, "finance.format", fullName); + finances.credit(WEALTH, today, money, reason); + + return reportMessage; + } + + /** + * Calculates the expenditure based on a person's willpower and wealth level. + * + *

Expenditure is determined by multiplying the person's willpower attribute by the major spending limit + * associated with their current wealth level.

+ * + * @param willpower The person's willpower attribute, which influences the total expenditure. + * @param wealth The person's current wealth level, used to determine the major spending limit. + * + * @return The total expenditure calculated as the product of the major spending limit and the willpower. + */ + public static int getExpenditure(int willpower, int wealth) { + final SpendingLimits spendingLimits = discretionarySpendingTable.get(wealth); + final int major = spendingLimits.major(); + + return major * willpower; + } + + /** + * Generates a report message indicating that expenditure has been exhausted for a given person. + * + *

The message is retrieved from a resource bundle and formatted with the given hyperlinked full title of the + * person.

+ * + * @param hyperlinkedFullTitle The full title of the person, formatted as a hyperlink, to be included in the report + * message. + * + * @return A formatted report message indicating that expenditure has been exhausted. + */ + public static String getExpenditureExhaustedReportMessage(String hyperlinkedFullTitle) { + final String openingSpan = spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorWarningHexColor()); + return getFormattedTextAt(RESOURCE_BUNDLE, + "report.format.exhausted", + hyperlinkedFullTitle, + openingSpan, + CLOSING_SPAN_TAG); + } + + /** + * A record that defines spending limits for different types of purchases. + * + * @param major The spending limit for major purchases. + * @param moderate The spending limit for moderate purchases. + * @param minor The spending limit for minor purchases. + */ + public record SpendingLimits(int major, int moderate, int minor) { + } +} diff --git a/MekHQ/src/mekhq/campaign/personnel/Person.java b/MekHQ/src/mekhq/campaign/personnel/Person.java index b233c0645d1..fbe71a231a3 100644 --- a/MekHQ/src/mekhq/campaign/personnel/Person.java +++ b/MekHQ/src/mekhq/campaign/personnel/Person.java @@ -199,6 +199,7 @@ public class Person { private int toughness; private int connections; private int wealth; + private boolean hasPerformedExtremeExpenditure; private int reputation; private int unlucky; private Attributes atowAttributes; @@ -428,6 +429,7 @@ public Person(final String preNominal, final String givenName, final String surn toughness = 0; connections = 0; wealth = 0; + hasPerformedExtremeExpenditure = false; reputation = 0; unlucky = 0; atowAttributes = new Attributes(); @@ -2367,6 +2369,10 @@ public void writeToXML(final PrintWriter pw, int indent, final Campaign campaign MHQXMLUtility.writeSimpleXMLTag(pw, indent, "wealth", wealth); } + if (hasPerformedExtremeExpenditure) { + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "hasPerformedExtremeExpenditure", true); + } + if (reputation != 0) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, "reputation", reputation); } @@ -2785,6 +2791,8 @@ public static Person generateInstanceFromXML(Node wn, Campaign campaign, Version person.connections = MathUtility.parseInt(wn2.getTextContent()); } else if (nodeName.equalsIgnoreCase("wealth")) { person.wealth = MathUtility.parseInt(wn2.getTextContent()); + } else if (nodeName.equalsIgnoreCase("hasPerformedExtremeExpenditure")) { + person.hasPerformedExtremeExpenditure = Boolean.parseBoolean(wn2.getTextContent()); } else if (nodeName.equalsIgnoreCase("reputation")) { person.reputation = MathUtility.parseInt(wn2.getTextContent()); } else if (nodeName.equalsIgnoreCase("unlucky")) { @@ -4927,6 +4935,14 @@ public void setWealth(final int wealth) { this.wealth = wealth; } + public boolean isHasPerformedExtremeExpenditure() { + return hasPerformedExtremeExpenditure; + } + + public void setHasPerformedExtremeExpenditure(final boolean hasPerformedExtremeExpenditure) { + this.hasPerformedExtremeExpenditure = hasPerformedExtremeExpenditure; + } + public int getReputation() { return reputation; } diff --git a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java index 5e02956fd21..63e893d355f 100644 --- a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java @@ -38,6 +38,9 @@ import static mekhq.campaign.mod.am.InjuryTypes.REPLACEMENT_LIMB_COST_LEG_TYPE_5; import static mekhq.campaign.mod.am.InjuryTypes.REPLACEMENT_LIMB_MINIMUM_SKILL_REQUIRED_TYPES_3_4_5; import static mekhq.campaign.mod.am.InjuryTypes.REPLACEMENT_LIMB_RECOVERY; +import static mekhq.campaign.personnel.DiscretionarySpending.getExpenditure; +import static mekhq.campaign.personnel.DiscretionarySpending.getExpenditureExhaustedReportMessage; +import static mekhq.campaign.personnel.DiscretionarySpending.performExtremeExpenditure; import static mekhq.campaign.personnel.Person.*; import static mekhq.campaign.personnel.education.Academy.skillParser; import static mekhq.campaign.personnel.education.EducationController.getAcademy; @@ -47,6 +50,7 @@ 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.SkillType.S_DOCTOR; +import static mekhq.campaign.personnel.skills.enums.SkillAttribute.WILLPOWER; import static mekhq.campaign.randomEvents.personalities.PersonalityController.writePersonalityDescription; import static mekhq.campaign.randomEvents.prisoners.PrisonerEventManager.processAdHocExecution; @@ -189,6 +193,7 @@ public class PersonnelTableMouseAdapter extends JPopupMenuAdapter { private static final String CMD_EDIT_HITS = "EDIT_HITS"; private static final String CMD_EDIT = "EDIT"; private static final String CMD_SACK = "SACK"; + private static final String CMD_SPENDING_SPREE = "SPENDING_SPREE"; private static final String CMD_REMOVE = "REMOVE"; private static final String CMD_EDGE_TRIGGER = "EDGE"; private static final String CMD_CHANGE_PRISONER_STATUS = "PRISONER_STATUS"; @@ -1006,6 +1011,29 @@ public void actionPerformed(ActionEvent action) { } break; } + case CMD_SPENDING_SPREE: { + for (Person person : people) { + if (person.getWealth() > MINIMUM_WEALTH) { + if (person.isHasPerformedExtremeExpenditure()) { + String report = getExpenditureExhaustedReportMessage(person.getHyperlinkedFullTitle()); + getCampaign().addReport(report); + continue; + } + + String report = performExtremeExpenditure(person, + getCampaign().getFinances(), + getCampaign().getLocalDate()); + getCampaign().addReport(report); + + if (!person.isFounder()) { + person.performForcedDirectionLoyaltyChange(getCampaign(), false, true, true); + } + + MekHQ.triggerEvent(new PersonChangedEvent(person)); + } + } + break; + } case CMD_EDIT: { for (Person person : people) { CustomizePersonDialog npd = new CustomizePersonDialog(getFrame(), true, person, getCampaign()); @@ -3272,6 +3300,20 @@ protected Optional createPopupMenu() { popup.add(menuItem); } + if (oneSelected) { + int wealth = person.getWealth(); + int willpower = person.getAttributeScore(WILLPOWER); + int spending = getExpenditure(willpower, wealth); + String spendingString = Money.of(spending).toAmountString(); + menuItem = new JMenuItem(String.format(resources.getString("wealth.extreme.single"), spendingString)); + menuItem.setEnabled(!person.isHasPerformedExtremeExpenditure()); + } else { + menuItem = new JMenuItem(resources.getString("wealth.extreme.multiple")); + } + menuItem.setActionCommand(CMD_SPENDING_SPREE); + menuItem.addActionListener(this); + popup.add(menuItem); + // region Flags Menu // This Menu contains the following flags, in the specified order: // 1) Clan Personnel diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java index a81ebdb8195..7279010e005 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java @@ -27,31 +27,39 @@ */ package mekhq.gui.campaignOptions.contents; +import static mekhq.campaign.parts.enums.PartQuality.QUALITY_F; +import static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel; +import static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory; + +import java.awt.GridBagConstraints; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JSpinner.DefaultEditor; +import javax.swing.JSpinner.NumberEditor; +import javax.swing.JTextField; +import javax.swing.SpinnerNumberModel; + import megamek.client.ui.baseComponents.MMComboBox; import megamek.common.annotations.Nullable; import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; import mekhq.campaign.finances.enums.FinancialYearDuration; import mekhq.campaign.parts.enums.PartQuality; -import mekhq.gui.campaignOptions.components.*; - -import javax.swing.*; -import javax.swing.JSpinner.DefaultEditor; -import javax.swing.JSpinner.NumberEditor; -import java.awt.*; - -import static mekhq.campaign.parts.enums.PartQuality.QUALITY_F; -import static mekhq.gui.campaignOptions.CampaignOptionsUtilities.createParentPanel; -import static mekhq.gui.campaignOptions.CampaignOptionsUtilities.getImageDirectory; +import mekhq.gui.campaignOptions.components.CampaignOptionsCheckBox; +import mekhq.gui.campaignOptions.components.CampaignOptionsGridBagConstraints; +import mekhq.gui.campaignOptions.components.CampaignOptionsHeaderPanel; +import mekhq.gui.campaignOptions.components.CampaignOptionsLabel; +import mekhq.gui.campaignOptions.components.CampaignOptionsSpinner; +import mekhq.gui.campaignOptions.components.CampaignOptionsStandardPanel; /** - * The FinancesTab class represents a UI tab within a larger financial management system - * for a campaign. It provides panels, checkboxes, spinners, combo boxes, and other controls - * to manage and configure various financial options, payments, sales, taxes, shares, - * and price multipliers for the campaign. + * The FinancesTab class represents a UI tab within a larger financial management system for a campaign. It provides + * panels, checkboxes, spinners, combo boxes, and other controls to manage and configure various financial options, + * payments, sales, taxes, shares, and price multipliers for the campaign. *

- * It is primarily composed of multiple `JPanel` sections organized using - * `GroupLayout` for modularity and clarity. + * It is primarily composed of multiple `JPanel` sections organized using `GroupLayout` for modularity and clarity. */ public class FinancesTab { private final Campaign campaign; @@ -67,7 +75,8 @@ public class FinancesTab { private JLabel lblFinancialYearDuration; private MMComboBox comboFinancialYearDuration; private JCheckBox newFinancialYearFinancesToCSVExportBox; - private JCheckBox simulateGrayMonday; + private JCheckBox chkSimulateGrayMonday; + private JCheckBox chkAllowMonthlyReinvestment; private JPanel pnlPayments; private JCheckBox payForPartsBox; @@ -125,11 +134,11 @@ public class FinancesTab { //end Price Multipliers /** - * Constructs a `FinancesTab` instance which manages the financial settings - * and configurations for a specific campaign. + * Constructs a `FinancesTab` instance which manages the financial settings and configurations for a specific + * campaign. * - * @param campaign The `Campaign` object that this `FinancesTab` will be associated with. - * Provides access to campaign-related options and data. + * @param campaign The `Campaign` object that this `FinancesTab` will be associated with. Provides access to + * campaign-related options and data. */ public FinancesTab(Campaign campaign) { this.campaign = campaign; @@ -139,10 +148,9 @@ public FinancesTab(Campaign campaign) { } /** - * Initializes the primary components and subcomponents of the `FinancesTab`. - * Specifically, sets up the 'General Options' and 'Price Multipliers' tabs - * through their respective initialization methods. - * This method ensures that the tabs are prepared prior to being displayed or used. + * Initializes the primary components and subcomponents of the `FinancesTab`. Specifically, sets up the 'General + * Options' and 'Price Multipliers' tabs through their respective initialization methods. This method ensures that + * the tabs are prepared prior to being displayed or used. */ private void initialize() { initializeGeneralOptionsTab(); @@ -152,15 +160,12 @@ private void initialize() { /** * Initializes the General Options tab within the application's UI. *

- * This method sets up various UI components and panels that - * provide configurable options for general settings, payments, - * sales, other systems, taxes, and shares. Components include - * checkboxes, labels, spinners, and combo boxes that allow - * the user to interact with and configure these settings. + * This method sets up various UI components and panels that provide configurable options for general settings, + * payments, sales, other systems, taxes, and shares. Components include checkboxes, labels, spinners, and combo + * boxes that allow the user to interact with and configure these settings. *

- * All UI components are initialized, but additional configuration - * such as layout placements, listeners, or actual visibility might - * need to be completed separately. + * All UI components are initialized, but additional configuration such as layout placements, listeners, or actual + * visibility might need to be completed separately. */ private void initializeGeneralOptionsTab() { // General Options @@ -172,12 +177,12 @@ private void initializeGeneralOptionsTab() { showPeacetimeCostBox = new JCheckBox(); lblFinancialYearDuration = new JLabel(); - comboFinancialYearDuration = new MMComboBox<>("comboFinancialYearDuration", - FinancialYearDuration.values()); + comboFinancialYearDuration = new MMComboBox<>("comboFinancialYearDuration", FinancialYearDuration.values()); newFinancialYearFinancesToCSVExportBox = new JCheckBox(); - simulateGrayMonday = new JCheckBox(); + chkSimulateGrayMonday = new JCheckBox(); + chkAllowMonthlyReinvestment = new JCheckBox(); // Payments pnlPayments = new JPanel(); @@ -204,23 +209,22 @@ private void initializeGeneralOptionsTab() { spnTaxesPercentage = new JSpinner(); // Shares - pnlShares= new JPanel(); + pnlShares = new JPanel(); chkUseShareSystem = new JCheckBox(); chkSharesForAll = new JCheckBox(); } /** - * Creates and configures the Finances General Options tab, assembling its components, - * layout, and panels which include general options, other systems, payments, and sales. - * This method initializes required sub-panels and arranges them within the overall - * structure to create a fully constructed tab for financial general options. + * Creates and configures the Finances General Options tab, assembling its components, layout, and panels which + * include general options, other systems, payments, and sales. This method initializes required sub-panels and + * arranges them within the overall structure to create a fully constructed tab for financial general options. * * @return A fully configured JPanel representing the Finances General Options tab. */ public JPanel createFinancesGeneralOptionsTab() { // Header JPanel headerPanel = new CampaignOptionsHeaderPanel("FinancesGeneralTab", - getImageDirectory() + "logo_star_league.png"); + getImageDirectory() + "logo_star_league.png"); // Contents pnlGeneralOptions = createGeneralOptionsPanel(); @@ -264,9 +268,9 @@ public JPanel createFinancesGeneralOptionsTab() { } /** - * Creates and configures a payments panel with various checkbox options for payment categories such as - * parts, repairs, units, salaries, overhead, maintenance, transport, and recruitment. The layout of - * the panel organizes the checkboxes in a grid-based format. + * Creates and configures a payments panel with various checkbox options for payment categories such as parts, + * repairs, units, salaries, overhead, maintenance, transport, and recruitment. The layout of the panel organizes + * the checkboxes in a grid-based format. * * @return a JPanel instance containing the configured payment options checkboxes. */ @@ -282,8 +286,7 @@ private JPanel createPaymentsPanel() { payForRecruitmentBox = new CampaignOptionsCheckBox("PayForRecruitmentBox"); // Layout the Panel - final JPanel panel = new CampaignOptionsStandardPanel("PaymentsPanel", true, - "PaymentsPanel"); + final JPanel panel = new CampaignOptionsStandardPanel("PaymentsPanel", true, "PaymentsPanel"); final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel); layout.gridx = 0; @@ -315,13 +318,11 @@ private JPanel createPaymentsPanel() { } /** - * Constructs and returns a {@link JPanel} for the 'Other Systems Panel'. - * This panel combines two sub-panels: 'Taxes Panel' and 'Shares Panel'. - * Each sub-panel is added sequentially to the main panel using a grid-bag layout. - * These panels are organized vertically in the resulting panel. + * Constructs and returns a {@link JPanel} for the 'Other Systems Panel'. This panel combines two sub-panels: 'Taxes + * Panel' and 'Shares Panel'. Each sub-panel is added sequentially to the main panel using a grid-bag layout. These + * panels are organized vertically in the resulting panel. * - * @return {@link JPanel} representing the 'Other Systems Panel', containing the - * 'Taxes Panel' and 'Shares Panel'. + * @return {@link JPanel} representing the 'Other Systems Panel', containing the 'Taxes Panel' and 'Shares Panel'. */ private JPanel createOtherSystemsPanel() { // Contents @@ -344,13 +345,11 @@ private JPanel createOtherSystemsPanel() { } /** - * Creates and initializes the General Options Panel with various configurable - * options related to loan limits, maintenance, parts modifiers, peacetime costs, - * and financial year settings. The panel includes checkboxes and labels for easy - * user interaction and configuration of these parameters. + * Creates and initializes the General Options Panel with various configurable options related to loan limits, + * maintenance, parts modifiers, peacetime costs, and financial year settings. The panel includes checkboxes and + * labels for easy user interaction and configuration of these parameters. * - * @return A JPanel containing the general options components laid out in a - * structured format. + * @return A JPanel containing the general options components laid out in a structured format. */ private JPanel createGeneralOptionsPanel() { // Contents @@ -364,7 +363,8 @@ private JPanel createGeneralOptionsPanel() { newFinancialYearFinancesToCSVExportBox = new CampaignOptionsCheckBox("NewFinancialYearFinancesToCSVExportBox"); - simulateGrayMonday = new CampaignOptionsCheckBox("SimulateGrayMonday"); + chkSimulateGrayMonday = new CampaignOptionsCheckBox("SimulateGrayMonday"); + chkAllowMonthlyReinvestment = new CampaignOptionsCheckBox("AllowMonthlyReinvestment"); // Layout the Panel final JPanel panel = new CampaignOptionsStandardPanel("GeneralOptionsPanel"); @@ -399,16 +399,18 @@ private JPanel createGeneralOptionsPanel() { panel.add(newFinancialYearFinancesToCSVExportBox, layout); layout.gridy++; - panel.add(simulateGrayMonday, layout); + panel.add(chkSimulateGrayMonday, layout); + + layout.gridy++; + panel.add(chkAllowMonthlyReinvestment, layout); return panel; } /** - * Creates and configures the sales panel within the finance tab. - * The panel contains checkboxes for options related to sales, including - * "Sell Units" and "Sell Parts". These checkboxes are added to a layout - * that organizes the components vertically. + * Creates and configures the sales panel within the finance tab. The panel contains checkboxes for options related + * to sales, including "Sell Units" and "Sell Parts". These checkboxes are added to a layout that organizes the + * components vertically. * * @return A JPanel instance containing the configured sales options. */ @@ -418,8 +420,7 @@ private JPanel createSalesPanel() { sellPartsBox = new CampaignOptionsCheckBox("SellPartsBox"); // Layout the Panel - final JPanel panel = new CampaignOptionsStandardPanel("SalesPanel", true, - "SalesPanel"); + final JPanel panel = new CampaignOptionsStandardPanel("SalesPanel", true, "SalesPanel"); final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel); layout.gridx = 0; @@ -434,9 +435,8 @@ private JPanel createSalesPanel() { } /** - * Creates and returns a JPanel representing the taxes panel in the campaign options. - * This panel includes a checkbox to enable or disable taxes and a spinner - * to set the percentage of taxes, along with corresponding labels. + * Creates and returns a JPanel representing the taxes panel in the campaign options. This panel includes a checkbox + * to enable or disable taxes and a spinner to set the percentage of taxes, along with corresponding labels. * * @return the configured JPanel containing the components for the taxes panel. */ @@ -445,12 +445,10 @@ private JPanel createTaxesPanel() { chkUseTaxes = new CampaignOptionsCheckBox("UseTaxesBox"); lblTaxesPercentage = new CampaignOptionsLabel("TaxesPercentage"); - spnTaxesPercentage = new CampaignOptionsSpinner("TaxesPercentage", - 30, 1, 100, 1); + spnTaxesPercentage = new CampaignOptionsSpinner("TaxesPercentage", 30, 1, 100, 1); // Layout the Panel - final JPanel panel = new CampaignOptionsStandardPanel("TaxesPanel", true, - "TaxesPanel"); + final JPanel panel = new CampaignOptionsStandardPanel("TaxesPanel", true, "TaxesPanel"); final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel); layout.gridx = 0; @@ -470,8 +468,8 @@ private JPanel createTaxesPanel() { /** * Creates and returns a JPanel representing the 'Shares Panel' within the finance tab. *

- * The panel is laid out using grid-based constraints to position the components - * in a structured vertical arrangement. + * The panel is laid out using grid-based constraints to position the components in a structured vertical + * arrangement. * * @return A JPanel containing the configured components for the 'Shares Panel'. */ @@ -481,8 +479,7 @@ private JPanel createSharesPanel() { chkSharesForAll = new CampaignOptionsCheckBox("SharesForAll"); // Layout the Panel - final JPanel panel = new CampaignOptionsStandardPanel("SharesPanel", true, - "SharesPanel"); + final JPanel panel = new CampaignOptionsStandardPanel("SharesPanel", true, "SharesPanel"); final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel); layout.gridx = 0; @@ -497,15 +494,12 @@ private JPanel createSharesPanel() { } /** - * Initializes the components and layout for the price multipliers tab. - * This tab includes controls for setting various price multipliers such as - * - General multipliers for unit and part prices. - * - Multipliers for used parts. - * - Miscellaneous multipliers for damaged, unrepairable parts, and order refunds. + * Initializes the components and layout for the price multipliers tab. This tab includes controls for setting + * various price multipliers such as - General multipliers for unit and part prices. - Multipliers for used parts. - + * Miscellaneous multipliers for damaged, unrepairable parts, and order refunds. *

- * The method creates and assigns UI components including panels, labels, and spinners - * to their respective class fields. Each field corresponds to a specific category - * of price multiplier. + * The method creates and assigns UI components including panels, labels, and spinners to their respective class + * fields. Each field corresponds to a specific category of price multiplier. */ private void initializePriceMultipliersTab() { pnlGeneralMultipliers = new JPanel(); @@ -536,17 +530,17 @@ private void initializePriceMultipliersTab() { } /** - * Creates and returns a JPanel representing the "Price Multipliers" tab in the user interface. - * The method includes a header section, general multipliers panel, used parts multipliers panel, - * and other multipliers panel. These components are arranged using a specific layout and added - * to a parent panel. + * Creates and returns a JPanel representing the "Price Multipliers" tab in the user interface. The method includes + * a header section, general multipliers panel, used parts multipliers panel, and other multipliers panel. These + * components are arranged using a specific layout and added to a parent panel. * * @return a JPanel representing the "Price Multipliers" tab with all its components and layout configured */ public JPanel createPriceMultipliersTab() { // Header JPanel headerPanel = new CampaignOptionsHeaderPanel("PriceMultipliersTab", - getImageDirectory() + "logo_clan_stone_lion.png", true); + getImageDirectory() + "logo_clan_stone_lion.png", + true); // Contents pnlGeneralMultipliers = createGeneralMultipliersPanel(); @@ -575,42 +569,48 @@ public JPanel createPriceMultipliersTab() { } /** - * Creates and configures the general multipliers panel, which includes labels - * and spinners for various pricing multipliers such as common parts, Inner Sphere - * units, Inner Sphere parts, Clan units, Clan parts, and mixed tech units. - * The panel is structured using a grid layout for organized placement of components. + * Creates and configures the general multipliers panel, which includes labels and spinners for various pricing + * multipliers such as common parts, Inner Sphere units, Inner Sphere parts, Clan units, Clan parts, and mixed tech + * units. The panel is structured using a grid layout for organized placement of components. * * @return a JPanel containing the components for setting general multipliers. */ private JPanel createGeneralMultipliersPanel() { // Contents lblCommonPartPriceMultiplier = new CampaignOptionsLabel("CommonPartPriceMultiplier"); - spnCommonPartPriceMultiplier = new CampaignOptionsSpinner("CommonPartPriceMultiplier", - 1.0, 0.1, 100, 0.1); + spnCommonPartPriceMultiplier = new CampaignOptionsSpinner("CommonPartPriceMultiplier", 1.0, 0.1, 100, 0.1); lblInnerSphereUnitPriceMultiplier = new CampaignOptionsLabel("InnerSphereUnitPriceMultiplier"); spnInnerSphereUnitPriceMultiplier = new CampaignOptionsSpinner("InnerSphereUnitPriceMultiplier", - 1.0, 0.1, 100, 0.1); + 1.0, + 0.1, + 100, + 0.1); lblInnerSpherePartPriceMultiplier = new CampaignOptionsLabel("InnerSpherePartPriceMultiplier"); spnInnerSpherePartPriceMultiplier = new CampaignOptionsSpinner("InnerSpherePartPriceMultiplier", - 1.0, 0.1, 100, 0.1); + 1.0, + 0.1, + 100, + 0.1); lblClanUnitPriceMultiplier = new CampaignOptionsLabel("ClanUnitPriceMultiplier"); - spnClanUnitPriceMultiplier = new CampaignOptionsSpinner("ClanUnitPriceMultiplier", - 1.0, 0.1, 100, 0.1); + spnClanUnitPriceMultiplier = new CampaignOptionsSpinner("ClanUnitPriceMultiplier", 1.0, 0.1, 100, 0.1); lblClanPartPriceMultiplier = new CampaignOptionsLabel("ClanPartPriceMultiplier"); - spnClanPartPriceMultiplier = new CampaignOptionsSpinner("ClanPartPriceMultiplier", - 1.0, 0.1, 100, 0.1); + spnClanPartPriceMultiplier = new CampaignOptionsSpinner("ClanPartPriceMultiplier", 1.0, 0.1, 100, 0.1); lblMixedTechUnitPriceMultiplier = new CampaignOptionsLabel("MixedTechUnitPriceMultiplier"); spnMixedTechUnitPriceMultiplier = new CampaignOptionsSpinner("MixedTechUnitPriceMultiplier", - 1.0, 0.1, 100, 0.1); + 1.0, + 0.1, + 100, + 0.1); // Layout the Panel - final JPanel panel = new CampaignOptionsStandardPanel("GeneralMultipliersPanel", true, - "GeneralMultipliersPanel"); + final JPanel panel = new CampaignOptionsStandardPanel("GeneralMultipliersPanel", + true, + "GeneralMultipliersPanel"); final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel); layout.gridx = 0; @@ -648,16 +648,14 @@ private JPanel createGeneralMultipliersPanel() { } /** - * Creates and returns a JPanel for configuring used parts price multipliers - * based on part quality. Each part quality level is represented with a label - * and a spinner for adjusting the multiplier value. + * Creates and returns a JPanel for configuring used parts price multipliers based on part quality. Each part + * quality level is represented with a label and a spinner for adjusting the multiplier value. *

- * The spinners are initialized with a range of values from 0.00 to 1.00, - * incrementing by 0.05, and include formatting for two decimal places. - * Additionally, the alignment of the spinner text fields is set to left. + * The spinners are initialized with a range of values from 0.00 to 1.00, incrementing by 0.05, and include + * formatting for two decimal places. Additionally, the alignment of the spinner text fields is set to left. *

- * The panel is arranged using GridBagLayout to ensure proper alignment - * between labels and spinners for each quality level. + * The panel is arranged using GridBagLayout to ensure proper alignment between labels and spinners for each quality + * level. * * @return A JPanel containing labels and spinners for used parts price multipliers. */ @@ -673,19 +671,19 @@ private JPanel createUsedPartsMultiplierPanel() { lblUsedPartPriceMultipliers[ordinal] = new JLabel(qualityLevel); lblUsedPartPriceMultipliers[ordinal].setName("lbl" + qualityLevel); - spnUsedPartPriceMultipliers[ordinal] = new JSpinner( - new SpinnerNumberModel(0.00, 0.00, 1.00, 0.05)); + spnUsedPartPriceMultipliers[ordinal] = new JSpinner(new SpinnerNumberModel(0.00, 0.00, 1.00, 0.05)); spnUsedPartPriceMultipliers[ordinal].setName("spn" + qualityLevel); - spnUsedPartPriceMultipliers[ordinal] - .setEditor(new NumberEditor(spnUsedPartPriceMultipliers[ordinal], "0.00")); + spnUsedPartPriceMultipliers[ordinal].setEditor(new NumberEditor(spnUsedPartPriceMultipliers[ordinal], + "0.00")); DefaultEditor editor = (DefaultEditor) spnUsedPartPriceMultipliers[ordinal].getEditor(); editor.getTextField().setHorizontalAlignment(JTextField.LEFT); } // Layout the Panel - final JPanel panel = new CampaignOptionsStandardPanel("UsedPartsMultiplierPanel", true, - "UsedPartsMultiplierPanel"); + final JPanel panel = new CampaignOptionsStandardPanel("UsedPartsMultiplierPanel", + true, + "UsedPartsMultiplierPanel"); final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel); layout.gridwidth = 1; @@ -702,10 +700,9 @@ private JPanel createUsedPartsMultiplierPanel() { } /** - * Creates and returns a JPanel configured with components for adjusting - * multipliers related to damaged parts value, unrepairable parts value, - * and cancelled order refunds. Each multiplier is represented with a label - * and an associated configurable spinner control. + * Creates and returns a JPanel configured with components for adjusting multipliers related to damaged parts value, + * unrepairable parts value, and cancelled order refunds. Each multiplier is represented with a label and an + * associated configurable spinner control. * * @return a JPanel instance containing the components for configuring the multipliers. */ @@ -713,19 +710,27 @@ private JPanel createOtherMultipliersPanel() { // Contents lblDamagedPartsValueMultiplier = new CampaignOptionsLabel("DamagedPartsValueMultiplier"); spnDamagedPartsValueMultiplier = new CampaignOptionsSpinner("DamagedPartsValueMultiplier", - 0.33, 0.00, 1.00, 0.05); + 0.33, + 0.00, + 1.00, + 0.05); lblUnrepairablePartsValueMultiplier = new CampaignOptionsLabel("UnrepairablePartsValueMultiplier"); spnUnrepairablePartsValueMultiplier = new CampaignOptionsSpinner("UnrepairablePartsValueMultiplier", - 0.10, 0.00, 1.00, 0.05); + 0.10, + 0.00, + 1.00, + 0.05); lblCancelledOrderRefundMultiplier = new CampaignOptionsLabel("CancelledOrderRefundMultiplier"); spnCancelledOrderRefundMultiplier = new CampaignOptionsSpinner("CancelledOrderRefundMultiplier", - 0.50, 0.00, 1.00, 0.05); + 0.50, + 0.00, + 1.00, + 0.05); // Layout the Panel - final JPanel panel = new CampaignOptionsStandardPanel("OtherMultipliersPanel", true, - "OtherMultipliersPanel"); + final JPanel panel = new CampaignOptionsStandardPanel("OtherMultipliersPanel", true, "OtherMultipliersPanel"); final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel); layout.gridx = 0; @@ -751,12 +756,10 @@ private JPanel createOtherMultipliersPanel() { } /** - * Applies the specified campaign options to the corresponding campaign settings. - * If no campaign options are provided, default options are used instead. + * Applies the specified campaign options to the corresponding campaign settings. If no campaign options are + * provided, default options are used instead. * - * @param presetCampaignOptions - * The campaign options to be applied. If null, default campaign options - * are applied. + * @param presetCampaignOptions The campaign options to be applied. If null, default campaign options are applied. */ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampaignOptions) { CampaignOptions options = presetCampaignOptions; @@ -772,7 +775,8 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa options.setShowPeacetimeCost(showPeacetimeCostBox.isSelected()); options.setFinancialYearDuration(comboFinancialYearDuration.getSelectedItem()); options.setNewFinancialYearFinancesToCSVExport(newFinancialYearFinancesToCSVExportBox.isSelected()); - options.setSimulateGrayMonday(simulateGrayMonday.isSelected()); + options.setSimulateGrayMonday(chkSimulateGrayMonday.isSelected()); + options.setAllowMonthlyReinvestment(chkAllowMonthlyReinvestment.isSelected()); options.setPayForParts(payForPartsBox.isSelected()); options.setPayForRepairs(payForRepairsBox.isSelected()); options.setPayForUnits(payForUnitsBox.isSelected()); @@ -796,8 +800,7 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa options.setClanPartPriceMultiplier((double) spnClanPartPriceMultiplier.getValue()); options.setMixedTechUnitPriceMultiplier((double) spnMixedTechUnitPriceMultiplier.getValue()); for (int i = 0; i < spnUsedPartPriceMultipliers.length; i++) { - options.getUsedPartPriceMultipliers()[i] = (Double) spnUsedPartPriceMultipliers[i] - .getValue(); + options.getUsedPartPriceMultipliers()[i] = (Double) spnUsedPartPriceMultipliers[i].getValue(); } options.setDamagedPartsValueMultiplier((double) spnDamagedPartsValueMultiplier.getValue()); options.setUnrepairablePartsValueMultiplier((double) spnUnrepairablePartsValueMultiplier.getValue()); @@ -805,24 +808,24 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa } /** - * Loads configuration values from the current campaign options to populate - * the financial settings and related UI components in the `FinancesTab`. + * Loads configuration values from the current campaign options to populate the financial settings and related UI + * components in the `FinancesTab`. *

* This method is a convenience overload that invokes the overloaded - * {@link #loadValuesFromCampaignOptions(CampaignOptions)} method with a - * `null` parameter, ensuring that default campaign options will be loaded. + * {@link #loadValuesFromCampaignOptions(CampaignOptions)} method with a `null` parameter, ensuring that default + * campaign options will be loaded. */ public void loadValuesFromCampaignOptions() { loadValuesFromCampaignOptions(null); } /** - * Loads and applies the values from the provided campaign options or the default campaign options - * if the provided options are null. Updates various UI components and internal variables based - * on the configuration of the campaign options. + * Loads and applies the values from the provided campaign options or the default campaign options if the provided + * options are null. Updates various UI components and internal variables based on the configuration of the campaign + * options. * - * @param presetCampaignOptions the campaign options to load values from; if null, the default - * campaign options will be used + * @param presetCampaignOptions the campaign options to load values from; if null, the default campaign options will + * be used */ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampaignOptions) { CampaignOptions options = presetCampaignOptions; @@ -838,7 +841,8 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai showPeacetimeCostBox.setSelected(options.isShowPeacetimeCost()); comboFinancialYearDuration.setSelectedItem(options.getFinancialYearDuration()); newFinancialYearFinancesToCSVExportBox.setSelected(options.isNewFinancialYearFinancesToCSVExport()); - simulateGrayMonday.setSelected(options.isSimulateGrayMonday()); + chkSimulateGrayMonday.setSelected(options.isSimulateGrayMonday()); + chkAllowMonthlyReinvestment.setSelected(options.isAllowMonthlyReinvestment()); payForPartsBox.setSelected(options.isPayForParts()); payForRepairsBox.setSelected(options.isPayForRepairs()); payForUnitsBox.setSelected(options.isPayForUnits()); From 99febeec3b6c37421a47dd7b915b8c26484bcaae Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 6 Apr 2025 04:54:11 -0500 Subject: [PATCH 2/2] Refactored Wealth Adjustment Logic To Use `changeWealth` Method - Replaced direct `setWealth` call with the new `changeWealth` method in `DiscretionarySpending` class for clarity and better encapsulation. - Added `changeWealth` method to `Person` class that adjusts the wealth by a specified delta, enhancing code maintainability. - Documented the new `changeWealth` method with clear Javadoc comments. --- .../campaign/personnel/DiscretionarySpending.java | 2 +- MekHQ/src/mekhq/campaign/personnel/Person.java | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/personnel/DiscretionarySpending.java b/MekHQ/src/mekhq/campaign/personnel/DiscretionarySpending.java index d812bd185f4..4ce7377f901 100644 --- a/MekHQ/src/mekhq/campaign/personnel/DiscretionarySpending.java +++ b/MekHQ/src/mekhq/campaign/personnel/DiscretionarySpending.java @@ -246,7 +246,7 @@ public static String performExtremeExpenditure(Person person, Finances finances, return ""; } - person.setWealth(wealth - 1); + person.changeWealth(-1); person.setHasPerformedExtremeExpenditure(true); int totalSpending = getExpenditure(person.getAttributeScore(WILLPOWER), wealth); diff --git a/MekHQ/src/mekhq/campaign/personnel/Person.java b/MekHQ/src/mekhq/campaign/personnel/Person.java index fbe71a231a3..20e6238ef83 100644 --- a/MekHQ/src/mekhq/campaign/personnel/Person.java +++ b/MekHQ/src/mekhq/campaign/personnel/Person.java @@ -4935,6 +4935,18 @@ public void setWealth(final int wealth) { this.wealth = wealth; } + /** + * Adjusts the person's wealth by the specified amount. + * + *

The change in wealth can be positive or negative, depending on the provided delta value.

+ * + * @param delta The amount by which to adjust the wealth. A positive value increases the wealth, while a negative + * value decreases it. + */ + public void changeWealth(final int delta) { + this.wealth += delta; + } + public boolean isHasPerformedExtremeExpenditure() { return hasPerformedExtremeExpenditure; }